diff --git a/src/Client.hh b/src/Client.hh index 928b6cb6..4a3ac866 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -191,6 +191,12 @@ struct Client : public std::enable_shared_from_this { std::weak_ptr ep3_tournament_team; std::shared_ptr ep3_prev_battle_record; std::shared_ptr last_menu_sent; + struct JoinCommand { + uint16_t command; + uint32_t flag; + std::string data; + }; + std::unique_ptr> game_join_command_queue; // Miscellaneous (used by chat commands) uint32_t next_exp_value; // next EXP value to give diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 4a75ff55..d2ef037f 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -4446,7 +4446,7 @@ struct G_CreateTelepipe_6x68 { G_UnusedHeader header; le_uint16_t client_id2 = 0; le_uint16_t unknown_a1 = 0; - le_uint16_t unused1 = 0; + le_uint16_t unknown_a2 = 0; parray unused2; le_float x = 0.0f; le_float y = 0.0f; @@ -4504,8 +4504,26 @@ struct G_SyncObjectState_6x6C_Entry_Decompressed { } __packed__; // 6x6D: Sync item state (used while loading into game; same header format as 6E) +// Internal name: RcvItemCondition // Compressed format is the same as 6x6B. +// There is a bug in the client that can cause desync between players' item IDs +// if the 6x6D command is sent too quickly. 6x6D is the first command sent by +// the leader when a player joins a game, so the joining player often receives +// it immediately after the 64 command. One of the first steps of the game join +// process is to reset the next item ID variables for each player in the game, +// which under normal operation, the 6x6D command overwrites with the values in +// the next_item_id_per_player array. However, the loading process doesn't +// actually start until the next frame after the 64 command is received, so if +// the 6x6D command is received on the same frame as the 64 command, it will +// set the next item ID variables correctly and the loading process will then +// clear all of them on the next frame. The client will then assign its own +// inventory item IDs based on the default base item ID, which will result in +// incorrect IDs if another player had previously been in the game in the same +// slot. To prevent this bug, we delay the 6x6D command and everything that +// depends on it until the client responds to a ping command (sent along with +// the 64 command). + 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 diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 85a3cb9d..fcea2f7b 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -329,6 +329,18 @@ static void on_1D(shared_ptr c, uint16_t, uint32_t, string&) { double ping_ms = static_cast(ping_usecs) / 1000.0; send_text_message_printf(c, "To server: %gms", ping_ms); } + + // See the comment on the 6x6D command in CommandFormats.hh to understand why + // we do this. + if (c->game_join_command_queue) { + c->log.info("Sending %zu queued command(s)", c->game_join_command_queue->size()); + while (!c->game_join_command_queue->empty()) { + const auto& cmd = c->game_join_command_queue->front(); + send_command(c, cmd.command, cmd.flag, cmd.data); + c->game_join_command_queue->pop_front(); + } + c->game_join_command_queue.reset(); + } } static void on_05_XB(shared_ptr c, uint16_t, uint32_t, string&) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index f7dd467e..5d3a1753 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -132,6 +132,62 @@ static void on_forward_check_game(shared_ptr c, uint8_t command, uint8_t forward_subcommand(c, command, flag, data, size); } +template +static void send_or_enqueue_joining_player_command(shared_ptr c, uint8_t command, uint8_t flag, const CmdT& data) { + if (c->game_join_command_queue) { + c->log.info("Client not ready to receive join commands; adding to queue"); + auto cmd = c->game_join_command_queue->emplace_back(); + cmd.command = command; + cmd.flag = flag; + cmd.data.assign(reinterpret_cast(&data), sizeof(data)); + } else { + send_command(c, command, flag, &data, sizeof(data)); + } +} + +static void send_or_enqueue_joining_player_command(shared_ptr c, uint8_t command, uint8_t flag, string&& data) { + if (c->game_join_command_queue) { + c->log.info("Client not ready to receive join commands; adding to queue"); + auto cmd = c->game_join_command_queue->emplace_back(); + cmd.command = command; + cmd.flag = flag; + cmd.data = std::move(data); + } else { + send_command(c, command, flag, data.data(), data.size()); + } +} + +static void send_or_enqueue_joining_player_command(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + if (c->game_join_command_queue) { + c->log.info("Client not ready to receive join commands; adding to queue"); + auto cmd = c->game_join_command_queue->emplace_back(); + cmd.command = command; + cmd.flag = flag; + cmd.data.assign(reinterpret_cast(data), size); + } else { + send_command(c, command, flag, data, size); + } +} + +static void on_forward_check_game_loading(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + auto l = c->require_lobby(); + if (!l->is_game() || !l->any_client_loading()) { + return; + } + if (command_is_private(command)) { + auto target = l->clients.at(flag); + if (target) { + send_or_enqueue_joining_player_command(target, command, flag, data, size); + } + } else { + for (auto lc : l->clients) { + if (lc) { + send_or_enqueue_joining_player_command(lc, command, flag, data, size); + } + } + } +} + static void on_forward_sync_joining_player_state(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { auto l = c->require_lobby(); if (!l->is_game() || !l->any_client_loading()) { @@ -150,7 +206,7 @@ static void on_forward_sync_joining_player_state(shared_ptr c, uint8_t c print_data(stderr, decompressed); } - forward_subcommand(c, command, flag, data, size); + on_forward_check_game_loading(c, command, flag, data, size); } static void on_sync_joining_player_item_state(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { @@ -164,26 +220,26 @@ static void on_sync_joining_player_item_state(shared_ptr c, uint8_t comm if (!command_is_private(command)) { throw runtime_error("6x6D sent via public command"); } + if (flag >= l->max_clients) { + return; + } + auto target = l->clients[flag]; + if (!target) { + 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)) { - forward_subcommand(c, command, flag, data, size); + send_or_enqueue_joining_player_command(target, command, flag, data, size); } else { - if (flag >= l->max_clients) { - return; - } - auto target = l->clients[flag]; - if (!target) { - return; - } bool target_is_gc = (target->version() == GameVersion::GC); if (target_is_gc == sender_is_gc) { - send_command(target, command, flag, data, size); + send_or_enqueue_joining_player_command(target, command, flag, data, size); } else { const auto& cmd = check_size_t(data, size, 0xFFFF); @@ -239,10 +295,10 @@ static void on_sync_joining_player_item_state(shared_ptr c, uint8_t comm c->log.info("Byteswapped and recompressed item sync data (%zX bytes)", out_compressed_data.size()); } - vector> blocks; - blocks.emplace_back(make_pair(&out_cmd, sizeof(out_cmd))); - blocks.emplace_back(make_pair(out_compressed_data.data(), out_compressed_data.size())); - send_command(target, command, flag, blocks); + 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())); } } } @@ -284,7 +340,7 @@ static void on_sync_joining_player_disp_and_inventory( bool sender_is_gc = (c->version() == GameVersion::GC); bool target_is_gc = (target->version() == GameVersion::GC); if (target_is_gc == sender_is_gc) { - send_command(target, command, flag, data, size); + send_or_enqueue_joining_player_command(target, command, flag, data, size); } else if (sender_is_gc) { // Convert GC command to XB command @@ -300,7 +356,7 @@ static void on_sync_joining_player_disp_and_inventory( out_cmd.inventory.items[z].data.decode_for_version(GameVersion::GC); out_cmd.inventory.items[z].data.encode_for_version(GameVersion::XB, s->item_parameter_table_for_version(GameVersion::XB)); } - send_command_t(target, command, flag, out_cmd); + send_or_enqueue_joining_player_command(target, command, flag, out_cmd); } else { // Convert XB command to GC command @@ -312,18 +368,10 @@ static void on_sync_joining_player_disp_and_inventory( out_cmd.inventory.items[z].data.decode_for_version(GameVersion::XB); out_cmd.inventory.items[z].data.encode_for_version(GameVersion::GC, s->item_parameter_table_for_version(GameVersion::GC)); } - send_command(target, command, flag, &out_cmd, sizeof(G_SyncPlayerDispAndInventory_DC_PC_GC_6x70)); + send_or_enqueue_joining_player_command(target, command, flag, out_cmd); } } -static void on_forward_check_game_loading(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - auto l = c->require_lobby(); - if (!l->is_game() || !l->any_client_loading()) { - return; - } - forward_subcommand(c, command, flag, data, size); -} - static void on_forward_check_size_client(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { const auto& cmd = check_size_t(data, size, 0xFFFF); if (cmd.client_id != c->lobby_client_id) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 9e6d7b6a..034b2442 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1691,6 +1691,10 @@ void send_join_game(shared_ptr c, shared_ptr l) { default: throw logic_error("invalid game version"); } + + c->log.info("Creating game join command queue"); + c->game_join_command_queue.reset(new deque()); + send_command(c, 0x1D, 0x00); } template diff --git a/tests/DCv1-GameSmokeTest.test.txt b/tests/DCv1-GameSmokeTest.test.txt index 624e2843..8a17cc40 100644 --- a/tests/DCv1-GameSmokeTest.test.txt +++ b/tests/DCv1-GameSmokeTest.test.txt @@ -517,6 +517,10 @@ I 40469 2023-05-26 10:41:30 - [Commands] Sending to C-2 (Tali) (version=DC comma 00E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0100 | 00 00 00 00 00 00 01 00 00 00 04 00 94 1B EE 22 | " +I 40469 2023-05-26 10:41:41 - [Commands] Sending to C-2 (Tali) (version=DC command=1D flag=00) +0000 | 1D 00 04 00 | +I 40469 2023-05-26 10:41:41 - [Commands] Received from C-2 (Tali) (version=DC command=1D flag=00) +0000 | 1D 00 04 00 | I 40469 2023-05-26 10:41:41 - [Commands] Received from C-2 (Tali) (version=DC command=60 flag=00) 0000 | 60 00 1C 00 3F 06 00 00 00 00 00 C0 00 00 00 00 | ` ? 0010 | CE FE 64 43 00 00 00 00 B8 FF 7D 43 | dC }C diff --git a/tests/DCv2-GameSmokeTest.test.txt b/tests/DCv2-GameSmokeTest.test.txt index c20c368a..e9ad2ee7 100644 --- a/tests/DCv2-GameSmokeTest.test.txt +++ b/tests/DCv2-GameSmokeTest.test.txt @@ -649,6 +649,10 @@ I 40992 2023-05-26 10:53:41 - [Commands] Sending to C-2 (Tali) (version=DC comma 00E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0100 | 00 00 00 00 00 00 01 00 00 00 04 00 21 07 72 30 | ! r0 +I 40992 2023-05-26 10:53:42 - [Commands] Sending to C-2 (Tali) (version=DC command=1D flag=00) +0000 | 1D 00 04 00 | +I 40992 2023-05-26 10:53:42 - [Commands] Received from C-2 (Tali) (version=DC command=1D flag=00) +0000 | 1D 00 04 00 | I 40992 2023-05-26 10:53:42 - [Commands] Received from C-2 (Tali) (version=DC command=8A flag=00) 0000 | 8A 00 04 00 | I 40992 2023-05-26 10:53:42 - [Commands] Sending to C-2 (Tali) (version=DC command=8A flag=00) diff --git a/tests/GC-Episode3Battle.test.txt b/tests/GC-Episode3Battle.test.txt index e4ff742e..9636d5b8 100644 --- a/tests/GC-Episode3Battle.test.txt +++ b/tests/GC-Episode3Battle.test.txt @@ -3996,6 +3996,10 @@ I 16332 2023-09-17 10:14:46 - [Commands] Received from C-2 (Tali) (version=GC co 02F0 | CF 0C 3D 32 FB E8 89 AE 67 04 15 6A 13 60 E1 66 | =2 g j ` f 0300 | FF FC ED A2 2B D8 39 1E | + 9 I 16332 2023-09-17 10:14:46 - [Lobby/15] Creating Episode 3 server state +I 16332 2023-09-17 10:14:46 - [Commands] Sending to C-2 (Tali) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | +I 16332 2023-09-17 10:14:46 - [Commands] Received from C-2 (Tali) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | I 16332 2023-09-17 10:14:46 - [Commands] Sending to C-2 (Tali) (version=GC command=C9 flag=00) 0000 | C9 00 24 00 B4 08 00 00 03 00 00 00 01 00 00 FF | $ 0010 | FF 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 00 | diff --git a/tests/GC-Episode3BattleWithSpectator.test.txt b/tests/GC-Episode3BattleWithSpectator.test.txt index 49ea7773..709d71c5 100644 --- a/tests/GC-Episode3BattleWithSpectator.test.txt +++ b/tests/GC-Episode3BattleWithSpectator.test.txt @@ -3939,6 +3939,10 @@ I 17097 2023-09-19 21:52:56 - [Commands] Sending to C-2 (Tali) (version=GC comma 1160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 1170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 1180 | 00 00 00 00 | +I 17097 2023-09-19 21:52:57 - [Commands] Sending to C-2 (Tali) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | +I 17097 2023-09-19 21:52:57 - [Commands] Received from C-2 (Tali) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | I 17097 2023-09-19 21:52:57 - [Commands] Received from C-2 (Tali) (version=GC command=C9 flag=00) 0000 | C9 00 10 00 B5 03 00 00 47 00 00 00 03 00 00 00 | G I 17097 2023-09-19 21:52:59 - [Commands] Received from C-2 (Tali) (version=GC command=C9 flag=00) diff --git a/tests/GC-ForestGame.test.txt b/tests/GC-ForestGame.test.txt index 52e04585..74b52f0f 100644 --- a/tests/GC-ForestGame.test.txt +++ b/tests/GC-ForestGame.test.txt @@ -654,6 +654,10 @@ I 49108 2023-05-26 16:18:32 - [Commands] Sending to C-2 (Jess) (version=GC comma 00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0100 | 00 00 00 00 00 00 01 00 00 00 05 00 61 49 35 3D | aI5= 0110 | 01 00 00 00 | +I 49108 2023-05-26 16:18:33 - [Commands] Sending to C-2 (Jess) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | +I 49108 2023-05-26 16:18:33 - [Commands] Received from C-2 (Jess) (version=GC command=1D flag=00) +0000 | 1D 00 04 00 | I 49108 2023-05-26 16:18:33 - [Commands] Received from C-2 (Jess) (version=GC command=8A flag=00) 0000 | 8A 00 04 00 | I 49108 2023-05-26 16:18:33 - [Commands] Sending to C-2 (Jess) (version=GC command=8A flag=00) diff --git a/tests/PC-BasicGame.test.txt b/tests/PC-BasicGame.test.txt index 840aa957..83e938f2 100644 --- a/tests/PC-BasicGame.test.txt +++ b/tests/PC-BasicGame.test.txt @@ -490,6 +490,10 @@ I 49484 2023-05-26 16:35:40 - [Commands] Sending to C-3 (Tali) (version=PC comma 0120 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0130 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0140 | 00 00 00 00 00 00 01 00 00 00 04 00 DB 6E A2 8C | n +I 49484 2023-05-26 16:35:40 - [Commands] Sending to C-3 (Tali) (version=DC command=1D flag=00) +0000 | 04 00 1D 00 | +I 49484 2023-05-26 16:35:40 - [Commands] Received from C-3 (Tali) (version=DC command=1D flag=00) +0000 | 04 00 1D 00 | I 49484 2023-05-26 16:35:40 - [Commands] Received from C-3 (Tali) (version=PC command=8A flag=00) 0000 | 04 00 8A 00 | I 49484 2023-05-26 16:35:40 - [Commands] Sending to C-3 (Tali) (version=PC command=8A flag=00) diff --git a/tests/XB-ForestGame.test.txt b/tests/XB-ForestGame.test.txt index 41db43ed..9e9a62bd 100644 --- a/tests/XB-ForestGame.test.txt +++ b/tests/XB-ForestGame.test.txt @@ -452,6 +452,10 @@ I 16496 2023-11-08 01:54:41 - [Commands] Sending to C-2 (Tali) (version=XB comma 01B0 | 00 00 00 00 00 00 01 00 00 00 04 00 50 E0 96 09 | P 01C0 | 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 | +I 16496 2023-11-08 01:54:42 - [Commands] Sending to C-2 (Tali) (version=XB command=1D flag=00) +0000 | 1D 00 04 00 | +I 16496 2023-11-08 01:54:42 - [Commands] Received from C-2 (Tali) (version=XB command=1D flag=00) +0000 | 1D 00 04 00 | I 16496 2023-11-08 01:54:42 - [Commands] Received from C-2 (Tali) (version=XB command=8A flag=00) 0000 | 8A 00 04 00 | I 16496 2023-11-08 01:54:42 - [Commands] Sending to C-2 (Tali) (version=XB command=8A flag=00)