implement ep3 extended game/tournament info commands

This commit is contained in:
Martin Michelsen
2022-12-11 11:04:11 -08:00
parent 14639c63e3
commit cceaf5efde
7 changed files with 247 additions and 83 deletions
+85 -35
View File
@@ -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<char, 0x20> game_name;
struct Entry {
struct PlayerEntry {
ptext<char, 0x10> name; // From disp.name
ptext<char, 0x20> description; // Usually something like "FOmarl CLv30 J"
} __packed__;
/* 0024 */ parray<Entry, 4> entries;
/* 0024 */ parray<PlayerEntry, 4> player_entries;
/* 00E4 */ parray<uint8_t, 0x20> unknown_a3;
/* 0104 */ parray<uint8_t, 0x14> unknown_a4;
/* 0118 */ parray<uint8_t, 0x180> unknown_a5;
/* 0104 */ Episode3::Rules rules;
/* 0114 */ parray<uint8_t, 4> unknown_a4;
/* 0118 */ parray<PlayerEntry, 8> 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<char, 0x20> name;
/* 0024/034C */ ptext<char, 0x20> map_name;
/* 0044/036C */ Episode3::Rules rules;
/* 0054/037C */ parray<uint8_t, 4> 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<char, 0x20> team_name;
ptext<char, 0x18> team_name;
parray<uint8_t, 8> unused;
} __packed__;
ptext<char, 0x20> name;
ptext<char, 0x20> map_name;
parray<uint8_t, 4> unknown_a1;
Episode3::Rules rules;
parray<Entry, 0x20> entries;
parray<uint8_t, 0xE0> 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<uint8_t, 0x180> unknown_a6;
/* 0058/0380 */ parray<BracketEntry, 0x20> 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<char, 0x10> name;
ptext<char, 0x20> description; // Usually something like "RAmarl CLv24 E"
} __packed__;
struct TeamEntry {
ptext<char, 0x10> team_name;
parray<PlayerEntry, 2> players;
} __packed__;
/* 04D8/0800 */ parray<TeamEntry, 2> 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<PlayerEntry, 8> 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<uint8_t, 4> unknown_a1;
struct Entry {
uint8_t type = 0; // 1 = human, 2 = COM
uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM
ptext<char, 0x10> player_name;
ptext<char, 0x10> deck_name; // Seems to only be used for COM players
ptext<char, 0x10> deck_name; // Only be used for COM players
parray<uint8_t, 5> unknown_a1;
parray<le_uint16_t, 0x1F> card_ids;
parray<le_uint16_t, 0x1F> 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<uint8_t, 2> 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<uint8_t, 0x100> data;
} __packed__;
+1 -1
View File
@@ -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
+10
View File
@@ -235,3 +235,13 @@ uint32_t Lobby::generate_item_id(uint8_t client_id) {
}
return this->next_game_item_id++;
}
unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() const {
unordered_map<uint32_t, shared_ptr<Client>> ret;
for (auto c : this->clients) {
if (c) {
ret.emplace(c->license->serial_number, c);
}
}
return ret;
}
+2
View File
@@ -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<uint32_t, std::shared_ptr<Client>> clients_by_serial_number() const;
};
+2 -17
View File
@@ -1284,22 +1284,7 @@ static void on_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Clie
} else if ((c->flags & 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<ServerState> s, shared_ptr<Clie
}
auto tourn = s->ep3_tournament_index->get_tournament(cmd.item_id);
if (tourn) {
send_ep3_tournament_info(c, tourn);
send_ep3_tournament_details(c, tourn);
}
break;
}
+141 -30
View File
@@ -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<ServerState> s, const u16string& text) {
}
__attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
std::shared_ptr<ServerState> s, const char* format, ...) {
shared_ptr<ServerState> 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<Client> c,
std::shared_ptr<const Episode3::Tournament> 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<Client> c,
shared_ptr<const Episode3::Tournament> 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<size_t>(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<Client> 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<Client> c, shared_ptr<Lobby> l) {
shared_ptr<Lobby> 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<size_t>(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<const Episode3::Tournament::Team> 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<Lobby> l,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament::Match> match) {
shared_ptr<Lobby> l,
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament::Match> 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<uint32_t, shared_ptr<Client>> 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<const Episode3::Tournament::Team> 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<uint32_t, shared_ptr<Client>> 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<const Episode3::Tournament::Team> team) -> void {
size_t z = 0;
+6
View File
@@ -326,6 +326,12 @@ void send_ep3_tournament_match_result(
std::shared_ptr<Lobby> l,
std::shared_ptr<const Episode3::Tournament::Match> match);
void send_ep3_tournament_details(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_game_details(
std::shared_ptr<Client> c, std::shared_ptr<Lobby> 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);