diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 99daaa3d..b2bc31bd 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -289,13 +289,6 @@ struct S_Reconnect_Patch_14 : S_Reconnect { } __packed__; // a key to continue. The maximum length of the message is 0x200 bytes. // This format is shared by multiple commands; for all of them except 06 (S->C), // the guild_card_number field is unused and should be 0. -// During Episode 3 battles, the first byte of an inbound 06 command's message -// is interpreted differently. It should be treated as a bit field, with the low -// 4 bits intended as masks for who can see the message. If the low bit (1) is -// set, for example, then the chat message displays as " (whisper)" on player -// 0's screen regardless of the message contents. The next bit (2) hides the -// message from player 1, etc. The high 4 bits of this byte appear not to be -// used, but are often nonzero and set to the value 4. struct SC_TextHeader_01_06_11_B0_EE { le_uint32_t unused = 0; @@ -461,11 +454,15 @@ struct S_UpdateClientConfig_BB_04 : S_UpdateClientConfig { } __p // is 0x200 bytes. // When sent by the client, the text field includes only the message. When sent // by the server, the text field includes the origin player's name, followed by -// a tab character, followed by the message. During Episode 3 battles, chat -// messages can additionally be targeted to only your teammate; in this case, -// the message (at least, when seen by spectators) is of the form -// '\tE\t'. Messages sent to the entire battle group -// (including the opponents) are of the form '\t@\t'. +// a tab character, followed by the message. +// During Episode 3 battles, the first byte of an inbound 06 command's message +// is interpreted differently. It should be treated as a bit field, with the low +// 4 bits intended as masks for who can see the message. If the low bit (1) is +// set, for example, then the chat message displays as " (whisper)" on player +// 0's screen regardless of the message contents. The next bit (2) hides the +// message from player 1, etc. The high 4 bits of this byte appear not to be +// used, but are often nonzero and set to the value 4. We call this byte +// private_flags in the places where newserv uses it. // Client->server format is very similar; we include a zero-length array in this // struct to make parsing easier. @@ -1705,8 +1702,6 @@ struct C_UpdateQuestStatistics_V3_BB_AA { // AB (S->C): Confirm update quest statistics (V3/BB) // This command is not valid on PSO GC Episodes 1&2 Trial Edition. -// TODO: Does this command have a different meaning in Episode 3? Is it used at -// all there, or is the handler an undeleted vestige from Episodes 1&2? struct S_ConfirmUpdateQuestStatistics_V3_BB_AB { le_uint16_t unknown_a1 = 0; // 0 @@ -1893,6 +1888,14 @@ struct S_UpdateMediaHeader_GC_Ep3_B9 { // BA: Meseta transaction (Episode 3) // This command is not valid on Episode 3 Trial Edition. +// header.flag specifies the transaction purpose. This has little meaning on the +// client. Specific known values: +// 00 = unknown +// 01 = unknown (S->C; request_token must match the last token sent by client) +// 02 = Spend meseta (at e.g. lobby jukebox or Pinz's shop) (C->S) +// 03 = Spend meseta response (S->C; request_token must match the last token +// sent by client) +// 04 = unknown (S->C; request_token must match the last token sent by client) struct C_Meseta_GC_Ep3_BA { le_uint32_t transaction_num = 0; @@ -1906,20 +1909,32 @@ struct S_Meseta_GC_Ep3_BA { le_uint32_t request_token = 0; // Should match the token sent by the client } __packed__; -// BB (S->C): Unknown (Episode 3) -// header.flag is used, but it's not clear for what. It may be the number of -// valid entries, similarly to how command 07 is implemented. -// This command is not valid on Episode 3 Trial Edition. +// BB (S->C): Tournament match information (Episode 3) +// This command is not valid on Episode 3 Trial Edition. Because of this, it +// must have been added fairly late in development, but it seems to be unused, +// perhaps because the E1/E3 commands are generally more useful... but the E1/E3 +// commands exist in the Trial Edition! So why was this added? Was it just never +// finished? We may never know... +// header.flag is the number of valid match entries. -struct S_Unknown_GC_Ep3_BB { - struct Entry { - parray unknown_a1; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; +struct S_TournamentMatchInformation_GC_Ep3_BB { + ptext tournament_name; + struct TeamEntry { + le_uint16_t win_count = 0; + le_uint16_t is_active = 0; + ptext name; } __packed__; - // The first entry here is probably fake, like for ship select menus (07) - parray entries; - parray unknown_a3; + parray team_entries; + le_uint16_t num_teams = 0; + le_uint16_t unknown_a3 = 0; // Probably actually unused + struct MatchEntry { + parray name; + uint8_t locked = 0; + uint8_t count = 0; + uint8_t max_count = 0; + uint8_t unused = 0; + } __packed__; + parray match_entries; } __packed__; // BC: Invalid command @@ -2163,7 +2178,11 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server // Episode 3 will send D6 only for large message boxes that occur before the // client has joined a lobby. (After joining a lobby, large message boxes will // still be displayed if sent by the server, but the client won't send a D6 when -// they are closed.) +// they are closed.) In some of these versions, there is a bug that sets an +// incorrect interaction mode when the message box is closed while the player is +// in the lobby; some servers (e.g. Schtserv) send a lobby welcome message +// anyway, along with an 01 (lobby message box) which properly sets the +// interaction mode when closed. // D7 (C->S): Request GBA game file (V3) // The server should send the requested file using A6/A7 commands. @@ -4571,28 +4590,33 @@ struct G_AcceptItemIdentification_BB_6xBA { le_uint32_t item_id; } __packed__; -// 6xBB: Unknown (Episode 3) +// 6xBB: Sync card trade state (Episode 3) +// TODO: Certain invalid values for slot/args in this command can crash the +// client (what is properly bounds-checked). Find out the actual limits for +// slot/args and make newserv enforce them. -struct G_Unknown_GC_Ep3_6xBB { +struct G_SyncCardTradeState_GC_Ep3_6xBB { G_ClientIDHeader header; - le_uint16_t unknown_a1; // Low byte must be < 5 - le_uint16_t unknown_a2; - parray unknown_a3; + le_uint16_t what; // Must be < 5; this indexes into a jump table + le_uint16_t slot; + parray args; } __packed__; // 6xBB: BB bank request (handled by the server) -// 6xBC: Unknown (Episode 3) +// 6xBC: Card counts (Episode 3) // It's possible that this was an early, now-unused implementation of the CAx49 // command. When the client receives this command, it copies the data into a // globally-allocated array, but nothing ever reads from this array. +// Curiously, this command is smaller than 0x400 bytes, but uses the extended +// subcommand format anyway (and uses the 6D command rather than 62). -struct G_Unknown_GC_Ep3_6xBC { +struct G_CardCounts_GC_Ep3_6xBC { G_UnusedHeader header; - parray unused1; - // The length of this array strongly implies one flag or value per card type. + le_uint32_t size; parray unknown_a1; - parray unused2; + // The client sends uninitialized data in this field + parray unused; } __packed__; // 6xBC: BB bank contents (server->client only) @@ -4954,11 +4978,13 @@ struct G_Unknown_GC_Ep3_6xB5x17 { // No arguments } __packed__; -// 6xB5x1A: Unknown -// TODO: Document this from Episode 3 client/server disassembly +// 6xB5x1A: Force disconnect +// This command seems to cause the client to unconditionally disconnect. The +// player is returned to the main menu (the "The line was disconnected" message +// box is skipped). -struct G_Unknown_GC_Ep3_6xB5x1A { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0}; +struct G_ForceDisconnect_GC_Ep3_6xB5x1A { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_ForceDisconnect_GC_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0}; // No arguments } __packed__; @@ -5208,6 +5234,11 @@ struct G_PhotonBlastStatus_GC_Ep3_6xB4x35 { // 6xB5x36: Unknown // TODO: Document this from Episode 3 client/server disassembly +// Setting unknown_a1 to a value 4 or greater while in a game causes the player +// to be temporarily replaced with a default HUmar and placed inside the central +// column in the Morgue, rendering them unable to move. The only ways out of +// this predicament appear to be either to disconnect or receive an ED (force +// leave game) command. struct G_Unknown_GC_Ep3_6xB5x36 { G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0}; @@ -5223,13 +5254,15 @@ struct G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37_CAx37 { parray unused2; } __packed__; -// 6xB5x38: Unknown -// TODO: Document this from Episode 3 client/server disassembly +// 6xB5x38: Card counts request +// This command causes the client identified by requested_client_id to send a +// 6xBC command to the client identified by reply_to_client_id (privately, via +// the 6D command). -struct G_Unknown_GC_Ep3_6xB5x38 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x38) / 4, 0, 0x38, 0, 0, 0}; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; +struct G_CardCountsRequest_GC_Ep3_6xB5x38 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardCountsRequest_GC_Ep3_6xB5x38) / 4, 0, 0x38, 0, 0, 0}; + uint8_t requested_client_id = 0; + uint8_t reply_to_client_id = 0; parray unused; } __packed__; @@ -5254,13 +5287,18 @@ struct G_Unknown_GC_Ep3_6xB4x3B { parray unused; } __packed__; -// 6xB5x3C: Unknown -// TODO: Document this from Episode 3 client/server disassembly +// 6xB5x3C: Set player substatus +// This command sets the text that appears under the player's name in the HUD. struct G_Unknown_GC_Ep3_6xB5x3C { + // Note: header.sender_client_id specifies which client's status to update G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x3C) / 4, 0, 0x3C, 0, 0, 0}; - // Note: This command uses header_b1 for... something. - uint8_t unknown_a1 = 0; + // Status values: + // 00 (or any value not listed below) = (nothing) + // 01 = Editing + // 02 = Trading... + // 03 = At Counter + uint8_t status = 0; parray unused; } __packed__; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index f7fd5be3..4473368b 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -35,11 +35,12 @@ ServerBase::ServerBase( shared_ptr lobby, shared_ptr data_index, uint32_t random_seed, - bool is_tournament) + shared_ptr map_if_tournament) : lobby(lobby), data_index(data_index), random_seed(random_seed), - is_tournament(is_tournament) { } + is_tournament(!!map_if_tournament), + last_chosen_map(map_if_tournament) { } void ServerBase::init() { this->reset(); @@ -228,8 +229,9 @@ void Server::send_commands_for_joining_spectator(Channel& c) const { } } - if (this->last_chosen_map) { - string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map); + auto map = this->base()->last_chosen_map; + if (map) { + string data = this->prepare_6xB6x41_map_definition(map); c.send(0x6C, 0x00, data); } @@ -2134,13 +2136,14 @@ void Server::handle_6xB3x41_map_request(const string& data) { this->send_debug_command_received_message( cmd.header.subsubcommand, "MAP DATA"); - auto l = this->base()->lobby.lock(); + auto base = this->base(); + auto l = base->lobby.lock(); if (!l) { throw runtime_error("lobby is deleted"); } - this->last_chosen_map = this->base()->data_index->definition_for_map_number(cmd.map_number); - auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map); + base->last_chosen_map = base->data_index->definition_for_map_number(cmd.map_number); + auto out_cmd = this->prepare_6xB6x41_map_definition(base->last_chosen_map); send_command(l, 0x6C, 0x00, out_cmd); for (auto watcher_l : l->watcher_lobbies) { send_command(watcher_l, 0x6C, 0x00, out_cmd); diff --git a/src/Episode3/Server.hh b/src/Episode3/Server.hh index 06c8fa29..5452cdfd 100644 --- a/src/Episode3/Server.hh +++ b/src/Episode3/Server.hh @@ -59,7 +59,7 @@ public: std::shared_ptr lobby, std::shared_ptr data_index, uint32_t random_seed, - bool is_tournament); + std::shared_ptr map_if_tournament); void init(); void reset(); void recreate_server(); @@ -76,6 +76,7 @@ public: std::shared_ptr data_index; uint32_t random_seed; bool is_tournament; + std::shared_ptr last_chosen_map; std::shared_ptr map_and_rules1; std::shared_ptr map_and_rules2; @@ -235,7 +236,6 @@ private: static const std::unordered_map subcommand_handlers; std::weak_ptr w_base; - std::shared_ptr last_chosen_map; public: uint32_t battle_finished; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 05c08c4a..6435a41a 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -947,6 +947,18 @@ static HandlerResult S_6x(shared_ptr, false, cmd.area, cmd.x, cmd.z, cmd.request_id); session.next_drop_item.clear(); return HandlerResult::Type::SUPPRESS; + + } else if ((static_cast(data[0]) == 0xB5) && + (session.version == GameVersion::GC) && + (data.size() > 4)) { + if (data[4] == 0x1A) { + return HandlerResult::Type::SUPPRESS; + } else if (data[4] == 0x36) { + const auto& cmd = check_size_t(data); + if (session.is_in_game && (cmd.unknown_a1 >= 4)) { + return HandlerResult::Type::SUPPRESS; + } + } } } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index e2016132..b5935a1d 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1061,8 +1061,9 @@ static void on_ep3_server_data_request(shared_ptr s, shared_ptrlog.info("Recreating Episode 3 server state"); } + auto tourn = l->tournament_match ? l->tournament_match->tournament.lock() : nullptr; l->ep3_server_base = make_shared( - l, s->ep3_data_index, l->random_seed, l->tournament_match ? true : false); + l, s->ep3_data_index, l->random_seed, tourn ? tourn->get_map() : nullptr); l->ep3_server_base->init(); if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { @@ -3056,8 +3057,11 @@ static void on_client_ready(shared_ptr s, shared_ptr c, send_get_player_info(c); } + auto watched_lobby = l->watched_lobby.lock(); if (l->battle_player && (l->flags & Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY)) { l->battle_player->start(); + } else if (watched_lobby && watched_lobby->ep3_server_base) { + watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator(c->channel); } } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index ec1c5a51..1f5bfd50 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -230,10 +230,10 @@ static void on_subcommand_forward_check_size_ep3_game(shared_ptr, //////////////////////////////////////////////////////////////////////////////// // Ep3 subcommands -static void on_subcommand_ep3_battle_subs(shared_ptr, +static void on_subcommand_ep3_battle_subs(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& orig_data) { - check_size_sc( + const auto& header = check_size_sc( orig_data, sizeof(G_CardBattleCommandHeader), 0xFFFF); if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { return; @@ -242,11 +242,25 @@ static void on_subcommand_ep3_battle_subs(shared_ptr, string data = orig_data; set_mask_for_ep3_game_command(data.data(), data.size(), 0); - uint8_t mask_key = 0; - while (!mask_key) { - mask_key = random_object(); + if (header.subcommand == 0xB5) { + if (header.subsubcommand == 0x1A) { + return; + } else if (header.subsubcommand == 0x36) { + const auto& cmd = check_size_t(data); + if (l->is_game() && (cmd.unknown_a1 >= 4)) { + return; + } + } } - set_mask_for_ep3_game_command(data.data(), data.size(), mask_key); + + if (!(s->ep3_data_index->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { + uint8_t mask_key = 0; + while (!mask_key) { + mask_key = random_object(); + } + set_mask_for_ep3_game_command(data.data(), data.size(), mask_key); + } + forward_subcommand(l, c, command, flag, data); }