From 6aef245eabcfbb5b28ba548b7f7f465e4a126806 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 15 Nov 2023 17:18:46 -0800 Subject: [PATCH] fix item ID inconsistencies between server and client due to shops and tekker in BB --- src/CommandFormats.hh | 2 +- src/Items.cc | 2 +- src/Lobby.cc | 10 +-- src/Map.cc | 2 - src/ReceiveSubcommands.cc | 176 +++++++++++++++++++------------------- src/SendCommands.cc | 9 +- src/SendCommands.hh | 2 +- 7 files changed, 102 insertions(+), 101 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index b83ed391..cab187c7 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -5312,7 +5312,7 @@ struct G_ShopContents_BB_6xB6 { struct G_BuyShopItem_BB_6xB7 { G_UnusedHeader header; - le_uint32_t inventory_item_id = 0; + le_uint32_t shop_item_id = 0; uint8_t shop_type = 0; uint8_t item_index = 0; uint8_t amount = 0; diff --git a/src/Items.cc b/src/Items.cc index a938c279..156b5dfa 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -194,7 +194,7 @@ void player_use_item(shared_ptr c, size_t item_index) { auto l = c->lobby.lock(); if (l) { - send_create_inventory_item(c, item.data); + send_create_inventory_item(c, item.data, false); } break; } diff --git a/src/Lobby.cc b/src/Lobby.cc index ef695ba2..6ae01893 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -196,15 +196,7 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { // If the lobby is a game and item tracking is enabled, assign the inventory's // item IDs if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { - auto s = this->require_server_state(); - auto p = c->game_data.character(); - auto& inv = p->inventory; - size_t count = min(inv.num_items, 30); - for (size_t x = 0; x < count; x++) { - inv.items[x].data.id = this->generate_item_id(c->lobby_client_id); - } - this->log.info("Assigned item IDs for joining player %zd", index); - p->print_inventory(stderr, c->version(), s->item_name_index); + this->assign_inventory_item_ids(c); } // If the lobby is recording a battle record, add the player join event diff --git a/src/Map.cc b/src/Map.cc index 64ae537f..7f8161da 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -472,7 +472,6 @@ struct DATParserRandomState { size_t rand_int_biased(size_t min_v, size_t max_v) { float max_f = static_cast(max_v + 1); uint32_t crypt_v = this->random.next(); - fprintf(stderr, "(global) => %08" PRIX32 "\n", crypt_v); float det_f = static_cast(crypt_v); return max(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v); } @@ -517,7 +516,6 @@ struct DATParserRandomState { for (size_t z = 0; z < 4; z++) { for (size_t x = 0; x < sec.count; x++) { uint32_t crypt_v = this->location_table_random.next(); - fprintf(stderr, "(local?) => %08" PRIX32 "\n", crypt_v); size_t choice = floorf((static_cast(sec.count) * static_cast(crypt_v)) / UINT32_MAX_AS_FLOAT); uint32_t t = this->location_index_table[x]; this->location_index_table[x] = this->location_index_table[choice]; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 1c12a362..e7d2a9ee 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -228,78 +228,82 @@ static void on_sync_joining_player_item_state(shared_ptr c, uint8_t comm return; } - // For non-V3 versions, just forward the data verbatim. For V3, we need to - // byteswap mags' data2 fields if exactly one of the sender and recipient is - // PSO GC - bool sender_is_gc = (c->version() == GameVersion::GC); - if (!sender_is_gc && (c->version() != GameVersion::XB)) { + const auto& cmd = check_size_t(data, size, 0xFFFF); + if (cmd.compressed_size > size - sizeof(cmd)) { + throw runtime_error("compressed end offset is beyond end of command"); + } + + string decompressed = bc0_decompress(reinterpret_cast(data) + sizeof(cmd), cmd.compressed_size); + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + c->log.info("Decompressed item sync data (%" PRIX32 " -> %zX bytes; expected %" PRIX32 "):", + cmd.compressed_size.load(), decompressed.size(), cmd.decompressed_size.load()); + print_data(stderr, decompressed); + } + + if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) { + throw runtime_error(string_printf( + "decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)", + decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed))); + } + auto* decompressed_cmd = reinterpret_cast(decompressed.data()); + + size_t num_floor_items = 0; + for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) { + num_floor_items += decompressed_cmd->floor_item_count_per_floor[z]; + } + + size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem); + if (decompressed.size() < required_size) { + throw runtime_error(string_printf( + "decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)", + decompressed.size(), required_size)); + } + auto* floor_items = reinterpret_cast(decompressed.data() + sizeof(G_SyncItemState_6x6D_Decompressed)); + + size_t target_num_items = target->game_data.character()->inventory.num_items; + for (size_t z = 0; z < 12; z++) { + uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z]; + uint32_t server_next_id = l->next_item_id[z]; + if (client_next_id == server_next_id) { + l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id[z]); + } else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) { + l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id[z], static_cast(server_next_id - target_num_items)); + } else { + l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")", + z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id[z]); + } + } + + // We need to byteswap mags' data2 fields if exactly one of the sender and + // recipient is PSO GC + if ((c->version() == GameVersion::GC) == (target->version() == GameVersion::GC)) { send_or_enqueue_joining_player_command(target, command, flag, data, size); } else { - bool target_is_gc = (target->version() == GameVersion::GC); - - if (target_is_gc == sender_is_gc) { - send_or_enqueue_joining_player_command(target, command, flag, data, size); - - } else { - const auto& cmd = check_size_t(data, size, 0xFFFF); - if (cmd.compressed_size > size - sizeof(cmd)) { - throw runtime_error("compressed end offset is beyond end of command"); - } - - string decompressed = bc0_decompress(reinterpret_cast(data) + sizeof(cmd), cmd.compressed_size); - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - c->log.info("Decompressed item sync data (%" PRIX32 " -> %zX bytes; expected %" PRIX32 "):", - cmd.compressed_size.load(), decompressed.size(), cmd.decompressed_size.load()); - print_data(stderr, decompressed); - } - - if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) { - throw runtime_error(string_printf( - "decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)", - decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed))); - } - auto* decompressed_cmd = reinterpret_cast(decompressed.data()); - - size_t num_floor_items = 0; - for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) { - num_floor_items += decompressed_cmd->floor_item_count_per_floor[z]; - } - - size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem); - if (decompressed.size() < required_size) { - throw runtime_error(string_printf( - "decompressed 6x6D data (0x%zX bytes) is too short for all items (0x%zX bytes)", - decompressed.size(), required_size)); - } - auto* floor_items = reinterpret_cast(decompressed.data() + sizeof(G_SyncItemState_6x6D_Decompressed)); - - for (size_t z = 0; z < num_floor_items; z++) { - // NOTE: If we use this codepath for non-V3 in the future, we'll need to - // change this hardcoded version. This only works because GC's mag - // encoding/decoding is symmetric (encode and decode do the same thing). - floor_items[z].item.decode_for_version(GameVersion::GC); - } - - string out_compressed_data = bc0_compress(decompressed); - - G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E out_cmd; - out_cmd.header.basic_header.subcommand = 0x6D; - out_cmd.header.basic_header.size = 0x00; - out_cmd.header.basic_header.unused = 0x0000; - out_cmd.header.size = ((out_compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3); - out_cmd.decompressed_size = decompressed.size(); - out_cmd.compressed_size = out_compressed_data.size(); - - if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - c->log.info("Byteswapped and recompressed item sync data (%zX bytes)", out_compressed_data.size()); - } - - StringWriter w; - w.write(&out_cmd, sizeof(out_cmd)); - w.write(out_compressed_data); - send_or_enqueue_joining_player_command(target, command, flag, std::move(w.str())); + auto s = target->require_server_state(); + for (size_t z = 0; z < num_floor_items; z++) { + floor_items[z].item.decode_for_version(c->version()); + floor_items[z].item.encode_for_version(target->version(), s->item_parameter_table_for_version(target->version())); } + + string out_compressed_data = bc0_compress(decompressed); + + G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E out_cmd; + out_cmd.header.basic_header.subcommand = 0x6D; + out_cmd.header.basic_header.size = 0x00; + out_cmd.header.basic_header.unused = 0x0000; + out_cmd.header.size = ((out_compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3); + out_cmd.decompressed_size = decompressed.size(); + out_cmd.compressed_size = out_compressed_data.size(); + + if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + c->log.info("Transcoded and recompressed item sync data (%zX bytes)", out_compressed_data.size()); + } + + StringWriter w; + w.write(&out_cmd, sizeof(out_cmd)); + w.write(out_compressed_data); + send_or_enqueue_joining_player_command(target, command, flag, std::move(w.str())); } } @@ -1307,7 +1311,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr c, uint8_t com throw runtime_error("invalid shop type"); } for (auto& item : c->game_data.shop_contents[cmd.shop_type]) { - item.id = l->generate_item_id(c->lobby_client_id); + item.id = 0xFFFFFFFF; item.data2d = s->item_parameter_table_for_version(c->version())->price_for_item(item); } @@ -1383,9 +1387,9 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint } else { // Take item auto item = p->bank.remove_item(cmd.item_id, cmd.item_amount); - item.id = l->generate_item_id(0xFF); + item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, true); string name = s->item_name_index->describe_item(GameVersion::BB, item); l->log.info("Player %hu withdrew item %08" PRIX32 " (x%hhu) (%s) from the bank", @@ -1996,9 +2000,9 @@ void on_meseta_reward_request_bb(shared_ptr c, uint8_t, uint8_t, const v ItemData item; item.data1[0] = 0x04; item.data2d = cmd.amount.load(); - item.id = l->generate_item_id(0xFF); + item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, true); } } @@ -2008,9 +2012,9 @@ void on_item_reward_request_bb(shared_ptr c, uint8_t, uint8_t, const voi ItemData item; item = cmd.item_data; - item.id = l->generate_item_id(0xFF); + item.id = l->generate_item_id(c->lobby_client_id); c->game_data.character()->add_item(item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, true); } static void on_destroy_inventory_item(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { @@ -2111,7 +2115,7 @@ static void on_accept_identify_item_bb(shared_ptr c, uint8_t command, ui throw runtime_error("accepted item ID does not match previous identify request"); } c->game_data.character()->add_item(c->game_data.identify_result); - send_create_inventory_item(c, c->game_data.identify_result); + send_create_inventory_item(c, c->game_data.identify_result, false); c->game_data.identify_result.clear(); } else { @@ -2170,19 +2174,19 @@ static void on_buy_shop_item_bb(shared_ptr c, uint8_t, uint8_t, const vo auto p = c->game_data.character(); p->remove_meseta(price, false); - item.id = cmd.inventory_item_id; + item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, true); auto s = c->require_server_state(); auto name = s->describe_item(c->version(), item, false); l->log.info("Player %hhu purchased item %08" PRIX32 " (%s) for %zu meseta", - c->lobby_client_id, cmd.inventory_item_id.load(), name.c_str(), price); + c->lobby_client_id, item.id.load(), name.c_str(), price); p->print_inventory(stderr, c->version(), s->item_name_index); if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { string name = s->describe_item(c->version(), item, true); send_text_message_printf(c, "$C5CREATE/BUY %08" PRIX32 "\n-%zu Meseta\n%s", - cmd.inventory_item_id.load(), price, name.c_str()); + item.id.load(), price, name.c_str()); } } } @@ -2261,7 +2265,7 @@ static void on_quest_exchange_item_bb(shared_ptr c, uint8_t, uint8_t, co ItemData new_item = cmd.replace_item; new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item); - send_create_inventory_item(c, new_item); + send_create_inventory_item(c, new_item, true); send_quest_function_call(c, cmd.success_function_id); @@ -2282,7 +2286,7 @@ static void on_wrap_item_bb(shared_ptr c, uint8_t, uint8_t, const void* send_destroy_item(c, item.id, 1); item.wrap(); p->add_item(cmd.item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, false); } } @@ -2303,7 +2307,7 @@ static void on_photon_drop_exchange_bb(shared_ptr c, uint8_t, uint8_t, c ItemData new_item = cmd.new_item; new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item); - send_create_inventory_item(c, new_item); + send_create_inventory_item(c, new_item, true); send_quest_function_call(c, cmd.success_function_id); @@ -2350,7 +2354,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, : s->secret_lottery_results[random_object() % s->secret_lottery_results.size()]; item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, true); } S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd; @@ -2430,7 +2434,7 @@ static void on_momoka_item_exchange_bb(shared_ptr c, uint8_t, uint8_t, c ItemData new_item = cmd.replace_item; new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item); - send_create_inventory_item(c, new_item); + send_create_inventory_item(c, new_item, true); send_command(c, 0x23, 0x00); } catch (const exception& e) { @@ -2484,7 +2488,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ item.data1[attribute_index] += attribute_amount; send_destroy_item(c, item.id, 1); - send_create_inventory_item(c, item); + send_create_inventory_item(c, item, false); send_quest_function_call(c, cmd.success_function_id); } catch (const exception& e) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ab996744..c5f518c8 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2180,11 +2180,18 @@ void send_pick_up_item(shared_ptr c, uint32_t item_id, uint8_t floor) { send_command_t(l, 0x60, 0x00, cmd); } -void send_create_inventory_item(shared_ptr c, const ItemData& item) { +void send_create_inventory_item(shared_ptr c, const ItemData& item, bool has_newest_item_id) { auto l = c->require_lobby(); if (c->version() != GameVersion::BB) { throw logic_error("6xBE can only be sent to BB clients"); } + // This command consumes an item ID on the client even though it's never used, + // because the passed-in item's ID overwrites it. If the passed-in ID was just + // generated by calling l->generate_item_id, then we shouldn't waste one here, + // but if not, we should (to keep our state in sync with the client). + if (!has_newest_item_id) { + l->generate_item_id(c->lobby_client_id); + } uint16_t client_id = c->lobby_client_id; G_CreateInventoryItem_BB_6xBE cmd = {{0xBE, 0x07, client_id}, item, 0}; send_command_t(l, 0x60, 0x00, cmd); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 5f26af20..3539dc5a 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -301,7 +301,7 @@ void send_drop_stacked_item(std::shared_ptr s, Channel& ch, const I void send_drop_stacked_item(std::shared_ptr l, const ItemData& item, uint8_t floor, float x, float z); void send_pick_up_item(std::shared_ptr c, uint32_t id, uint8_t floor); -void send_create_inventory_item(std::shared_ptr c, const ItemData& item); +void send_create_inventory_item(std::shared_ptr c, const ItemData& item, bool has_newest_item_id); void send_destroy_item(std::shared_ptr c, uint32_t item_id, uint32_t amount); void send_item_identify_result(std::shared_ptr c); void send_bank(std::shared_ptr c);