From 612b5d28ba5321e1e3b7301cda08b58a48b5e6af Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 4 Jan 2024 10:39:18 -0800 Subject: [PATCH] fix tech disk stacking on 11/2000 --- src/ItemCreator.cc | 8 ++++---- src/ItemCreator.hh | 2 +- src/ItemData.cc | 35 ++++++++++++++++++++++++++--------- src/ItemData.hh | 3 +-- src/ItemNameIndex.cc | 39 ++++++++++++--------------------------- src/Items.cc | 32 ++++++++++++++++---------------- src/Main.cc | 5 +++-- src/PlayerSubordinates.cc | 6 +++--- src/RareItemSet.cc | 12 ++++++------ src/ReceiveSubcommands.cc | 12 ++++++------ src/SaveFileFormats.cc | 6 +++--- 11 files changed, 81 insertions(+), 79 deletions(-) diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index cb4ca576..129089d9 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -987,7 +987,7 @@ bool ItemCreator::shop_does_not_contain_duplicate_or_too_many_similar_weapons( return true; } -bool ItemCreator::shop_does_not_contain_duplicate_item_by_primary_identifier( +bool ItemCreator::shop_does_not_contain_duplicate_item_by_data1_0_1_2( const vector& shop, const ItemData& item) { for (const auto& shop_item : shop) { if ((shop_item.data1[0] == item.data1[0]) && @@ -1080,7 +1080,7 @@ void ItemCreator::generate_armor_shop_shields(vector& shop, size_t pla } } - if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) { + if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) { shop.emplace_back(std::move(item)); items_generated++; } @@ -1114,7 +1114,7 @@ void ItemCreator::generate_armor_shop_units(vector& shop, size_t playe item.data1[0] = 1; item.data1[1] = 3; item.data1[2] = pt.pop(); - if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) { + if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) { shop.emplace_back(std::move(item)); items_generated++; } @@ -1225,7 +1225,7 @@ void ItemCreator::generate_rare_tool_shop_recovery_items( item.data1[0] = 3; item.data1[1] = tool_item_defs[type].first; item.data1[2] = tool_item_defs[type].second; - if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) { + if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) { shop.emplace_back(std::move(item)); items_generated++; } diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index e5f0f917..33872202 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -134,7 +134,7 @@ private: const std::vector& shop, const ItemData& item); static bool shop_does_not_contain_duplicate_or_too_many_similar_weapons( const std::vector& shop, const ItemData& item); - static bool shop_does_not_contain_duplicate_item_by_primary_identifier( + static bool shop_does_not_contain_duplicate_item_by_data1_0_1_2( const std::vector& shop, const ItemData& item); void generate_armor_shop_armors( std::vector& shop, size_t player_level); diff --git a/src/ItemData.cc b/src/ItemData.cc index 8edf4d74..b16278f1 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -71,16 +71,17 @@ uint32_t ItemData::primary_identifier() const { // The game treats any item starting with 04 as Meseta, and ignores the rest // of data1 (the value is in data2) if (this->data1[0] == 0x04) { - return 0x040000; + return 0x04000000; } if (this->data1[0] == 0x03 && this->data1[1] == 0x02) { - return 0x030200; // Tech disk (data1[2] is level, so omit it) + // Tech disk (tech ID is data1[4], not [2]) + return 0x03020000 | (this->data1[4] << 8) | this->data1[2]; } else if (this->data1[0] == 0x02) { - return 0x020000 | (this->data1[1] << 8); // Mag + return 0x02000000 | (this->data1[1] << 16); // Mag } else if (this->is_s_rank_weapon()) { - return (this->data1[0] << 16) | (this->data1[1] << 8); + return (this->data1[0] << 24) | (this->data1[1] << 16); } else { - return (this->data1[0] << 16) | (this->data1[1] << 8) | this->data1[2]; + return (this->data1[0] << 24) | (this->data1[1] << 16) | (this->data1[2] << 8); } } @@ -164,10 +165,9 @@ void ItemData::enforce_min_stack_size(Version version) { } bool ItemData::is_common_consumable(uint32_t primary_identifier) { - if (primary_identifier == 0x030200) { - return false; - } - return (primary_identifier >= 0x030000) && (primary_identifier < 0x030A00); + return (primary_identifier >= 0x03000000) && + (primary_identifier < 0x030A0000) && + ((primary_identifier & 0xFFFF0000) != 0x03020000); } bool ItemData::is_common_consumable() const { @@ -667,6 +667,23 @@ ItemData ItemData::from_data(const string& data) { return ret; } +ItemData ItemData::from_primary_identifier(Version version, uint32_t primary_identifier) { + ItemData ret; + if (primary_identifier > 0x04000000) { + throw runtime_error("invalid item class"); + } + ret.data1[0] = (primary_identifier >> 24) & 0xFF; + ret.data1[1] = (primary_identifier >> 16) & 0xFF; + if ((primary_identifier & 0xFFFF0000) == 0x03020000) { + ret.data1[4] = (primary_identifier >> 8) & 0xFF; + ret.data1[2] = primary_identifier & 0xFF; + } else { + ret.data1[2] = (primary_identifier >> 8) & 0xFF; + } + ret.set_tool_item_amount(version, 1); + return ret; +} + 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], diff --git a/src/ItemData.hh b/src/ItemData.hh index ffe6c75e..535e03b9 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -6,8 +6,6 @@ #include "Text.hh" #include "Version.hh" -constexpr uint32_t MESETA_IDENTIFIER = 0x040000; - class ItemParameterTable; enum class EquipSlot { @@ -126,6 +124,7 @@ struct ItemData { // 0x14 bytes void clear(); static ItemData from_data(const std::string& data); + static ItemData from_primary_identifier(Version version, uint32_t primary_identifier); std::string hex() const; uint32_t primary_identifier() const; diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index 22555cc0..904a2d9f 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -4,22 +4,6 @@ using namespace std; -// class ItemNameIndex { -// public: -// ItemNameIndex(std::shared_ptr pmt, const std::vector& name_coll); -// std::string describe_item(const ItemData& item, bool include_color_escapes = false) const; -// ItemData parse_item_description(const std::string& description) const; -// private: -// ItemData parse_item_description_phase(const std::string& description, bool skip_special) const; -// std::shared_ptr item_parameter_table; -// struct ItemMetadata { -// uint32_t primary_identifier; -// std::string name; -// }; -// std::unordered_map> primary_identifier_indexes; -// std::map> name_indexes; -// }; - ItemNameIndex::ItemNameIndex( Version version, std::shared_ptr item_parameter_table, @@ -55,7 +39,9 @@ ItemNameIndex::ItemNameIndex( }; auto find_items_2d = [&](uint64_t data1) { for (size_t x = 0; x < 0x100; x++) { - if (find_items_1d(data1 | (static_cast(x) << 48), 2) == 0) { + size_t effective_data1 = data1 | (static_cast(x) << 48); + size_t data2_position = (effective_data1 == 0x0302000000000000) ? 4 : 2; + if (find_items_1d(effective_data1, data2_position) == 0) { break; } } @@ -182,17 +168,15 @@ std::string ItemNameIndex::describe_item( ret_tokens.emplace_back("Wrapped"); } - // Add the item name. Technique disks are special because the level is part of - // the primary identifier, so we manually generate the name instead of looking - // it up. + // Add the item name uint32_t primary_identifier = item.primary_identifier(); - if ((primary_identifier & 0xFFFFFF00) == 0x00030200) { + if ((primary_identifier & 0xFFFF0000) == 0x03020000) { string technique_name; try { technique_name = tech_id_to_name.at(item.data1[4]); technique_name[0] = toupper(technique_name[0]); } catch (const out_of_range&) { - technique_name = string_printf("!TECH:%02hhX", item.data1[4]); + technique_name = string_printf("!TD:%02hhX", item.data1[4]); } // Hide the level for Reverser and Ryuker, unless the level isn't 1 if ((item.data1[2] == 0) && ((item.data1[4] == 0x0E) || (item.data1[4] == 0x11))) { @@ -204,9 +188,8 @@ std::string ItemNameIndex::describe_item( try { auto meta = this->primary_identifier_index.at(primary_identifier); ret_tokens.emplace_back(meta->name); - } catch (const out_of_range&) { - ret_tokens.emplace_back(string_printf("!ID:%06" PRIX32, primary_identifier)); + ret_tokens.emplace_back(string_printf("!ID:%08" PRIX32, primary_identifier)); } } @@ -492,10 +475,12 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript desc = desc.substr(1); } + // Tech disks should have already been handled above, so we don't need to + // special-case 0302xxxx identifiers here. uint32_t primary_identifier = name_it->second->primary_identifier; - ret.data1[0] = (primary_identifier >> 16) & 0xFF; - ret.data1[1] = (primary_identifier >> 8) & 0xFF; - ret.data1[2] = primary_identifier & 0xFF; + ret.data1[0] = (primary_identifier >> 24) & 0xFF; + ret.data1[1] = (primary_identifier >> 16) & 0xFF; + ret.data1[2] = (primary_identifier >> 8) & 0xFF; if (ret.data1[0] == 0x00) { // Weapons: add special, grind and percentages (or name, if S-rank) diff --git a/src/Items.cc b/src/Items.cc index 572bcbc5..9b991aa9 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -17,12 +17,12 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrcharacter(); auto& item = player->inventory.items[item_index]; - uint32_t item_identifier = item.data.primary_identifier(); + uint32_t primary_identifier = item.data.primary_identifier(); if (item.data.is_common_consumable()) { // Monomate, etc. // Nothing to do (it should be deleted) - } else if (item_identifier == 0x030200) { // Technique disk + } else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk auto item_parameter_table = s->item_parameter_table(c->version()); uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]); if (item.data.data1[2] > max_level) { @@ -30,7 +30,7 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrset_technique_level(item.data.data1[4], item.data.data1[2]); - } else if ((item_identifier & 0xFFFF00) == 0x030A00) { // Grinder + } else if ((primary_identifier & 0xFFFF00) == 0x030A0000) { // Grinder if (item.data.data1[2] > 2) { throw runtime_error("incorrect grinder value"); } @@ -51,7 +51,7 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrcharacter(); using Type = PSOBBCharacterFile::MaterialType; @@ -104,7 +104,7 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrset_material_usage(type, p->get_material_usage(type) + 1); } - } else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot + } else if ((primary_identifier & 0xFFFF0000) == 0x030F0000) { // AddSlot 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"); @@ -116,57 +116,57 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrversion()); should_delete_item = false; - } else if (item_identifier == 0x003300) { + } else if (primary_identifier == 0x00330000) { // Unseal Sealed J-Sword => Tsumikiri J-Sword item.data.data1[1] = 0x32; should_delete_item = false; - } else if (item_identifier == 0x00AB00) { + } else if (primary_identifier == 0x00AB0000) { // Unseal Lame d'Argent => Excalibur item.data.data1[1] = 0xAC; should_delete_item = false; - } else if (item_identifier == 0x01034D) { + } else if (primary_identifier == 0x01034D00) { // Unseal Limiter => Adept item.data.data1[2] = 0x4E; should_delete_item = false; - } else if (item_identifier == 0x01034F) { + } else if (primary_identifier == 0x01034F00) { // Unseal Swordsman Lore => Proof of Sword-Saint item.data.data1[2] = 0x50; should_delete_item = false; - } else if (item_identifier == 0x030C00) { + } else if (primary_identifier == 0x030C0000) { // Cell of MAG 502 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) { + } else if (primary_identifier == 0x030C0100) { // Cell of MAG 213 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) { + } else if (primary_identifier == 0x030C0200) { // Parts of RoboChao auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; mag.data.data1[1] = 0x28; - } else if (item_identifier == 0x030C03) { + } else if (primary_identifier == 0x030C0300) { // Heart of Opa Opa auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; mag.data.data1[1] = 0x29; - } else if (item_identifier == 0x030C04) { + } else if (primary_identifier == 0x030C0400) { // Heart of Pian auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; mag.data.data1[1] = 0x2A; - } else if (item_identifier == 0x030C05) { + } else if (primary_identifier == 0x030C0500) { // Heart of Chao auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; mag.data.data1[1] = 0x2B; - } else if ((item_identifier & 0xFFFF00) == 0x031500) { + } else if ((primary_identifier & 0xFFFF0000) == 0x03150000) { // Christmas Present, etc. - use unwrap_table + probabilities therein auto item_parameter_table = s->item_parameter_table(c->version()); auto table = item_parameter_table->get_event_items(item.data.data1[2]); diff --git a/src/Main.cc b/src/Main.cc index c92af79e..ddacf2d6 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1472,11 +1472,12 @@ Action a_name_all_items( fputc('\n', stderr); for (uint32_t primary_identifier : all_primary_identifiers) { - fprintf(stderr, "%06" PRIX32 ":", primary_identifier); + fprintf(stderr, "%08" PRIX32 ":", primary_identifier); for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { const auto& index = s.item_name_indexes.at(v_s); if (index) { - ItemData item(static_cast(primary_identifier) << 40); + Version version = static_cast(v_s); + ItemData item = ItemData::from_primary_identifier(version, primary_identifier); string name = index->describe_item(item); fprintf(stderr, " %30s", name.c_str()); } diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 9f76eaed..32d9d783 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -451,9 +451,9 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge() const { } void PlayerBank::add_item(const ItemData& item, Version version) { - uint32_t pid = item.primary_identifier(); + uint32_t primary_identifier = item.primary_identifier(); - if (pid == MESETA_IDENTIFIER) { + if (primary_identifier == 0x04000000) { this->meseta += item.data2d; if (this->meseta > 999999) { this->meseta = 999999; @@ -465,7 +465,7 @@ void PlayerBank::add_item(const ItemData& item, Version version) { if (combine_max > 1) { size_t y; for (y = 0; y < this->num_items; y++) { - if (this->items[y].data.primary_identifier() == item.primary_identifier()) { + if (this->items[y].data.primary_identifier() == primary_identifier) { break; } } diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index fbfd28e7..cc916d55 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -446,13 +446,13 @@ std::string RareItemSet::serialize_json(shared_ptr name_ind } for (const auto& spec : this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, rt_index)) { - uint32_t primary_identifier = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2]; - if (primary_identifier == 0) { + uint32_t data1_0_1_2 = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2]; + if (data1_0_1_2 == 0) { continue; } auto frac = reduce_fraction(spec.probability, 0x100000000); - auto spec_json = JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), primary_identifier}); + auto spec_json = JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), data1_0_1_2}); if (name_index) { ItemData data; data.data1[0] = spec.item_code[0]; @@ -473,13 +473,13 @@ std::string RareItemSet::serialize_json(shared_ptr name_ind auto area_list = JSON::list(); for (const auto& spec : this->get_box_specs(GameMode::NORMAL, episode, difficulty, section_id, area)) { - uint32_t primary_identifier = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2]; - if (primary_identifier == 0) { + uint32_t data1_0_1_2 = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2]; + if (data1_0_1_2 == 0) { continue; } auto frac = reduce_fraction(spec.probability, 0x100000000); - area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), std::move(primary_identifier)})); + area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), data1_0_1_2})); if (name_index) { ItemData data; data.data1[0] = spec.item_code[0]; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 04a9f1bf..a4326868 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -3118,7 +3118,7 @@ static void on_photon_drop_exchange_for_item_bb(shared_ptr c, uint8_t, u try { auto p = c->character(); - size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000); + size_t found_index = p->inventory.find_item_by_primary_identifier(0x03100000); auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, c->version()); send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size(c->version())); @@ -3150,7 +3150,7 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(shared_ptr c, static const array costs({60, 60, 20, 20, 30, 30, 30, 50, 40, 50, 40, 40, 50, 40, 40, 40}); uint8_t cost = costs.at(cmd.special_type); - size_t payment_item_index = p->inventory.find_item_by_primary_identifier(0x031000); + size_t payment_item_index = p->inventory.find_item_by_primary_identifier(0x03100000); // Ensure weapon exists before removing PDs, so inventory state will be // consistent in case of error p->inventory.find_item(cmd.item_id); @@ -3186,7 +3186,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, auto p = c->character(); ssize_t slt_index = -1; try { - slt_index = p->inventory.find_item_by_primary_identifier(0x031003); // Secret Lottery Ticket + slt_index = p->inventory.find_item_by_primary_identifier(0x03100300); // Secret Lottery Ticket } catch (const out_of_range&) { } @@ -3234,7 +3234,7 @@ static void on_photon_crystal_exchange_bb(shared_ptr c, uint8_t, uint8_t if (l->is_game() && (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { check_size_t(data, size); auto p = c->character(); - size_t index = p->inventory.find_item_by_primary_identifier(0x031002); + size_t index = p->inventory.find_item_by_primary_identifier(0x03100200); auto item = p->remove_item(p->inventory.items[index].data.id, 1, c->version()); send_destroy_item_to_lobby(c, item.id, 1); } @@ -3284,7 +3284,7 @@ static void on_quest_F95F_result_bb(shared_ptr c, uint8_t, uint8_t, void throw runtime_error("invalid result index"); } - size_t index = p->inventory.find_item_by_primary_identifier(0x031004); // Photon Ticket + size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, c->version()); // TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an // actual game @@ -3417,7 +3417,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ size_t item_index = p->inventory.find_item(cmd.item_id); auto& item = p->inventory.items[item_index].data; - uint32_t payment_primary_identifier = cmd.payment_type ? 0x031001 : 0x031000; + uint32_t payment_primary_identifier = cmd.payment_type ? 0x03100100 : 0x03100000; size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier); auto& payment_item = p->inventory.items[payment_index].data; if (payment_item.stack_size(c->version()) < cmd.payment_count) { diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 13f66485..582a13cd 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -397,11 +397,11 @@ PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry:: // TODO: Eliminate duplication between this function and the parallel function // in PlayerBank void PSOBBCharacterFile::add_item(const ItemData& item, Version version) { - uint32_t pid = item.primary_identifier(); + uint32_t primary_identifier = item.primary_identifier(); // Annoyingly, meseta is in the disp data, not in the inventory struct. If the // item is meseta, we have to modify disp instead. - if (pid == MESETA_IDENTIFIER) { + if (primary_identifier == 0x04000000) { this->add_meseta(item.data2d); return; } @@ -413,7 +413,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item, Version version) { // player's inventory size_t y; for (y = 0; y < this->inventory.num_items; y++) { - if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) { + if (this->inventory.items[y].data.primary_identifier() == primary_identifier) { break; } }