diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 545b6bb9..d87859ef 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -4266,6 +4266,7 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { // Decompressed format is a list of these struct G_SyncEnemyState_6x6B_Entry_Decompressed { + // TODO: Verify this format on DC and PC. It appears correct for GC and BB. le_uint32_t unknown_a1; // Possibly some kind of flags // enemy_index is not the same as enemy_id, unfortunately - the enemy_id sent // in the 6x76 command when an enemy is killed does not match enemy_index @@ -4290,6 +4291,7 @@ struct G_SyncObjectState_6x6C_Entry_Decompressed { // Compressed format is the same as 6x6B. struct G_SyncItemState_6x6D_Decompressed { + // TODO: Verify this format on DC and PC. It appears correct for GC and BB. // Note: 16 vs. 15 is not a bug here - there really is an extra field in the // total drop count vs. the floor item count. Despite this, Pioneer 2 or Lab // (area 0) isn't included in total_items_dropped_per_area (so Forest 1 is [0] diff --git a/src/ItemData.cc b/src/ItemData.cc index c5d86475..4c7c3dcb 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -76,6 +76,17 @@ size_t ItemData::max_stack_size() const { return max_stack_size_for_item(this->data1[0], this->data1[1]); } +bool ItemData::is_common_consumable(uint32_t primary_identifier) { + if (primary_identifier == 0x030200) { + return false; + } + return (primary_identifier >= 0x030000) && (primary_identifier < 0x030A00); +} + +bool ItemData::is_common_consumable() const { + return this->is_common_consumable(this->primary_identifier()); +} + void ItemData::assign_mag_stats(const ItemMagStats& mag) { // this->data1[0] and [1] unchanged this->data1[2] = mag.level(); @@ -98,6 +109,114 @@ void ItemData::clear_mag_stats() { } } +uint16_t ItemData::compute_mag_level() const { + return (this->data1w[2] / 100) + + (this->data1w[3] / 100) + + (this->data1w[4] / 100) + + (this->data1w[5] / 100); +} + +uint16_t ItemData::compute_mag_strength_flags() const { + uint16_t pow = this->data1w[3] / 100; + uint16_t dex = this->data1w[4] / 100; + uint16_t mind = this->data1w[5] / 100; + + uint16_t ret = 0; + if ((dex < pow) && (mind < pow)) { + ret = 0x008; + } + if ((pow < dex) && (mind < dex)) { + ret |= 0x010; + } + if ((dex < mind) && (pow < mind)) { + ret |= 0x020; + } + + uint16_t highest = max(dex, max(pow, mind)); + if ((pow == highest) + (dex == highest) + (mind == highest) > 1) { + ret |= 0x100; + } + return ret; +} + +uint8_t ItemData::mag_photon_blast_for_slot(uint8_t slot) const { + uint8_t flags = this->data2[2]; + uint8_t pb_nums = this->data1[3]; + + if (slot == 0) { // Center + return (flags & 1) ? (pb_nums & 0x07) : 0xFF; + + } else if (slot == 1) { // Right + return (flags & 2) ? ((pb_nums & 0x38) >> 3) : 0xFF; + + } else if (slot == 2) { // Left + if (!(flags & 4)) { + return 0xFF; + } + + uint8_t used_pbs[6] = {0, 0, 0, 0, 0, 0}; + used_pbs[pb_nums & 0x07] = '\x01'; + used_pbs[(pb_nums & 0x38) >> 3] = '\x01'; + uint8_t left_pb_num = (pb_nums & 0xC0) >> 6; + for (size_t z = 0; z < 6; z++) { + if (!used_pbs[z]) { + if (!left_pb_num) { + return z; + } + left_pb_num--; + } + } + throw logic_error("failed to find unused photon blast number"); + + } else { + throw logic_error("invalid slot index"); + } +} + +bool ItemData::mag_has_photon_blast_in_any_slot(uint8_t pb_num) const { + if (pb_num < 6) { + for (size_t slot = 0; slot < 3; slot++) { + if (this->mag_photon_blast_for_slot(slot) == pb_num) { + return true; + } + } + } + return false; +} + +void ItemData::add_mag_photon_blast(uint8_t pb_num) { + if (pb_num >= 6) { + return; + } + if (this->mag_has_photon_blast_in_any_slot(pb_num)) { + return; + } + + uint8_t& flags = this->data2[2]; + uint8_t& pb_nums = this->data1[3]; + + if (!(flags & 1)) { // Center + pb_nums |= pb_num; + flags |= 1; + } else if (!(flags & 2)) { // Right + pb_nums |= (pb_num << 3); + flags |= 2; + } else if (!(flags & 4)) { + uint8_t orig_pb_num = pb_num; + if (this->mag_photon_blast_for_slot(0) < orig_pb_num) { + pb_num--; + } + if (this->mag_photon_blast_for_slot(1) < orig_pb_num) { + pb_num--; + } + if (pb_num >= 4) { + throw runtime_error("left photon blast number is too high"); + pb_nums |= (pb_num << 6); + } + flags |= 4; + } +} + void ItemData::set_sealed_item_kill_count(uint16_t v) { this->data1[10] = (v >> 8) | 0x80; this->data1[11] = v; @@ -1245,6 +1364,15 @@ const unordered_map name_info_for_primary_identifier({ {0x031903, "Team Points 10000"}, }); +string ItemData::hex() const { + return string_printf("%02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX (%08" PRIX32 ") %02hhX%02hhX%02hhX%02hhX", + this->data1[0], this->data1[1], this->data1[2], this->data1[3], + this->data1[4], this->data1[5], this->data1[6], this->data1[7], + this->data1[8], this->data1[9], this->data1[10], this->data1[11], + this->id.load(), + this->data2[0], this->data2[1], this->data2[2], this->data2[3]); +} + string ItemData::name(bool include_color_codes) const { if (this->data1[0] == 0x04) { return string_printf("%s%" PRIu32 " Meseta", @@ -1271,7 +1399,7 @@ string ItemData::name(bool include_color_codes) const { } } // Mags can be wrapped as well - if ((this->data1[0] == 0x02) && (this->data2[1] & 0x40)) { + if ((this->data1[0] == 0x02) && (this->data2[2] & 0x40)) { ret_tokens.emplace_back("Wrapped"); } @@ -1391,49 +1519,27 @@ string ItemData::name(bool include_color_codes) const { ret_tokens.emplace_back(string_printf("%d/%d/%d/%d", this->data1w[2] / 100, this->data1w[3] / 100, this->data1w[4] / 100, this->data1w[5] / 100)); - ret_tokens.emplace_back(string_printf("%hhu%%", this->data2[3])); - ret_tokens.emplace_back(string_printf("%hhuIQ", this->data2[2])); + ret_tokens.emplace_back(string_printf("%hhu%%", this->data2[0])); + ret_tokens.emplace_back(string_printf("%hhuIQ", this->data2[1])); - uint8_t flags = this->data2[1]; + uint8_t flags = this->data2[2]; if (flags & 7) { static const vector pb_shortnames = { "F", "E", "G", "P", "L", "M&Y", "MG", "GR"}; const char* pb_names[3] = {nullptr, nullptr, nullptr}; - uint8_t center_pb = (flags & 2) ? (this->data1[3] & 7) : 0xFF; - uint8_t right_pb = (flags & 1) ? ((this->data1[3] >> 3) & 7) : 0xFF; - uint8_t left_pb = (flags & 4) ? ((this->data1[3] >> 6) & 3) : 0xFF; + uint8_t left_pb = this->mag_photon_blast_for_slot(2); + uint8_t center_pb = this->mag_photon_blast_for_slot(0); + uint8_t right_pb = this->mag_photon_blast_for_slot(1); + if (left_pb != 0xFF) { + pb_names[0] = pb_shortnames[left_pb]; + } if (center_pb != 0xFF) { pb_names[1] = pb_shortnames[center_pb]; } if (right_pb != 0xFF) { pb_names[2] = pb_shortnames[right_pb]; } - if (left_pb != 0xFF) { - // There are only two bits for the left PB (as opposed to 3 for the - // center and right PBs). This works because PBs can't be duplicated; - // there are 6 valid PBs for each slot, but the center and right slots - // are used first, leaving 4 valid options for the left slot. To encode - // this in two bits, the game takes the list of all PBs, removes the - // center and right PBs from the list, and the left PB is then used as - // an index into this modified list to determine the actual left PB. - // Here, we don't construct a temporary list and instead just skip the - // center and right PB values with a loop. - uint8_t actual_left_pb = 0; - for (;;) { - if ((actual_left_pb == center_pb) || (actual_left_pb == right_pb)) { - actual_left_pb++; - continue; - } - if (left_pb > 0) { - actual_left_pb++; - left_pb--; - continue; - } - break; - } - pb_names[0] = pb_shortnames[actual_left_pb]; - } string token = "PB:"; for (size_t x = 0; x < 3; x++) { @@ -1469,9 +1575,9 @@ string ItemData::name(bool include_color_codes) const { /* 12 */ "costume color", }); try { - ret_tokens.emplace_back(string_printf("(%s)", mag_colors.at(this->data2[0]))); + ret_tokens.emplace_back(string_printf("(%s)", mag_colors.at(this->data2[3]))); } catch (const out_of_range&) { - ret_tokens.emplace_back(string_printf("(!CL:%02hhX)", this->data2[0])); + ret_tokens.emplace_back(string_printf("(!CL:%02hhX)", this->data2[3])); } } diff --git a/src/ItemData.hh b/src/ItemData.hh index d6f6598a..1b829a51 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -68,6 +68,7 @@ struct ItemData { // 0x14 bytes void clear(); + std::string hex() const; std::string name(bool include_color_codes) const; uint32_t primary_identifier() const; @@ -75,8 +76,16 @@ struct ItemData { // 0x14 bytes size_t stack_size() const; size_t max_stack_size() const; + static bool is_common_consumable(uint32_t primary_identifier); + bool is_common_consumable() const; + void assign_mag_stats(const ItemMagStats& mag); void clear_mag_stats(); + uint16_t compute_mag_level() const; + uint16_t compute_mag_strength_flags() const; + uint8_t mag_photon_blast_for_slot(uint8_t slot) const; + bool mag_has_photon_blast_in_any_slot(uint8_t pb_num) const; + void add_mag_photon_blast(uint8_t pb_num); void set_sealed_item_kill_count(uint16_t v); uint8_t get_tool_item_amount() const; diff --git a/src/ItemParameterTable.cc b/src/ItemParameterTable.cc index 4f1f9c21..07e6b822 100644 --- a/src/ItemParameterTable.cc +++ b/src/ItemParameterTable.cc @@ -3,7 +3,8 @@ using namespace std; ItemParameterTable::ItemParameterTable(shared_ptr data) - : data(data), r(*data) { + : data(data), + r(*data) { size_t offset_table_offset = this->r.pget_u32l(this->data->size() - 0x10); this->offsets = &r.pget(offset_table_offset); } @@ -111,6 +112,19 @@ float ItemParameterTable::get_sale_divisor(uint8_t data1_0, uint8_t data1_1) con return 0.0f; } +const ItemParameterTable::MagFeedResult& ItemParameterTable::get_mag_feed_result( + uint8_t table_index, uint8_t item_index) const { + if (table_index >= 8) { + throw runtime_error("invalid mag feed table index"); + } + if (item_index >= 11) { + throw runtime_error("invalid mag feed item index"); + } + const auto& table_offsets = this->r.pget(this->offsets->mag_feed_table); + const auto& results = this->r.pget(table_offsets.offsets[table_index]); + return results.results[item_index]; +} + uint8_t ItemParameterTable::get_item_stars(uint16_t slot) const { if ((slot >= 0xB1) && (slot < 0x437)) { return this->r.pget_u8(this->offsets->star_value_table + slot - 0xB1); @@ -172,8 +186,8 @@ uint8_t ItemParameterTable::get_item_base_stars(const ItemData& item) const { return this->get_item_stars(this->get_item_definition(item).id); } else if (item.data1[0] == 3) { const auto& def = (item.data1[1] == 2) - ? this->get_tool(2, item.data1[4]) - : this->get_tool(item.data1[1], item.data1[2]); + ? this->get_tool(2, item.data1[4]) + : this->get_tool(item.data1[1], item.data1[2]); return (def.item_flag & 0x80) ? 12 : 0; } else { return 0; @@ -221,6 +235,59 @@ bool ItemParameterTable::is_unsealable_item(const ItemData& item) const { return false; } +void ItemParameterTable::populate_item_combination_index() const { + if (!this->item_combination_index.empty()) { + return; + } + + const auto& co = this->r.pget(this->offsets->combination_table); + const auto* defs = &this->r.pget( + co.offset, co.count * sizeof(ItemCombination)); + for (size_t z = 0; z < co.count; z++) { + const auto& def = defs[z]; + uint32_t key = (def.used_item[0] << 16) | (def.used_item[1] << 8) | def.used_item[2]; + this->item_combination_index[key].emplace_back(def); + } +} + +const ItemParameterTable::ItemCombination& ItemParameterTable::get_item_combination( + const ItemData& used_item, const ItemData& equipped_item) const { + for (const auto& def : this->get_all_combinations_for_used_item(used_item)) { + if ((def.equipped_item[0] == 0xFF || def.equipped_item[0] == equipped_item.data1[0]) && + (def.equipped_item[1] == 0xFF || def.equipped_item[1] == equipped_item.data1[1]) && + (def.equipped_item[2] == 0xFF || def.equipped_item[2] == equipped_item.data1[2])) { + return def; + } + } + throw out_of_range("no item combination applies"); +} + +const std::vector& ItemParameterTable::get_all_combinations_for_used_item( + const ItemData& used_item) const { + try { + uint32_t key = (used_item.data1[0] << 16) | (used_item.data1[1] << 8) | used_item.data1[2]; + return this->get_all_item_combinations().at(key); + } catch (const out_of_range&) { + static const vector ret; + return ret; + } +} + +const std::map>& ItemParameterTable::get_all_item_combinations() const { + this->populate_item_combination_index(); + return this->item_combination_index; +} + +std::pair ItemParameterTable::get_event_items(uint8_t event_number) const { + const auto& co = this->r.pget(this->offsets->unwrap_table); + if (event_number >= co.count) { + throw runtime_error("invalid event number"); + } + const auto& event_co = this->r.pget(co.offset + sizeof(CountAndOffset) * event_number); + const auto* defs = &this->r.pget(event_co.offset, event_co.count * sizeof(ItemCombination)); + return make_pair(defs, event_co.count); +} + size_t ItemParameterTable::price_for_item(const ItemData& item) const { switch (item.data1[0]) { case 0: { @@ -275,9 +342,7 @@ size_t ItemParameterTable::price_for_item(const ItemData& item) const { const auto& def = this->get_armor_or_shield(item.data1[1], item.data1[2]); double power_factor = def.dfp + def.evp + def_bonus + evp_bonus; double power_factor_floor = static_cast((power_factor * power_factor) / sale_divisor); - return power_factor_floor + (70.0 * - static_cast(item.data1[5] + 1) * - static_cast(def.required_level + 1)); + return power_factor_floor + (70.0 * static_cast(item.data1[5] + 1) * static_cast(def.required_level + 1)); } case 2: @@ -296,3 +361,15 @@ size_t ItemParameterTable::price_for_item(const ItemData& item) const { } throw logic_error("this should be impossible"); } + +MagEvolutionTable::MagEvolutionTable(shared_ptr data) + : data(data), + r(*data) { + size_t offset_table_offset = this->r.pget_u32l(this->data->size() - 0x10); + this->offsets = &r.pget(offset_table_offset); +} + +uint8_t MagEvolutionTable::get_evolution_number(uint8_t data1_1) const { + const auto& table = this->r.pget(this->offsets->evolution_number); + return table.values[data1_1]; +} diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index 6b1c6b54..8714b1dc 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -2,15 +2,22 @@ #include +#include #include #include #include +#include #include "ItemData.hh" #include "Text.hh" class ItemParameterTable { public: + struct CountAndOffset { + le_uint32_t count; + le_uint32_t offset; + } __attribute__((packed)); + struct ItemBase { le_uint32_t id; le_uint16_t type; @@ -104,12 +111,12 @@ public: } __attribute__((packed)); struct MagFeedResult { - int8_t defense; - int8_t power; - int8_t dexterity; + int8_t def; + int8_t pow; + int8_t dex; int8_t mind; int8_t iq; - int8_t sync; + int8_t synchro; parray unused; } __attribute__((packed)); @@ -117,8 +124,8 @@ public: parray results; } __attribute__((packed)); - struct MagFeedResultsTable { - parray table; + struct MagFeedResultsListOffsets { + parray offsets; // Offsets of MagFeedResultsList structs } __attribute__((packed)); struct ItemStarValue { @@ -146,7 +153,7 @@ public: parray used_item; parray equipped_item; parray result_item; - uint8_t maglevel; + uint8_t mag_level; uint8_t grind; uint8_t level; uint8_t char_class; @@ -190,26 +197,21 @@ public: /* 1C / 0E194 */ le_uint32_t weapon_range_table; // -> ??? /* 20 / 0F5A8 */ le_uint32_t weapon_sale_divisor_table; // -> [float](0xED) /* 24 / 0F83C */ le_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors - /* 28 / 1502C */ le_uint32_t mag_feed_table; // -> [offset -> MagFeedResultsList](8) + /* 28 / 1502C */ le_uint32_t mag_feed_table; // -> MagFeedResultsTable /* 2C / 0FB0C */ le_uint32_t star_value_table; // -> [uint8_t] (indexed by .id from weapon, armor, etc.) /* 30 / 0FE3C */ le_uint32_t special_data_table; // -> [Special] /* 34 / 0FEE0 */ le_uint32_t weapon_effect_table; // -> [16-byte structs] /* 38 / 1275C */ le_uint32_t stat_boost_table; // -> [StatBoost] /* 3C / 11C80 */ le_uint32_t shield_effect_table; // -> [8-byte structs] /* 40 / 12894 */ le_uint32_t max_tech_level_table; // -> MaxTechniqueLevels - /* 44 / 14FF4 */ le_uint32_t combination_table; // -> [{count, offset -> [ItemCombination]}] + /* 44 / 14FF4 */ le_uint32_t combination_table; // -> {count, offset -> [ItemCombination]} /* 48 / 12754 */ le_uint32_t unknown_a1; /* 4C / 14278 */ le_uint32_t tech_boost_table; // -> [TechniqueBoost] (always 0x2C of them? from counts struct?) - /* 50 / 15014 */ le_uint32_t unwrap_table; // -> {count, offset -> [{count, offset -> [4-byte structs]}]} + /* 50 / 15014 */ le_uint32_t unwrap_table; // -> {count, offset -> [{count, offset -> [EventItem]}]} /* 54 / 1501C */ le_uint32_t unsealable_table; // -> {count, offset -> [UnsealableItem]} /* 58 / 15024 */ le_uint32_t ranged_special_table; // -> {count, offset -> [4-byte structs]} } __attribute__((packed)); - struct CountAndOffset { - le_uint32_t count; - le_uint32_t offset; - } __attribute__((packed)); - ItemParameterTable(std::shared_ptr data); ~ItemParameterTable() = default; @@ -230,9 +232,44 @@ public: uint8_t get_item_adjusted_stars(const ItemData& item) const; bool is_item_rare(const ItemData& item) const; bool is_unsealable_item(const ItemData& param_1) const; + const ItemCombination& get_item_combination(const ItemData& used_item, const ItemData& equipped_item) const; + const std::vector& get_all_combinations_for_used_item(const ItemData& used_item) const; + const std::map>& get_all_item_combinations() const; + std::pair get_event_items(uint8_t event_number) const; size_t price_for_item(const ItemData& item) const; +private: + std::shared_ptr data; + StringReader r; + const TableOffsets* offsets; + + // Key is used_item. We can't index on (used_item, equipped_item) because + // equipped_item may contain wildcards, and the matching order matters. + void populate_item_combination_index() const; + mutable std::map> item_combination_index; +}; + +class MagEvolutionTable { +public: + struct TableOffsets { + /* 00 / 0400 */ le_uint32_t unknown_a1; // -> [offset -> (0xC-byte struct)[0x53], offset -> (same as first offset)] + /* 04 / 0408 */ le_uint32_t unknown_a2; // -> (2-byte struct, or single word)[0x53] + /* 08 / 04AE */ le_uint32_t unknown_a3; // -> (0xA8 bytes; possibly (8-byte struct)[0x15]) + /* 0C / 0556 */ le_uint32_t unknown_a4; // -> (uint8_t)[0x53] + /* 10 / 05AC */ le_uint32_t unknown_a5; // -> (float)[0x48] + /* 14 / 06CC */ le_uint32_t evolution_number; // -> (uint8_t)[0x53] + } __attribute__((packed)); + + struct EvolutionNumberTable { + parray values; + } __attribute__((packed)); + + MagEvolutionTable(std::shared_ptr data); + ~MagEvolutionTable() = default; + + uint8_t get_evolution_number(uint8_t data1_1) const; + private: std::shared_ptr data; StringReader r; diff --git a/src/Items.cc b/src/Items.cc index e10a964e..cecab5e4 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -4,184 +4,43 @@ #include +#include "SendCommands.hh" + using namespace std; -/* These items all need some kind of special handling that hasn't been implemented yet. - -030B04 = TP Material (?) -030C00 = Cell Of MAG 502 -030C01 = Cell Of MAG 213 -030C02 = Parts Of RoboChao -030C03 = Heart Of Opa Opa -030C04 = Heart Of Pian -030C05 = Heart Of Chao - -030D00 = Sorcerer's Right Arm -030D01 = S-beat's Arms -030D02 = P-arm's Arms -030D03 = Delsaber's Right Arm -030D04 = C-bringer's Right Arm -030D05 = Delsaber's Left Arm -030D06 = S-red's Arms -030D07 = Dragon's Claw -030D08 = Hildebear's Head -030D09 = Hildeblue's Head -030D0A = Parts of Baranz -030D0B = Belra's Right Arms -030D0C = GIGUE'S ARMS -030D0D = S-BERILL'S ARMS -030D0E = G-ASSASIN'S ARMS -030D0F = BOOMA'S RIGHT ARMS -030D10 = GOBOOMA'S RIGHT ARMS -030D11 = GIGOBOOMA'S RIGHT ARMS -030D12 = GAL WIND -030D13 = RAPPY'S WING - -030E00 = BERILL PHOTON -030E01 = PARASITIC GENE FLOW -030E02 = MAGICSTONE IRITISTA -030E03 = BLUE BLACK STONE -030E04 = SYNCESTA -030E05 = MAGIC WATER -030E06 = PARASITIC CELL TYPE D -030E07 = MAGIC ROCK HEART KEY -030E08 = MAGIC ROCK MOOLA -030E09 = STAR AMPLIFIER -030E0A = BOOK OF HITOGATA -030E0B = HEART OF CHU CHU -030E0C = PART OF EGG BLASTER -030E0D = HEART OF ANGLE -030E0E = HEART OF DEVIL -030E0F = KIT OF HAMBERGER -030E10 = PANTHER'S SPIRIT -030E11 = KIT OF MARK3 -030E12 = KIT OF MASTER SYSTEM -030E13 = KIT OF GENESIS -030E14 = KIT OF SEGA SATURN -030E15 = KIT OF DREAMCAST -030E16 = AMP. RESTA -030E17 = AMP. ANTI -030E18 = AMP. SHIFTA -030E19 = AMP. DEBAND -030E1A = AMP. -030E1B = AMP. -030E1C = AMP. -030E1D = AMP. -030E1E = AMP. -030E1F = AMP. -030E20 = AMP. -030E21 = AMP. -030E22 = AMP. -030E23 = AMP. -030E24 = AMP. -030E25 = AMP. -030E26 = HEART OF KAPUKAPU -030E27 = PROTON BOOSTER -030F00 = ADD SLOT -031000 = PHOTON DROP -031001 = PHOTON SPHERE -031002 = PHOTON CRYSTAL -031100 = BOOK OF KATANA 1 -031101 = BOOK OF KATANA 2 -031102 = BOOK OF KATANA 3 -031200 = WEAPONS BRONZE BADGE -031201 = WEAPONS SILVER BADGE -031202 = WEAPONS GOLD BADGE -031203 = WEAPONS CRYSTAL BADGE -031204 = WEAPONS STEEL BADGE -031205 = WEAPONS ALUMINUM BADGE -031206 = WEAPONS LEATHER BADGE -031207 = WEAPONS BONE BADGE -031208 = LETTER OF APPRECATION -031209 = AUTOGRAPH ALBUM -03120A = VALENTINE'S CHOCOLATE -03120B = NEWYEAR'S CARD -03120C = CRISMAS CARD -03120D = BIRTHDAY CARD -03120E = PROOF OF SONIC TEAM -03120F = SPECIAL EVENT TICKET -031300 = PRESENT -031400 = CHOCOLATE -031401 = CANDY -031402 = CAKE -031403 = SILVER BADGE -031404 = GOLD BADGE -031405 = CRYSTAL BADGE -031406 = IRON BADGE -031407 = ALUMINUM BADGE -031408 = LEATHER BADGE -031409 = BONE BADGE -03140A = BONQUET -03140B = DECOCTION -031500 = CRISMAS PRESENT -031501 = EASTER EGG -031502 = JACK-O'S-LANTERN -031700 = HUNTERS REPORT -031701 = HUNTERS REPORT RANK A -031702 = HUNTERS REPORT RANK B -031703 = HUNTERS REPORT RANK C -031704 = HUNTERS REPORT RANK F -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031705 = HUNTERS REPORT -031802 = Dragon Scale -031803 = Heaven Striker Coat -031807 = Rappys Beak -031802 = Dragon Scale */ - -//////////////////////////////////////////////////////////////////////////////// - -void player_use_item(shared_ptr c, size_t item_index) { - auto player = c->game_data.player(); - - ssize_t equipped_weapon = -1; - // ssize_t equipped_armor = -1; - // ssize_t equipped_shield = -1; - // ssize_t equipped_mag = -1; - for (size_t y = 0; y < c->game_data.player()->inventory.num_items; y++) { - if (c->game_data.player()->inventory.items[y].flags & 0x00000008) { - if (c->game_data.player()->inventory.items[y].data.data1[0] == 0) { - equipped_weapon = y; - } - // else if ((c->game_data.player()->inventory.items[y].data.data1[0] == 1) && - // (c->game_data.player()->inventory.items[y].data.data1[1] == 1)) { - // equipped_armor = y; - // } else if ((c->game_data.player()->inventory.items[y].data.data1[0] == 1) && - // (c->game_data.player()->inventory.items[y].data.data1[1] == 2)) { - // equipped_shield = y; - // } else if (c->game_data.player()->inventory.items[y].data.data1[0] == 2) { - // equipped_mag = y; - // } - } - } - +void player_use_item(shared_ptr s, shared_ptr c, size_t item_index) { // On PC (and presumably DC), the client sends a 6x29 after this to delete the // used item. On GC and later versions, this does not happen, so we should // delete the item here. - bool should_delete_item = (c->version() != GameVersion::DC) && - (c->version() != GameVersion::PC); + bool should_delete_item = (c->version() != GameVersion::DC) && (c->version() != GameVersion::PC); - auto& item = c->game_data.player()->inventory.items[item_index]; - if (item.data.data1w[0] == 0x0203) { // technique disk - c->game_data.player()->disp.technique_levels.data()[item.data.data1[4]] = item.data.data1[2]; + auto player = c->game_data.player(); + auto& item = player->inventory.items[item_index]; + uint32_t item_identifier = item.data.primary_identifier(); - } else if (item.data.data1w[0] == 0x0A03) { // grinder - if (equipped_weapon < 0) { - throw invalid_argument("grinder used with no weapon equipped"); + if (item.data.is_common_consumable()) { // Monomate, etc. + // Nothing to do (it should be deleted) + + } else if (item_identifier == 0x030200) { // Technique disk + uint8_t max_level = s->item_parameter_table->get_max_tech_level(player->disp.char_class, item.data.data1[4]); + if (item.data.data1[2] > max_level) { + throw runtime_error("technique level too high"); } + player->disp.technique_levels.data()[item.data.data1[4]] = item.data.data1[2]; + + } else if ((item_identifier & 0xFFFF00) == 0x030A00) { // Grinder if (item.data.data1[2] > 2) { throw invalid_argument("incorrect grinder value"); } - c->game_data.player()->inventory.items[equipped_weapon].data.data1[3] += (item.data.data1[2] + 1); - // TODO: we should check for max grind here + auto& weapon = player->inventory.items[player->inventory.find_equipped_weapon()]; + auto weapon_def = s->item_parameter_table->get_weapon( + weapon.data.data1[1], weapon.data.data1[2]); + if (weapon.data.data1[3] >= weapon_def.max_grind) { + throw runtime_error("weapon already at maximum grind"); + } + weapon.data.data1[3] += (item.data.data1[2] + 1); - } else if (item.data.data1w[0] == 0x0B03) { // material + } else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material switch (item.data.data1[2]) { case 0: // Power Material c->game_data.player()->disp.stats.atp += 2; @@ -208,20 +67,412 @@ void player_use_item(shared_ptr c, size_t item_index) { throw invalid_argument("unknown material used"); } + } else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot + auto& armor = player->inventory.items[player->inventory.find_equipped_armor()]; + if (armor.data.data1[5] >= 4) { + throw runtime_error("armor already at maximum slot count"); + } + armor.data.data1[5]++; + + } else if ((item.data.data1[0] == 0x02) && (item.data.data2[2] & 0x40)) { + // Unwrap mag present + item.data.data2[2] &= 0xBF; + should_delete_item = false; + + } else if ((item.data.data1[0] != 0x02) && (item.data.data1[4] & 0x40)) { + // Unwrap non-mag present + item.data.data1[4] &= 0xBF; + should_delete_item = false; + + } else if (item_identifier == 0x003300) { + // Unseal Sealed J-Sword => Tsumikiri J-Sword + item.data.data1[1] = 0x32; + should_delete_item = false; + + } else if (item_identifier == 0x00AB00) { + // Unseal Lame d'Argent => Excalibur + item.data.data1[1] = 0xAC; + should_delete_item = false; + + } else if (item_identifier == 0x01034D) { + // Unseal Limiter => Adept + item.data.data1[2] = 0x4E; + should_delete_item = false; + + } else if (item_identifier == 0x01034F) { + // Unseal Swordsman Lore => Proof of Sword-Saint + item.data.data1[2] = 0x50; + should_delete_item = false; + + } else if (item_identifier == 0x030C00) { + // Cell of MAG 502 + auto& mag = player->inventory.items[player->inventory.find_equipped_mag()]; + mag.data.data1[1] = (player->disp.section_id & 1) ? 0x1D : 0x21; + + } else if (item_identifier == 0x030C01) { + // Cell of MAG 213 + auto& mag = player->inventory.items[player->inventory.find_equipped_mag()]; + mag.data.data1[1] = (player->disp.section_id & 1) ? 0x27 : 0x22; + + } else if (item_identifier == 0x030C02) { + // Parts of RoboChao + auto& mag = player->inventory.items[player->inventory.find_equipped_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()]; + mag.data.data1[1] = 0x29; + + } else if (item_identifier == 0x030C04) { + // Heart of Pian + auto& mag = player->inventory.items[player->inventory.find_equipped_mag()]; + mag.data.data1[1] = 0x2A; + + } else if (item_identifier == 0x030C05) { + // Heart of Chao + auto& mag = player->inventory.items[player->inventory.find_equipped_mag()]; + mag.data.data1[1] = 0x2B; + + } else if ((item_identifier & 0xFFFF00) == 0x031500) { + // Christmas Present, etc. - use unwrap_table + probabilities therein + auto table = s->item_parameter_table->get_event_items(item.data.data1[2]); + size_t sum = 0; + for (size_t z = 0; z < table.second; z++) { + sum += table.first[z].probability; + } + if (sum == 0) { + throw runtime_error("no unwrap results available for event"); + } + size_t det = random_object() % sum; + for (size_t z = 0; z < table.second; z++) { + const auto& entry = table.first[z]; + if (det > entry.probability) { + det -= entry.probability; + } else { + item.data.data2d = 0; + item.data.data1[0] = entry.item[0]; + item.data.data1[1] = entry.item[1]; + item.data.data1[2] = entry.item[2]; + item.data.data1.clear_after(3); + should_delete_item = false; + + auto l = s->find_lobby(c->lobby_id); + send_create_inventory_item(l, c, item.data); + break; + } + } + } else { - // default item action is to unwrap the item if it's a present - if ((item.data.data1[0] == 2) && (item.data.data2[2] & 0x40)) { - item.data.data2[2] &= 0xBF; - should_delete_item = false; - } else if ((item.data.data1[0] != 2) && (item.data.data1[4] & 0x40)) { - item.data.data1[4] &= 0xBF; - should_delete_item = false; + // Use item combinations table from ItemPMT + bool combo_applied = false; + for (size_t z = 0; z < player->inventory.num_items; z++) { + auto& inv_item = player->inventory.items[z]; + if (!(inv_item.flags & 0x00000008)) { + continue; + } + try { + const auto& combo = s->item_parameter_table->get_item_combination( + item.data, inv_item.data); + if (combo.char_class != 0xFF && combo.char_class != player->disp.char_class) { + throw runtime_error("item combination requires specific char_class"); + } + if (combo.mag_level != 0xFF) { + if (inv_item.data.data1[0] != 2) { + throw runtime_error("item combination applies with mag level requirement, but equipped item is not a mag"); + } + if (inv_item.data.compute_mag_level() < combo.mag_level) { + throw runtime_error("item combination applies with mag level requirement, but equipped mag level is too low"); + } + } + if (combo.grind != 0xFF) { + if (inv_item.data.data1[0] != 0) { + throw runtime_error("item combination applies with grind requirement, but equipped item is not a weapon"); + } + if (inv_item.data.data1[3] < combo.grind) { + throw runtime_error("item combination applies with grind requirement, but equipped weapon grind is too low"); + } + } + if (combo.level != 0xFF && player->disp.level + 1 < combo.level) { + throw runtime_error("item combination applies with level requirement, but player level is too low"); + } + // If we get here, then the combo applies + if (combo_applied) { + throw runtime_error("multiple combinations apply"); + } + combo_applied = true; + + inv_item.data.data1[0] = combo.result_item[0]; + inv_item.data.data1[1] = combo.result_item[1]; + inv_item.data.data1[2] = combo.result_item[2]; + inv_item.data.data1[3] = 0; // Grind + inv_item.data.data1[4] = 0; // Flags + special + } catch (const out_of_range&) { + } + } + + if (!combo_applied) { + throw runtime_error("no combinations apply"); } } if (should_delete_item) { // Allow overdrafting meseta if the client is not BB, since the server isn't // informed when meseta is added or removed from the bank. - c->game_data.player()->remove_item(item.data.id, 1, c->version() != GameVersion::BB); + player->remove_item(item.data.id, 1, c->version() != GameVersion::BB); } } + +void player_feed_mag(std::shared_ptr s, std::shared_ptr c, size_t mag_item_index, size_t fed_item_index) { + static const unordered_map result_index_for_fed_item({ + {0x030000, 0}, // Monomate + {0x030001, 1}, // Dimate + {0x030002, 2}, // Trimate + {0x030100, 3}, // Monofluid + {0x030101, 4}, // Difluid + {0x030102, 5}, // Trifluid + {0x030600, 6}, // Antidote + {0x030601, 7}, // Antiparalysis + {0x030300, 8}, // Sol Atomizer + {0x030400, 9}, // Moon Atomizer + {0x030500, 10}, // Star Atomizer + }); + + auto player = c->game_data.player(); + auto& fed_item = player->inventory.items[fed_item_index]; + auto& mag_item = player->inventory.items[mag_item_index]; + + size_t result_index = result_index_for_fed_item.at(fed_item.data.primary_identifier()); + const auto& mag_def = s->item_parameter_table->get_mag(mag_item.data.data1[1]); + const auto& feed_result = s->item_parameter_table->get_mag_feed_result(mag_def.feed_table, result_index); + + fprintf(stderr, "[feed-mag] table = %hu, index = %zu\n", mag_def.feed_table.load(), result_index); + print_data(stderr, &feed_result, sizeof(feed_result)); + + auto update_stat = +[](ItemData& data, size_t which, int8_t delta) -> void { + uint16_t existing_stat = data.data1w[which] % 100; + if ((delta > 0) || ((delta < 0) && (-delta < existing_stat))) { + uint16_t level = data.compute_mag_level(); + if (level > 200) { + throw runtime_error("mag level is too high"); + } + if ((level == 200) && ((99 - existing_stat) < delta)) { + delta = 99 - existing_stat; + } + data.data1w[which] += delta; + } + }; + + auto print_mag_item = [&](const char* step) -> void { + auto hex = mag_item.data.hex(); + fprintf(stderr, "[feed-mag] step: %s\n[feed-mag] %s\n", step, hex.c_str()); + }; + + print_mag_item("pre"); + update_stat(mag_item.data, 2, feed_result.def); + print_mag_item("update-def"); + update_stat(mag_item.data, 3, feed_result.pow); + print_mag_item("update-pow"); + update_stat(mag_item.data, 4, feed_result.dex); + print_mag_item("update-dex"); + update_stat(mag_item.data, 5, feed_result.mind); + print_mag_item("update-mind"); + mag_item.data.data2[0] = clamp(static_cast(mag_item.data.data2[0]) + feed_result.synchro, 0, 120); + print_mag_item("update-synchro"); + mag_item.data.data2[1] = clamp(static_cast(mag_item.data.data2[1]) + feed_result.iq, 0, 200); + print_mag_item("update-iq"); + + uint8_t mag_level = mag_item.data.compute_mag_level(); + mag_item.data.data1[2] = mag_level; + print_mag_item("compute-level"); + uint8_t evolution_number = s->mag_evolution_table->get_evolution_number(mag_item.data.data1[1]); + uint8_t mag_number = mag_item.data.data1[1]; + fprintf(stderr, "[feed-mag] evo_num = %02hhX, mag_num = %02hhX\n", evolution_number, mag_number); + + // Note: Sega really did just hardcode all these rules into the client. There + // is no data file describing these evolutions, unfortunately. + + if (mag_level < 10) { + // Nothing to do + + } else if (mag_level < 35) { // Level 10 evolution + if (evolution_number < 1) { + switch (player->disp.char_class) { + case 0: // HUmar + case 1: // HUnewearl + case 2: // HUcast + case 9: // HUcaseal + mag_item.data.data1[1] = 0x01; // Varuna + break; + case 3: // RAmar + case 11: // RAmarl + case 4: // RAcast + case 5: // RAcaseal + mag_item.data.data1[1] = 0x0D; // Kalki + break; + case 10: // FOmar + case 6: // FOmarl + case 7: // FOnewm + case 8: // FOnewearl + mag_item.data.data1[1] = 0x19; // Vritra + break; + default: + throw runtime_error("invalid character class"); + } + } + + } else if (mag_level < 50) { // Level 35 evolution + if (evolution_number < 2) { + uint16_t flags = mag_item.data.compute_mag_strength_flags(); + if (mag_number == 0x0D) { + if ((flags & 0x110) == 0) { + mag_item.data.data1[1] = 0x02; + } else if (flags & 8) { + mag_item.data.data1[1] = 0x03; + } else if (flags & 0x20) { + mag_item.data.data1[1] = 0x0B; + } + } else if (mag_number == 1) { + if (flags & 0x108) { + mag_item.data.data1[1] = 0x0E; + } else if (flags & 0x10) { + mag_item.data.data1[1] = 0x0F; + } else if (flags & 0x20) { + mag_item.data.data1[1] = 0x04; + } + } else if (mag_number == 0x19) { + if (flags & 0x120) { + mag_item.data.data1[1] = 0x1A; + } else if (flags & 8) { + mag_item.data.data1[1] = 0x1B; + } else if (flags & 0x10) { + mag_item.data.data1[1] = 0x14; + } + } + } + + } else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions + if (evolution_number < 4) { + + if (mag_level >= 100) { + uint8_t section_id_group = player->disp.section_id % 3; + uint16_t def = mag_item.data.data1w[2] / 100; + uint16_t pow = mag_item.data.data1w[3] / 100; + uint16_t dex = mag_item.data.data1w[4] / 100; + uint16_t mind = mag_item.data.data1w[5] / 100; + bool is_male = char_class_is_male(player->disp.char_class); + size_t table_index = (is_male ? 0 : 1) + section_id_group * 2; + + bool is_hunter = char_class_is_hunter(player->disp.char_class); + bool is_ranger = char_class_is_ranger(player->disp.char_class); + bool is_force = char_class_is_force(player->disp.char_class); + if (is_force) { + table_index += 12; + } else if (is_ranger) { + table_index += 6; + } else if (!is_hunter) { + throw logic_error("char class is not any of the top-level classes"); + } + + // Note: The original code checks the class (hunter/ranger/force) again + // here, and goes into 3 branches that each do these same checks. + // However, the result of all 3 branches is exactly the same! + if (((section_id_group == 0) && (pow + mind == def + dex)) || + ((section_id_group == 1) && (pow + dex == mind + def)) || + ((section_id_group == 2) && (pow + def == mind + dex))) { + // clang-format off + static const uint8_t result_table[] = { + // M0 F0 M1 F1 M2 F2 + 0x39, 0x3B, 0x3A, 0x3B, 0x3A, 0x3B, // Hunter + 0x3D, 0x3C, 0x3D, 0x3C, 0x3D, 0x3E, // Ranger + 0x41, 0x3F, 0x41, 0x40, 0x41, 0x40, // Force + }; + // clang-format on + mag_item.data.data1[1] = result_table[table_index]; + } + } + + // If a special evolution did not occur, do a normal level 50 evolution + if (mag_number == mag_item.data.data1[1]) { + uint16_t flags = mag_item.data.compute_mag_strength_flags(); + uint16_t def = mag_item.data.data1w[2] / 100; + uint16_t pow = mag_item.data.data1w[3] / 100; + uint16_t dex = mag_item.data.data1w[4] / 100; + uint16_t mind = mag_item.data.data1w[5] / 100; + + bool is_hunter = char_class_is_hunter(player->disp.char_class); + bool is_ranger = char_class_is_ranger(player->disp.char_class); + bool is_force = char_class_is_force(player->disp.char_class); + if (is_hunter + is_ranger + is_force != 1) { + throw logic_error("char class is not exactly one of the top-level classes"); + } + + if (is_hunter) { + if (flags & 0x108) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((dex < mind) ? 0x08 : 0x06) + : ((dex < mind) ? 0x0C : 0x05); + } else if (flags & 0x010) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((mind < pow) ? 0x12 : 0x10) + : ((mind < pow) ? 0x17 : 0x13); + } else if (flags & 0x020) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((pow < dex) ? 0x16 : 0x24) + : ((pow < dex) ? 0x07 : 0x1E); + } + } else if (is_ranger) { + if (flags & 0x110) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((mind < pow) ? 0x0A : 0x05) + : ((mind < pow) ? 0x0C : 0x06); + } else if (flags & 0x008) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((dex < mind) ? 0x0A : 0x26) + : ((dex < mind) ? 0x0C : 0x06); + } else if (flags & 0x020) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((pow < dex) ? 0x18 : 0x1E) + : ((pow < dex) ? 0x08 : 0x05); + } + } else if (is_force) { + if (flags & 0x120) { + if (def < 45) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((pow < dex) ? 0x17 : 0x09) + : ((pow < dex) ? 0x1E : 0x1C); + } else { + mag_item.data.data1[1] = 0x24; + } + } else if (flags & 0x008) { + if (def < 45) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((dex < mind) ? 0x1C : 0x20) + : ((dex < mind) ? 0x1F : 0x25); + } else { + mag_item.data.data1[1] = 0x23; + } + } else if (flags & 0x010) { + if (def < 45) { + mag_item.data.data1[1] = (player->disp.section_id & 1) + ? ((mind < pow) ? 0x12 : 0x0C) + : ((mind < pow) ? 0x15 : 0x11); + } else { + mag_item.data.data1[1] = 0x24; + } + } + } + } + } + } + + print_mag_item("evolution-check"); + + // If the mag has evolved, add its new photon blast + if (mag_number != mag_item.data.data1[1]) { + const auto& new_mag_def = s->item_parameter_table->get_mag(mag_item.data.data1[1]); + mag_item.data.add_mag_photon_blast(new_mag_def.photon_blast); + } + + print_mag_item("add-pb"); +} diff --git a/src/Items.hh b/src/Items.hh index d053ab87..5fd0e8eb 100644 --- a/src/Items.hh +++ b/src/Items.hh @@ -6,6 +6,8 @@ #include #include "Client.hh" +#include "ServerState.hh" #include "StaticGameData.hh" -void player_use_item(std::shared_ptr c, size_t item_index); +void player_use_item(std::shared_ptr s, std::shared_ptr c, size_t item_index); +void player_feed_mag(std::shared_ptr s, std::shared_ptr c, size_t mag_item_index, size_t fed_item_index); diff --git a/src/Player.cc b/src/Player.cc index a6050d6a..45d0658b 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -832,7 +832,7 @@ PlayerBankItem PlayerBank::remove_item(uint32_t item_id, uint32_t amount) { return ret; } -size_t PlayerInventory::find_item(uint32_t item_id) { +size_t PlayerInventory::find_item(uint32_t item_id) const { for (size_t x = 0; x < this->num_items; x++) { if (this->items[x].data.id == item_id) { return x; @@ -841,6 +841,69 @@ size_t PlayerInventory::find_item(uint32_t item_id) { throw out_of_range("item not present"); } +size_t PlayerInventory::find_equipped_weapon() 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] != 0) { + continue; + } + if (ret < 0) { + ret = y; + } else { + throw runtime_error("multiple weapons are equipped"); + } + } + if (ret < 0) { + throw runtime_error("no weapon is equipped"); + } + 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"); + } + } + if (ret < 0) { + throw runtime_error("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"); + } + } + if (ret < 0) { + throw runtime_error("no mag is equipped"); + } + return ret; +} + size_t PlayerBank::find_item(uint32_t item_id) { for (size_t x = 0; x < this->num_items; x++) { if (this->items[x].data.id == item_id) { @@ -856,7 +919,7 @@ void SavedPlayerDataBB::print_inventory(FILE* stream) const { for (size_t x = 0; x < this->inventory.num_items; x++) { const auto& item = this->inventory.items[x]; auto name = item.data.name(false); - fprintf(stream, "[PlayerInventory] %zu (%08" PRIX32 "): %06" PRIX32 " (%s)\n", - x, item.data.id.load(), item.data.primary_identifier(), name.c_str()); + auto hex = item.data.hex(); + fprintf(stream, "[PlayerInventory] %zu: %s (%s)\n", x, hex.c_str(), name.c_str()); } } diff --git a/src/Player.hh b/src/Player.hh index 3a619515..bf186391 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -46,7 +46,11 @@ struct PlayerInventory { // 0x34C bytes PlayerInventory(); - size_t find_item(uint32_t item_id); + size_t find_item(uint32_t item_id) const; + + size_t find_equipped_weapon() const; + size_t find_equipped_armor() const; + size_t find_equipped_mag() const; } __attribute__((packed)); struct PlayerBank { // 0x12C8 bytes diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index a812cc18..c5cac538 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -788,8 +788,12 @@ static void on_equip_unequip_item(shared_ptr, forward_subcommand(l, c, command, flag, data); } -static void on_use_item(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, +static void on_use_item( + shared_ptr s, + shared_ptr l, + shared_ptr c, + uint8_t command, + uint8_t flag, const string& data) { const auto& cmd = check_size_sc(data); @@ -807,7 +811,7 @@ static void on_use_item(shared_ptr, name = item.name(false); colored_name = item.name(true); } - player_use_item(c, index); + player_use_item(s, c, index); l->log.info("Player used item %hu:%08" PRIX32 " (%s)", cmd.header.client_id.load(), cmd.item_id.load(), name.c_str()); @@ -821,6 +825,57 @@ static void on_use_item(shared_ptr, forward_subcommand(l, c, command, flag, data); } +static void on_feed_mag( + shared_ptr s, + shared_ptr l, + shared_ptr c, + uint8_t command, + uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc(data); + + if (cmd.header.client_id != c->lobby_client_id) { + return; + } + + if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { + size_t mag_index = c->game_data.player()->inventory.find_item(cmd.mag_item_id); + size_t fed_index = c->game_data.player()->inventory.find_item(cmd.fed_item_id); + string mag_name, mag_colored_name, fed_name, fed_colored_name; + { + // Note: We do this weird scoping thing because player_use_item will + // likely delete the item, which will break the reference here. + const auto& fed_item = c->game_data.player()->inventory.items[fed_index].data; + fed_name = fed_item.name(false); + fed_colored_name = fed_item.name(true); + const auto& mag_item = c->game_data.player()->inventory.items[mag_index].data; + mag_name = mag_item.name(false); + mag_colored_name = mag_item.name(true); + } + player_feed_mag(s, c, mag_index, fed_index); + + // On BB, the player only sends a 6x28; on other versions, the player sends + // a 6x29 immediately after to destroy the fed item. So on BB, we should + // remove the fed item here, but on other versions, we allow the following + // 6x29 command to do that. + if (l->version == GameVersion::BB) { + c->game_data.player()->remove_item(cmd.fed_item_id, 1, false); + } + + l->log.info("Player fed item %hu:%08" PRIX32 " (%s) to mag %hu:%08" PRIX32 " (%s)", + cmd.header.client_id.load(), cmd.fed_item_id.load(), fed_name.c_str(), + cmd.header.client_id.load(), cmd.mag_item_id.load(), mag_name.c_str()); + if (c->options.debug) { + send_text_message_printf(c, "$C5Items: feed %08" PRIX32 "\n%s\n...to %08" PRIX32 "\n%s", + cmd.fed_item_id.load(), fed_colored_name.c_str(), + cmd.mag_item_id.load(), mag_colored_name.c_str()); + } + c->game_data.player()->print_inventory(stderr); + } + + forward_subcommand(l, c, command, flag, data); +} + static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { @@ -1413,7 +1468,7 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 25 */ on_equip_unequip_item, // Equip item /* 26 */ on_equip_unequip_item, // Unequip item /* 27 */ on_use_item, - /* 28 */ on_forward_check_size_game, // Feed MAG + /* 28 */ on_feed_mag, // Feed MAG /* 29 */ on_destroy_inventory_item, // Delete item (via bank deposit / sale / feeding MAG) /* 2A */ on_player_drop_item, /* 2B */ on_create_inventory_item, // Create inventory item (e.g. from tekker or bank withdrawal) diff --git a/src/ServerState.cc b/src/ServerState.cc index 03369601..20fa644f 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -862,9 +862,14 @@ void ServerState::load_item_tables() { } config_log.info("Loading item definition table"); - shared_ptr data(new string(prs_decompress(load_file( + shared_ptr pmt_data(new string(prs_decompress(load_file( "system/blueburst/ItemPMT.prs")))); - this->item_parameter_table.reset(new ItemParameterTable(data)); + this->item_parameter_table.reset(new ItemParameterTable(pmt_data)); + + config_log.info("Loading mag evolution table"); + shared_ptr mag_data(new string(prs_decompress(load_file( + "system/blueburst/ItemMagEdit.prs")))); + this->mag_evolution_table.reset(new MagEvolutionTable(mag_data)); } void ServerState::load_ep3_data() { diff --git a/src/ServerState.hh b/src/ServerState.hh index 0b79fea7..2f075802 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -16,7 +16,6 @@ #include "FunctionCompiler.hh" #include "GSLArchive.hh" #include "ItemParameterTable.hh" -#include "Items.hh" #include "LevelTable.hh" #include "License.hh" #include "Lobby.hh" @@ -83,6 +82,7 @@ struct ServerState { std::shared_ptr tool_random_set; std::array, 4> weapon_random_sets; std::shared_ptr item_parameter_table; + std::shared_ptr mag_evolution_table; std::shared_ptr ep3_tournament_index; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index f9e3b3e0..a5019628 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -377,6 +377,59 @@ const char* abbreviation_for_char_class(uint8_t cls) { } } +enum ClassFlag { + MALE = 0x01, + HUMAN = 0x02, + NEWMAN = 0x04, + ANDROID = 0x08, + HUNTER = 0x10, + RANGER = 0x20, + FORCE = 0x40, +}; + +static array class_flags = { + ClassFlag::HUNTER | ClassFlag::HUMAN | ClassFlag::MALE, // HUmar + ClassFlag::HUNTER | ClassFlag::NEWMAN, // HUnewearl + ClassFlag::HUNTER | ClassFlag::ANDROID | ClassFlag::MALE, // HUcast + ClassFlag::RANGER | ClassFlag::HUMAN | ClassFlag::MALE, // RAmar + ClassFlag::RANGER | ClassFlag::ANDROID | ClassFlag::MALE, // RAcast + ClassFlag::RANGER | ClassFlag::ANDROID, // RAcaseal + ClassFlag::FORCE | ClassFlag::HUMAN, // FOmarl + ClassFlag::FORCE | ClassFlag::NEWMAN | ClassFlag::MALE, // FOnewm + ClassFlag::FORCE | ClassFlag::NEWMAN, // FOnewearl + ClassFlag::HUNTER | ClassFlag::ANDROID, // HUcaseal + ClassFlag::FORCE | ClassFlag::HUMAN | ClassFlag::MALE, // FOmar + ClassFlag::RANGER | ClassFlag::HUMAN, // RAmarl +}; + +bool char_class_is_male(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::MALE; +} + +bool char_class_is_human(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::HUMAN; +} + +bool char_class_is_newman(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::NEWMAN; +} + +bool char_class_is_android(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::ANDROID; +} + +bool char_class_is_hunter(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::HUNTER; +} + +bool char_class_is_ranger(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::RANGER; +} + +bool char_class_is_force(uint8_t cls) { + return class_flags.at(cls) & ClassFlag::FORCE; +} + const char* name_for_difficulty(uint8_t difficulty) { static const array names = { "Normal", diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index 35e220dd..fdee1716 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -62,6 +62,13 @@ uint8_t npc_for_name(const std::u16string& name); const char* name_for_char_class(uint8_t cls); const char* abbreviation_for_char_class(uint8_t cls); +bool char_class_is_male(uint8_t cls); +bool char_class_is_human(uint8_t cls); +bool char_class_is_newman(uint8_t cls); +bool char_class_is_android(uint8_t cls); +bool char_class_is_hunter(uint8_t cls); +bool char_class_is_ranger(uint8_t cls); +bool char_class_is_force(uint8_t cls); const char* name_for_difficulty(uint8_t difficulty); char abbreviation_for_difficulty(uint8_t difficulty); diff --git a/src/Text.hh b/src/Text.hh index 0718c59f..8f718172 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -271,7 +271,7 @@ struct parray { if (offset + SubCount > Count) { throw std::out_of_range("sub-array out of range"); } - return *reinterpret_cast*>(&this->items[offset]); + return *reinterpret_cast*>(&this->items[offset]); } void assign_range(const ItemT* new_items, size_t count = Count, size_t start_offset = 0) {