From cceaf5efdea1ef4d3c3067beb4b255f3f2e49178 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 11 Dec 2022 11:04:11 -0800 Subject: [PATCH] implement ep3 extended game/tournament info commands --- src/CommandFormats.hh | 120 +++++++++++++++++-------- src/Episode3/PlayerState.cc | 2 +- src/Lobby.cc | 10 +++ src/Lobby.hh | 2 + src/ReceiveCommands.cc | 19 +--- src/SendCommands.cc | 171 +++++++++++++++++++++++++++++------- src/SendCommands.hh | 6 ++ 7 files changed, 247 insertions(+), 83 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7c75d963..40572df7 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2074,9 +2074,9 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> { } __packed__; // Same as 60, but only send to Episode 3 clients. // This command is identical to C9, except that CB is not valid on Episode 3 // Trial Edition (whereas C9 is valid). -// TODO: What's the difference here? The client-side handlers are identical, so -// presumably the server is supposed to do something different between the two -// commands, but it's not clear what that difference should be. +// TODO: Presumably this command is meant to be forwarded from spectator teams +// to the primary team, whereas 6x and C9 commands are not. I haven't verified +// this or implemented this behavior yet though. // CC (S->C): Confirm tournament entry (Episode 3) // This command is not valid on Episode 3 Trial Edition. @@ -2351,17 +2351,24 @@ struct S_TournamentList_GC_Ep3_E0 { // E0 (C->S): Request team and key config (BB) // E1 (S->C): Game information (Episode 3) +// The header.flag argument determines which fields are valid (and which panes +// should be shown in the information window). The values are the same as for +// the E3 command, but each value only makes sense for one command. That is, 00, +// 01, and 04 should be used with the E1 command, while 02, 03, and 05 should be +// used with the E3 command. See the E3 command for descriptions of what each +// flag value means. struct S_GameInformation_GC_Ep3_E1 { /* 0004 */ ptext game_name; - struct Entry { + struct PlayerEntry { ptext name; // From disp.name ptext description; // Usually something like "FOmarl CLv30 J" } __packed__; - /* 0024 */ parray entries; + /* 0024 */ parray player_entries; /* 00E4 */ parray unknown_a3; - /* 0104 */ parray unknown_a4; - /* 0118 */ parray unknown_a5; + /* 0104 */ Episode3::Rules rules; + /* 0114 */ parray unknown_a4; + /* 0118 */ parray spectator_entries; } __packed__; // E2 (C->S): Tournament control (Episode 3) @@ -2410,25 +2417,63 @@ struct S_TournamentEntryList_GC_Ep3_E2 { // E2 (S->C): Team and key config (BB) // See KeyAndTeamConfigBB in Player.hh for format -// E3 (S->C): Tournament info (Episode 3) +// E3 (S->C): Game or tournament info (Episode 3) +// The header.flag argument determines which fields are valid (and which panes +// should be shown in the information window). The values are: +// flag=00: Opponents pane only +// flag=01: Opponents and Rules panes +// flag=02: Rules and bracket panes (the bracket pane uses the tournament's name +// as its title) +// flag=03: Opponents, Rules, and bracket panes +// flag=04: Spectators and Opponents pane +// flag=05: Spectators, Opponents, Rules, and bracket panes +// Sending other values in the header.flag field results in a blank info window +// with unintended strings appearing in the window title. +// Presumably the cases above would be used in different scenarios, probably: +// 00: When inspecting a non-tournament game with no battle in progress +// 01: When inspecting a non-tournament game with a battle in progress +// 02: When inspecting tournaments that have not yet started +// 03: When inspecting a tournament match +// 04: When inspecting a non-tournament spectator team +// 05: When inspecting a tournament spectator team +// The 00, 01, and 04 cases don't really make sense, because the E1 command is +// more appropriate for inspecting non-tournament games. -struct S_TournamentInfo_GC_Ep3_E3 { - struct Entry { +struct S_TournamentGameDetails_GC_Ep3_E3 { + // These fields are used only if the Rules pane is shown + /* 0004/032C */ ptext name; + /* 0024/034C */ ptext map_name; + /* 0044/036C */ Episode3::Rules rules; + + /* 0054/037C */ parray unknown_a1; + + // This field is used only if the bracket pane is shown + struct BracketEntry { le_uint16_t win_count = 0; le_uint16_t is_active = 0; - ptext team_name; + ptext team_name; + parray unused; } __packed__; - ptext name; - ptext map_name; - parray unknown_a1; - Episode3::Rules rules; - parray entries; - parray unknown_a2; - le_uint16_t max_entries = 0; - le_uint16_t unknown_a3 = 1; - le_uint16_t unknown_a4 = 0; - le_uint16_t unknown_a5 = 0; - parray unknown_a6; + /* 0058/0380 */ parray bracket_entries; + + // This field is used only if the Opponents pane is shown. If players_per_team + // is 2, all fields are shown; if player_per_team is 1, team_name and + // players[1] is ignored (only players[0] is shown). + struct PlayerEntry { + ptext name; + ptext description; // Usually something like "RAmarl CLv24 E" + } __packed__; + struct TeamEntry { + ptext team_name; + parray players; + } __packed__; + /* 04D8/0800 */ parray team_entries; + + /* 05B8/08E0 */ le_uint16_t num_bracket_entries = 0; + /* 05BA/08E2 */ le_uint16_t players_per_team = 0; + /* 05BC/08E4 */ le_uint16_t unknown_a4 = 0; + /* 05BE/08E6 */ le_uint16_t num_spectators = 0; + /* 05C0/08E8 */ parray spectator_entries; } __packed__; // E3 (C->S): Player preview request (BB) @@ -2769,10 +2814,10 @@ struct S_StreamFileChunk_BB_02EB { // No arguments // Server should respond with a 01EB command. -// EC: Create game (Episode 3) +// EC (C->S): Create game (Episode 3) // Same format as C1; some fields are unused (e.g. episode, difficulty). -// EC: Leave character select (BB) +// EC (C->S): Leave character select (BB) struct C_LeaveCharacterSelect_BB_00EC { // Reason codes: @@ -4586,6 +4631,7 @@ struct G_BankAction_BB_6xBD { } __packed__; // 6xBE: Sound chat (Episode 3; not Trial Edition) +// This appears to be the only subcommand ever sent with the CB command. struct G_SoundChat_GC_Ep3_6xBE { G_UnusedHeader header; @@ -4743,8 +4789,8 @@ struct G_SetStateFlags_GC_Ep3_6xB4x03 { // 6xB4x04: Update SC/FC short statuses -struct G_UpdateStats_GC_Ep3_6xB4x04 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateStats_GC_Ep3_6xB4x04) / 4, 0, 0x04, 0, 0, 0}; +struct G_UpdateShortStatuses_GC_Ep3_6xB4x04 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateShortStatuses_GC_Ep3_6xB4x04) / 4, 0, 0x04, 0, 0, 0}; le_uint16_t client_id = 0; le_uint16_t unused = 0; // The slots in this array have heterogeneous meanings. Specifically: @@ -5224,11 +5270,11 @@ struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D { Episode3::Rules rules; parray unknown_a1; struct Entry { - uint8_t type = 0; // 1 = human, 2 = COM + uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM ptext player_name; - ptext deck_name; // Seems to only be used for COM players + ptext deck_name; // Only be used for COM players parray unknown_a1; - parray card_ids; + parray card_ids; // Can be blank for human players uint8_t client_id = 0; // Unused for COMs uint8_t unknown_a4 = 0; le_uint16_t unknown_a2 = 0; @@ -5252,7 +5298,7 @@ struct G_MakeCardAuctionBid_GC_Ep3_6xB5x3E { parray unused; } __packed__; -// 6xB5x3F: Open menu +// 6xB5x3F: Open blocking menu struct G_OpenBlockingMenu_GC_Ep3_6xB5x3F { G_CardBattleCommandHeader header = {0xB5, sizeof(G_OpenBlockingMenu_GC_Ep3_6xB5x3F) / 4, 0, 0x3F, 0, 0, 0}; @@ -5382,7 +5428,11 @@ struct G_EndTurn_GC_Ep3_6xB3x48_CAx48 { // verification at battle start time.) Sega presumably could have used this to // detect the presence of unreleased cards to ban cheaters, but the effects of // the non-saveable Have All Cards AR code don't appear in this data, so this -// would have been ineffective. +// would have been ineffective. There appears to be a place where Sega's server +// intended to use this data, however - the deck verification function takes a +// pointer to the card counts array, but Sega's implementation always passes +// null there, which skips the owned card count check. newserv uses this data at +// that callsite. struct G_CardCounts_GC_Ep3_6xB3x49_CAx49 { G_CardServerDataCommandHeader header = {0xB3, sizeof(G_CardCounts_GC_Ep3_6xB3x49_CAx49) / 4, 0, 0x49, 0, 0, 0, 0, 0}; @@ -5524,10 +5574,10 @@ struct G_TournamentMatchResult_GC_Ep3_6xB4x51 { struct G_Unknown_GC_Ep3_6xB4x52 { G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x52) / 4, 0, 0x52, 0, 0, 0}; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t size = 0; // Number of valid bytes in the data field (clamped to 0xFF) + le_uint16_t unknown_a1 = 0; // Clamped to [0, 999] by the client + le_uint16_t unknown_a2 = 0; // Clamped to [0, 999] by the client + le_uint16_t unused = 0; + le_uint16_t size = 0; // Number of used bytes in data (clamped to 0xFF) parray data; } __packed__; diff --git a/src/Episode3/PlayerState.cc b/src/Episode3/PlayerState.cc index 56a83b2e..35da12bd 100644 --- a/src/Episode3/PlayerState.cc +++ b/src/Episode3/PlayerState.cc @@ -1444,7 +1444,7 @@ void PlayerState::set_random_assist_card_from_hand_for_free() { } void PlayerState::send_6xB4x04_if_needed(bool always_send) { - G_UpdateStats_GC_Ep3_6xB4x04 cmd; + G_UpdateShortStatuses_GC_Ep3_6xB4x04 cmd; cmd.client_id = this->client_id; // Note: The original code calls memset to clear all the short status structs // at once. We don't do this because the default constructor has already diff --git a/src/Lobby.cc b/src/Lobby.cc index 68d0ed8c..7b034742 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -235,3 +235,13 @@ uint32_t Lobby::generate_item_id(uint8_t client_id) { } return this->next_game_item_id++; } + +unordered_map> Lobby::clients_by_serial_number() const { + unordered_map> ret; + for (auto c : this->clients) { + if (c) { + ret.emplace(c->license->serial_number, c); + } + } + return ret; +} diff --git a/src/Lobby.hh b/src/Lobby.hh index 43a4baa6..0051969c 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -133,4 +133,6 @@ struct Lobby { uint32_t generate_item_id(uint8_t client_id); static uint8_t game_event_for_lobby_event(uint8_t lobby_event); + + std::unordered_map> clients_by_serial_number() const; }; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index e2b27e0a..69c5ae47 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1284,22 +1284,7 @@ static void on_menu_item_info_request(shared_ptr s, shared_ptrflags & Client::Flag::IS_EPISODE_3) && (game->flags & Lobby::Flag::EPISODE_3_ONLY)) { - S_GameInformation_GC_Ep3_E1 cmd; - cmd.game_name = encode_sjis(game->name); - size_t num_players = 0; - for (const auto& client : game->clients) { - if (client) { - auto player = client->game_data.player(); - cmd.entries[num_players].name = player->disp.name; - cmd.entries[num_players].description = string_printf( - "%s CLv%" PRIu32 " %c", - name_for_char_class(player->disp.char_class), - player->disp.level + 1, - char_for_language_code(player->inventory.language)); - num_players++; - } - } - send_command_t(c, 0xE1, 0x00, cmd); + send_ep3_game_details(c, game); } else { string info; @@ -1401,7 +1386,7 @@ static void on_menu_item_info_request(shared_ptr s, shared_ptrep3_tournament_index->get_tournament(cmd.item_id); if (tourn) { - send_ep3_tournament_info(c, tourn); + send_ep3_tournament_details(c, tourn); } break; } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 3fc7def0..f42672ff 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -16,6 +16,7 @@ #include "Compression.hh" #include "FileContentsCache.hh" #include "Text.hh" +#include "StaticGameData.hh" using namespace std; @@ -704,12 +705,12 @@ void send_text_message(shared_ptr s, const u16string& text) { } __attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf( - std::shared_ptr s, const char* format, ...) { + shared_ptr s, const char* format, ...) { va_list va; va_start(va, format); - std::string buf = string_vprintf(format, va); + string buf = string_vprintf(format, va); va_end(va); - std::u16string decoded = decode_sjis(buf); + u16string decoded = decode_sjis(buf); for (auto& it : s->id_to_lobby) { for (auto& c : it.second->clients) { if (c && (c->flags & Client::Flag::IS_EPISODE_3)) { @@ -2006,27 +2007,147 @@ void send_ep3_tournament_entry_list( send_command_t(c, 0xE2, z, cmd); } -void send_ep3_tournament_info( - std::shared_ptr c, - std::shared_ptr t) { - S_TournamentInfo_GC_Ep3_E3 cmd; - cmd.name = t->get_name(); - cmd.map_name = t->get_map()->map.name; - cmd.rules = t->get_rules(); - const auto& teams = t->all_teams(); +void send_ep3_tournament_details( + shared_ptr c, + shared_ptr tourn) { + S_TournamentGameDetails_GC_Ep3_E3 cmd; + cmd.name = tourn->get_name(); + cmd.map_name = tourn->get_map()->map.name; + cmd.rules = tourn->get_rules(); + const auto& teams = tourn->all_teams(); for (size_t z = 0; z < min(teams.size(), 0x20); z++) { - cmd.entries[z].win_count = teams[z]->num_rounds_cleared; - cmd.entries[z].is_active = teams[z]->is_active ? 1 : 0; - cmd.entries[z].team_name = teams[z]->name; + cmd.bracket_entries[z].win_count = teams[z]->num_rounds_cleared; + cmd.bracket_entries[z].is_active = teams[z]->is_active ? 1 : 0; + cmd.bracket_entries[z].team_name = teams[z]->name; } - cmd.max_entries = teams.size(); + cmd.num_bracket_entries = teams.size(); + cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1; send_command_t(c, 0xE3, 0x02, cmd); } +string ep3_description_for_client(shared_ptr c) { + if (!(c->flags & Client::Flag::IS_EPISODE_3)) { + throw runtime_error("client is not Episode 3"); + } + auto player = c->game_data.player(); + return string_printf( + "%s CLv%" PRIu32 " %c", + name_for_char_class(player->disp.char_class), + player->disp.level + 1, + char_for_language_code(player->inventory.language)); +} + +void send_ep3_game_details(shared_ptr c, shared_ptr l) { + + shared_ptr primary_lobby; + if (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) { + primary_lobby = l->watched_lobby.lock(); + } else { + primary_lobby = l; + } + + auto tourn_match = primary_lobby ? primary_lobby->tournament_match : nullptr; + auto tourn = tourn_match ? tourn_match->tournament.lock() : nullptr; + + if (tourn) { + S_TournamentGameDetails_GC_Ep3_E3 cmd; + cmd.name = encode_sjis(l->name); + + cmd.map_name = tourn->get_map()->map.name; + cmd.rules = tourn->get_rules(); + + const auto& teams = tourn->all_teams(); + for (size_t z = 0; z < min(teams.size(), 0x20); z++) { + auto& entry = cmd.bracket_entries[z]; + entry.win_count = teams[z]->num_rounds_cleared; + entry.is_active = teams[z]->is_active ? 1 : 0; + entry.team_name = teams[z]->name; + } + cmd.num_bracket_entries = teams.size(); + + if (primary_lobby) { + auto serial_number_to_client = primary_lobby->clients_by_serial_number(); + auto describe_team = [&](S_TournamentGameDetails_GC_Ep3_E3::TeamEntry& entry, shared_ptr team) -> void { + entry.team_name = team->name; + size_t z = 0; + for (uint32_t serial_number : team->player_serial_numbers) { + auto c = serial_number_to_client.at(serial_number); + auto& player_entry = entry.players[z++]; + player_entry.name = c->game_data.player()->disp.name; + player_entry.description = ep3_description_for_client(c); + } + for (auto com_deck : team->com_decks) { + auto& player_entry = entry.players[z++]; + player_entry.name = com_deck->player_name; + player_entry.description = "Deck: " + com_deck->deck_name; + } + }; + describe_team(cmd.team_entries[0], tourn_match->preceding_a->winner_team); + describe_team(cmd.team_entries[1], tourn_match->preceding_b->winner_team); + } + + uint8_t flag; + if (l != primary_lobby) { + for (auto spec_c : l->clients) { + if (spec_c) { + auto& entry = cmd.spectator_entries[cmd.num_spectators++]; + entry.name = encode_sjis(spec_c->game_data.player()->disp.name); + entry.description = ep3_description_for_client(spec_c); + } + } + flag = 0x05; + } else { + flag = 0x03; + } + send_command_t(c, 0xE3, flag, cmd); + + } else { + S_GameInformation_GC_Ep3_E1 cmd; + cmd.game_name = encode_sjis(l->name); + if (primary_lobby) { + size_t num_players = 0; + for (const auto& opp_c : primary_lobby->clients) { + if (opp_c) { + cmd.player_entries[num_players].name = opp_c->game_data.player()->disp.name; + cmd.player_entries[num_players].description = ep3_description_for_client(opp_c); + num_players++; + } + } + } + + uint8_t flag; + if (l != primary_lobby) { + // TODO: This doesn't work (nothing shows up), but it appears to be a + // client bug? There doesn't appear to be a count field in the command + // anywhere...? + size_t num_spectators = 0; + for (auto spec_c : l->clients) { + if (spec_c) { + auto& entry = cmd.spectator_entries[num_spectators++]; + entry.name = encode_sjis(spec_c->game_data.player()->disp.name); + entry.description = ep3_description_for_client(spec_c); + } + } + flag = 0x04; + + } else if (primary_lobby && + primary_lobby->ep3_server_base && + primary_lobby->ep3_server_base->server->get_setup_phase() != Episode3::SetupPhase::REGISTRATION) { + cmd.rules = primary_lobby->ep3_server_base->map_and_rules1->rules; + flag = 0x01; + + } else { + flag = 0x00; + } + + send_command_t(c, 0xE1, flag, cmd); + } +} + void send_ep3_set_tournament_player_decks( - std::shared_ptr l, - std::shared_ptr c, - std::shared_ptr match) { + shared_ptr l, + shared_ptr c, + shared_ptr match) { auto tourn = match->tournament.lock(); if (!tourn) { throw runtime_error("tournament is deleted"); @@ -2046,12 +2167,7 @@ void send_ep3_set_tournament_player_decks( entry.client_id = z; } - unordered_map> serial_number_to_client; - for (auto client : l->clients) { - if (client) { - serial_number_to_client.emplace(client->license->serial_number, client); - } - } + auto serial_number_to_client = l->clients_by_serial_number(); size_t z = 0; auto add_entries_for_team = [&](shared_ptr team) -> void { @@ -2113,12 +2229,7 @@ void send_ep3_tournament_match_result( throw logic_error("cannot send tournament result without valid winner team"); } - unordered_map> serial_number_to_client; - for (auto client : l->clients) { - if (client) { - serial_number_to_client.emplace(client->license->serial_number, client); - } - } + auto serial_number_to_client = l->clients_by_serial_number(); auto write_player_names = [&](G_TournamentMatchResult_GC_Ep3_6xB4x51::NamesEntry& entry, shared_ptr team) -> void { size_t z = 0; diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 7e250b5a..fb1b1d81 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -326,6 +326,12 @@ void send_ep3_tournament_match_result( std::shared_ptr l, std::shared_ptr match); +void send_ep3_tournament_details( + std::shared_ptr c, + std::shared_ptr t); +void send_ep3_game_details( + std::shared_ptr c, std::shared_ptr l); + // Pass mask_key = 0 to unmask the command void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);