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__; } __packed__;
// 6x25: Equip item // 6x25: Equip item
// 6x26: Unequip item
struct G_EquipOrUnequipItem_6x25_6x26 { struct G_EquipItem_6x25 {
G_ClientIDHeader header; G_ClientIDHeader header;
le_uint32_t item_id = 0; 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__; } __packed__;
// 6x27: Use item // 6x27: Use item
+20
View File
@@ -567,6 +567,26 @@ bool ItemData::is_s_rank_weapon() const {
return false; 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) { bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) {
for (size_t z = 0; z < 12; z++) { for (size_t z = 0; z < 12; z++) {
if (a.data1[z] < b.data1[z]) { if (a.data1[z] < b.data1[z]) {
+14 -1
View File
@@ -6,10 +6,21 @@
#include "Text.hh" #include "Text.hh"
#include "Version.hh" #include "Version.hh"
constexpr uint32_t MESETA_IDENTIFIER = 0x00040000; constexpr uint32_t MESETA_IDENTIFIER = 0x040000;
class ItemParameterTable; 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 { struct ItemMagStats {
uint16_t iq; uint16_t iq;
uint16_t synchro; uint16_t synchro;
@@ -152,6 +163,8 @@ struct ItemData { // 0x14 bytes
bool has_bonuses() const; bool has_bonuses() const;
bool is_s_rank_weapon() const; bool is_s_rank_weapon() const;
bool can_be_equipped_in_slot(EquipSlot slot) const;
bool empty() const; bool empty() const;
static bool compare_for_sort(const ItemData& a, const ItemData& b); 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"); 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 // 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 // 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 // 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 } 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) { if (armor.data.data1[5] >= 4) {
throw runtime_error("armor already at maximum slot count"); 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) { } else if (item_identifier == 0x030C00) {
// Cell of MAG 502 // 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; mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21;
} else if (item_identifier == 0x030C01) { } else if (item_identifier == 0x030C01) {
// Cell of MAG 213 // 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; mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22;
} else if (item_identifier == 0x030C02) { } else if (item_identifier == 0x030C02) {
// Parts of RoboChao // 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; mag.data.data1[1] = 0x28;
} else if (item_identifier == 0x030C03) { } else if (item_identifier == 0x030C03) {
// Heart of Opa Opa // 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; mag.data.data1[1] = 0x29;
} else if (item_identifier == 0x030C04) { } else if (item_identifier == 0x030C04) {
// Heart of Pian // 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; mag.data.data1[1] = 0x2A;
} else if (item_identifier == 0x030C05) { } else if (item_identifier == 0x030C05) {
// Heart of Chao // 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; mag.data.data1[1] = 0x2B;
} else if ((item_identifier & 0xFFFF00) == 0x031500) { } 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"); 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; ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) { 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; continue;
} }
if (this->items[y].data.data1[0] != 0) { if (!i.data.can_be_equipped_in_slot(slot)) {
continue; 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) { if (ret < 0) {
ret = y; ret = y;
} else { } else {
throw runtime_error("multiple weapons are equipped"); throw runtime_error("multiple items are equipped in the same slot");
} }
} }
if (ret < 0) { if (ret < 0) {
throw out_of_range("no weapon is equipped"); throw out_of_range("no item is equipped in this slot");
} }
return ret; return ret;
} }
size_t PlayerInventory::find_equipped_armor() const { bool PlayerInventory::has_equipped_item(EquipSlot slot) const {
ssize_t ret = -1; try {
for (size_t y = 0; y < this->num_items; y++) { this->find_equipped_item(slot);
if (!(this->items[y].flags & 0x00000008)) { return true;
continue; } catch (const out_of_range&) {
} return false;
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");
}
} }
if (ret < 0) {
throw out_of_range("no armor is equipped");
}
return ret;
} }
size_t PlayerInventory::find_equipped_mag() const { void PlayerInventory::equip_item(uint32_t item_id, EquipSlot slot) {
ssize_t ret = -1; size_t index = this->find_item(item_id);
for (size_t y = 0; y < this->num_items; y++) { auto& item = this->items[index];
if (!(this->items[y].flags & 0x00000008)) {
continue; if (!item.data.can_be_equipped_in_slot(slot)) {
} throw runtime_error("incorrect item type for equip slot");
if (this->items[y].data.data1[0] != 2) { }
continue; if (this->has_equipped_item(slot)) {
} throw runtime_error("equip slot is already in use");
if (ret < 0) { }
ret = y;
} else { item.flags |= 0x00000008;
throw runtime_error("multiple mags are equipped"); // 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) { 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(uint32_t item_id) const;
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const; size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
size_t find_equipped_weapon() const; size_t find_equipped_item(EquipSlot slot) const;
size_t find_equipped_armor() const; bool has_equipped_item(EquipSlot slot) const;
size_t find_equipped_mag() 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); 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)) { if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
try { try {
auto& inventory = c->game_data.character()->inventory; 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; auto& data = inventory.items[mag_index].data;
data.data2[0] = max<int8_t>(static_cast<int8_t>(data.data2[0] - 5), 0); data.data2[0] = max<int8_t>(static_cast<int8_t>(data.data2[0] - 5), 0);
} catch (const out_of_range&) { } 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) { 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_EquipOrUnequipItem_6x25_6x26>(data, 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) { if (cmd.header.client_id != c->lobby_client_id) {
return; 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(); auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto p = c->game_data.character(); auto p = c->game_data.character();
size_t index = p->inventory.find_item(cmd.item_id); p->inventory.unequip_item(cmd.item_id);
if (cmd.header.subcommand == 0x25) { // Equip
p->inventory.items[index].flags |= 0x00000008;
} else { // Unequip
p->inventory.items[index].flags &= 0xFFFFFFF7;
}
} else if (l->base_version == GameVersion::BB) { } else if (l->base_version == GameVersion::BB) {
throw logic_error("item tracking not enabled in BB game"); 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(); auto p = c->game_data.character();
const auto& enemy = l->map->enemies.at(cmd.enemy_index); const auto& enemy = l->map->enemies.at(cmd.enemy_index);
const auto& inventory = p->inventory; 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()); 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, /* 6x22 */ on_forward_check_size_client,
/* 6x23 */ on_set_player_visibility, /* 6x23 */ on_set_player_visibility,
/* 6x24 */ on_forward_check_size_game, /* 6x24 */ on_forward_check_size_game,
/* 6x25 */ on_equip_unequip_item, /* 6x25 */ on_equip_item,
/* 6x26 */ on_equip_unequip_item, /* 6x26 */ on_unequip_item,
/* 6x27 */ on_use_item, /* 6x27 */ on_use_item,
/* 6x28 */ on_feed_mag, /* 6x28 */ on_feed_mag,
/* 6x29 */ on_destroy_inventory_item, /* 6x29 */ on_destroy_inventory_item,