From be6fd25190e6b0f3abac2fdc5983b3cb5cf9c79a Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 15 Nov 2023 12:47:14 -0800 Subject: [PATCH] implement proper equip/unequip tracking --- src/CommandFormats.hh | 14 ++++-- src/ItemData.cc | 20 ++++++++ src/ItemData.hh | 15 +++++- src/Items.cc | 16 +++---- src/PlayerSubordinates.cc | 97 +++++++++++++++++++++++---------------- src/PlayerSubordinates.hh | 7 +-- src/ReceiveSubcommands.cc | 38 ++++++++++----- 7 files changed, 140 insertions(+), 67 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index de489995..263719df 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -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 diff --git a/src/ItemData.cc b/src/ItemData.cc index ab5e26f8..8174c2ea 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -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]) { diff --git a/src/ItemData.hh b/src/ItemData.hh index f3c79e29..a4b4640a 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -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); diff --git a/src/Items.cc b/src/Items.cc index bdad98b6..a938c279 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -36,7 +36,7 @@ void player_use_item(shared_ptr 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 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 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) { diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index d89b9423..a2c10872 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -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(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) { diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index d0e690a8..fd614165 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -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); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 2659b37b..336374d2 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -625,7 +625,7 @@ static void on_player_died(shared_ptr 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(static_cast(data.data2[0] - 5), 0); } catch (const out_of_range&) { @@ -1146,8 +1146,27 @@ static void on_pick_up_item_request(shared_ptr c, uint8_t command, uint8 } } -static void on_equip_unequip_item(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - const auto& cmd = check_size_t(data, size); +static void on_equip_item(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + const auto& cmd = check_size_t(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(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 c, uint8_t command, uint8_t flag, const void* data, size_t size) { + const auto& cmd = check_size_t(data, size); if (cmd.header.client_id != c->lobby_client_id) { return; @@ -1156,12 +1175,7 @@ static void on_equip_unequip_item(shared_ptr 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 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,