implement proper equip/unequip tracking

This commit is contained in:
Martin Michelsen
2023-11-15 12:47:14 -08:00
parent ac57fb16a4
commit be6fd25190
7 changed files with 140 additions and 67 deletions
+11 -3
View File
@@ -3918,12 +3918,20 @@ struct G_TeleportPlayer_6x24 {
} __packed__;
// 6x25: Equip item
// 6x26: Unequip item
struct G_EquipOrUnequipItem_6x25_6x26 {
struct G_EquipItem_6x25 {
G_ClientIDHeader header;
le_uint32_t item_id = 0;
le_uint32_t equip_slot = 0; // Unused for 6x26 (unequip item)
// Values here match the EquipSlot enum (in ItemData.hh)
le_uint32_t equip_slot = 0;
} __packed__;
// 6x26: Unequip item
struct G_UnequipItem_6x26 {
G_ClientIDHeader header;
le_uint32_t item_id = 0;
le_uint32_t unused = 0;
} __packed__;
// 6x27: Use item
+20
View File
@@ -567,6 +567,26 @@ bool ItemData::is_s_rank_weapon() const {
return false;
}
bool ItemData::can_be_equipped_in_slot(EquipSlot slot) const {
switch (slot) {
case EquipSlot::MAG:
return (this->data1[0] == 0x02);
case EquipSlot::ARMOR:
return ((this->data1[0] == 0x01) && (this->data1[1] == 0x01));
case EquipSlot::SHIELD:
return ((this->data1[0] == 0x01) && (this->data1[1] == 0x02));
case EquipSlot::UNIT_1:
case EquipSlot::UNIT_2:
case EquipSlot::UNIT_3:
case EquipSlot::UNIT_4:
return ((this->data1[0] == 0x01) && (this->data1[1] == 0x03));
case EquipSlot::WEAPON:
return (this->data1[0] == 0x00);
default:
throw runtime_error("invalid equip slot");
}
}
bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) {
for (size_t z = 0; z < 12; z++) {
if (a.data1[z] < b.data1[z]) {
+14 -1
View File
@@ -6,10 +6,21 @@
#include "Text.hh"
#include "Version.hh"
constexpr uint32_t MESETA_IDENTIFIER = 0x00040000;
constexpr uint32_t MESETA_IDENTIFIER = 0x040000;
class ItemParameterTable;
enum class EquipSlot {
MAG = 0x01,
ARMOR = 0x02,
SHIELD = 0x03,
WEAPON = 0x06,
UNIT_1 = 0x09,
UNIT_2 = 0x0A,
UNIT_3 = 0x0B,
UNIT_4 = 0x0C,
};
struct ItemMagStats {
uint16_t iq;
uint16_t synchro;
@@ -152,6 +163,8 @@ struct ItemData { // 0x14 bytes
bool has_bonuses() const;
bool is_s_rank_weapon() const;
bool can_be_equipped_in_slot(EquipSlot slot) const;
bool empty() const;
static bool compare_for_sort(const ItemData& a, const ItemData& b);
+8 -8
View File
@@ -36,7 +36,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
throw runtime_error("incorrect grinder value");
}
auto& weapon = player->inventory.items[player->inventory.find_equipped_weapon()];
auto& weapon = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::WEAPON)];
// Don't enforce the weapon's grind limit on V1 and V2. This is necessary
// because the V2 client replaces its inventory items on the fly with items
// compatible with V1 when sending the 61 and 98 commands. There appears to
@@ -107,7 +107,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
}
} else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot
auto& armor = player->inventory.items[player->inventory.find_equipped_armor()];
auto& armor = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::ARMOR)];
if (armor.data.data1[5] >= 4) {
throw runtime_error("armor already at maximum slot count");
}
@@ -140,32 +140,32 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
} else if (item_identifier == 0x030C00) {
// Cell of MAG 502
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21;
} else if (item_identifier == 0x030C01) {
// Cell of MAG 213
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22;
} else if (item_identifier == 0x030C02) {
// Parts of RoboChao
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = 0x28;
} else if (item_identifier == 0x030C03) {
// Heart of Opa Opa
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = 0x29;
} else if (item_identifier == 0x030C04) {
// Heart of Pian
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = 0x2A;
} else if (item_identifier == 0x030C05) {
// Heart of Chao
auto& mag = player->inventory.items[player->inventory.find_equipped_mag()];
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
mag.data.data1[1] = 0x2B;
} else if ((item_identifier & 0xFFFF00) == 0x031500) {
+57 -40
View File
@@ -525,67 +525,84 @@ size_t PlayerInventory::find_item_by_primary_identifier(uint32_t primary_identif
throw out_of_range("item not present");
}
size_t PlayerInventory::find_equipped_weapon() const {
size_t PlayerInventory::find_equipped_item(EquipSlot slot) const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
const auto& i = this->items[y];
if (!(i.flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 0) {
if (!i.data.can_be_equipped_in_slot(slot)) {
continue;
}
// Units can be equipped in multiple slots, so the currently-equipped slot
// is stored in the item data itself.
if (((slot == EquipSlot::UNIT_1) && (i.data.data1[4] != 0x00)) ||
((slot == EquipSlot::UNIT_2) && (i.data.data1[4] != 0x01)) ||
((slot == EquipSlot::UNIT_3) && (i.data.data1[4] != 0x02)) ||
((slot == EquipSlot::UNIT_4) && (i.data.data1[4] != 0x03))) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple weapons are equipped");
throw runtime_error("multiple items are equipped in the same slot");
}
}
if (ret < 0) {
throw out_of_range("no weapon is equipped");
throw out_of_range("no item is equipped in this slot");
}
return ret;
}
size_t PlayerInventory::find_equipped_armor() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 1 || this->items[y].data.data1[1] != 1) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple armors are equipped");
}
bool PlayerInventory::has_equipped_item(EquipSlot slot) const {
try {
this->find_equipped_item(slot);
return true;
} catch (const out_of_range&) {
return false;
}
if (ret < 0) {
throw out_of_range("no armor is equipped");
}
return ret;
}
size_t PlayerInventory::find_equipped_mag() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 2) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple mags are equipped");
void PlayerInventory::equip_item(uint32_t item_id, EquipSlot slot) {
size_t index = this->find_item(item_id);
auto& item = this->items[index];
if (!item.data.can_be_equipped_in_slot(slot)) {
throw runtime_error("incorrect item type for equip slot");
}
if (this->has_equipped_item(slot)) {
throw runtime_error("equip slot is already in use");
}
item.flags |= 0x00000008;
// Units store which slot they're equipped in within the item data itself
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
item.data.data1[4] = static_cast<uint8_t>(slot) - 9;
}
}
void PlayerInventory::unequip_item(uint32_t item_id) {
size_t index = this->find_item(item_id);
auto& item = this->items[index];
item.flags &= (~0x00000008);
// Units store which slot they're equipped in within the item data itself
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
item.data.data1[4] = 0x00;
}
// If the item is an armor, remove all units too
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x01)) {
for (size_t z = 0; z < 30; z++) {
auto& unit = this->items[z];
if ((unit.data.data1[0] == 0x01) && (unit.data.data1[1] == 0x03)) {
unit.flags &= (~0x00000008);
unit.data.data1[4] = 0x00;
}
}
}
if (ret < 0) {
throw out_of_range("no mag is equipped");
}
return ret;
}
size_t PlayerInventory::remove_all_items_of_type(uint8_t data1_0, int16_t data1_1) {
+4 -3
View File
@@ -71,9 +71,10 @@ struct PlayerInventory {
size_t find_item(uint32_t item_id) const;
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
size_t find_equipped_weapon() const;
size_t find_equipped_armor() const;
size_t find_equipped_mag() const;
size_t find_equipped_item(EquipSlot slot) const;
bool has_equipped_item(EquipSlot slot) const;
void equip_item(uint32_t item_id, EquipSlot slot);
void unequip_item(uint32_t item_id);
size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1);
+26 -12
View File
@@ -625,7 +625,7 @@ static void on_player_died(shared_ptr<Client> c, uint8_t command, uint8_t flag,
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
try {
auto& inventory = c->game_data.character()->inventory;
size_t mag_index = inventory.find_equipped_mag();
size_t mag_index = inventory.find_equipped_item(EquipSlot::MAG);
auto& data = inventory.items[mag_index].data;
data.data2[0] = max<int8_t>(static_cast<int8_t>(data.data2[0] - 5), 0);
} catch (const out_of_range&) {
@@ -1146,8 +1146,27 @@ static void on_pick_up_item_request(shared_ptr<Client> c, uint8_t command, uint8
}
}
static void on_equip_unequip_item(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_EquipOrUnequipItem_6x25_6x26>(data, size);
static void on_equip_item(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_EquipItem_6x25>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
return;
}
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
EquipSlot slot = static_cast<EquipSlot>(cmd.equip_slot.load());
auto p = c->game_data.character();
p->inventory.equip_item(cmd.item_id, slot);
} else if (l->base_version == GameVersion::BB) {
throw logic_error("item tracking not enabled in BB game");
}
forward_subcommand(c, command, flag, data, size);
}
static void on_unequip_item(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_UnequipItem_6x26>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
return;
@@ -1156,12 +1175,7 @@ static void on_equip_unequip_item(shared_ptr<Client> c, uint8_t command, uint8_t
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.character();
size_t index = p->inventory.find_item(cmd.item_id);
if (cmd.header.subcommand == 0x25) { // Equip
p->inventory.items[index].flags |= 0x00000008;
} else { // Unequip
p->inventory.items[index].flags &= 0xFFFFFFF7;
}
p->inventory.unequip_item(cmd.item_id);
} else if (l->base_version == GameVersion::BB) {
throw logic_error("item tracking not enabled in BB game");
}
@@ -1833,7 +1847,7 @@ static void on_steal_exp_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
auto p = c->game_data.character();
const auto& enemy = l->map->enemies.at(cmd.enemy_index);
const auto& inventory = p->inventory;
const auto& weapon = inventory.items[inventory.find_equipped_weapon()];
const auto& weapon = inventory.items[inventory.find_equipped_item(EquipSlot::WEAPON)];
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
@@ -2524,8 +2538,8 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x22 */ on_forward_check_size_client,
/* 6x23 */ on_set_player_visibility,
/* 6x24 */ on_forward_check_size_game,
/* 6x25 */ on_equip_unequip_item,
/* 6x26 */ on_equip_unequip_item,
/* 6x25 */ on_equip_item,
/* 6x26 */ on_unequip_item,
/* 6x27 */ on_use_item,
/* 6x28 */ on_feed_mag,
/* 6x29 */ on_destroy_inventory_item,