From 398a93b56f2663b23645fc47b38a75cb8e80e847 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 11 Dec 2022 13:31:11 -0800 Subject: [PATCH] implement spectator teams --- README.md | 1 + src/ChatCommands.cc | 26 ++++++++++++++++++++++++- src/CommandFormats.hh | 5 ++++- src/Episode3/Server.cc | 6 ++++++ src/ReceiveCommands.cc | 41 ++++++++++++++++++++++++++++----------- src/ReceiveCommands.hh | 1 + src/ReceiveSubcommands.cc | 17 ++++++++++++++-- src/SendCommands.cc | 29 ++++++++++++++++++++++----- src/SendCommands.hh | 6 +++++- 9 files changed, 111 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ae05b340..78e77f4f 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ Some commands only work on the game server and not on the proxy server. The chat * `$maxlevel `: Sets the maximum level for players to join the current game. * `$minlevel `: Sets the minimum level for players to join the current game. * `$password `: Sets the game's join password. To unlock the game, run `$password` with nothing after it. + * `$spec`: Toggles the allow spectators flag. If any players are spectating when this flag is disabled, they will be sent back to the lobby. * Cheat mode commands * `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. This command does nothing on the proxy server - cheat commands are always available there. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index fa7fceeb..edc4aec0 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -434,7 +434,7 @@ static void server_command_playrec(shared_ptr s, shared_ptr shared_ptr record(new Episode3::BattleRecord(data)); shared_ptr battle_player( new Episode3::BattleRecordPlayer(record, s->game_server->get_base())); - auto game = create_game_generic(s, c, args.c_str(), u"", 0xFF, 0, flags, battle_player); + auto game = create_game_generic(s, c, args.c_str(), u"", 0xFF, 0, flags, nullptr, battle_player); } } @@ -516,6 +516,29 @@ static void server_command_password(shared_ptr, shared_ptr l } } +static void server_command_spec(shared_ptr, shared_ptr l, + shared_ptr c, const std::u16string&) { + check_is_game(l, true); + check_is_leader(l, c); + check_is_ep3(c, true); + if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + throw logic_error("Episode 3 client in non-Episode 3 game"); + } + + if (l->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) { + l->flags &= ~Lobby::Flag::SPECTATORS_FORBIDDEN; + send_text_message(l, u"$C6Spectators allowed"); + + } else { + l->flags |= Lobby::Flag::SPECTATORS_FORBIDDEN; + for (auto watcher_l : l->watcher_lobbies) { + send_command(watcher_l, 0xED, 0x00); + } + l->watcher_lobbies.clear(); + send_text_message(l, u"$C6Spectators forbidden"); + } +} + static void server_command_min_level(shared_ptr, shared_ptr l, shared_ptr c, const std::u16string& args) { check_is_game(l, true); @@ -1066,6 +1089,7 @@ static const unordered_map chat_commands({ {u"$silence", {server_command_silence, nullptr, u"Usage:\nsilence "}}, // TODO: implement this on proxy server {u"$song", {server_command_song, nullptr, u"Usage:\nsong "}}, + {u"$spec", {server_command_spec, nullptr, u"Usage:\nspec"}}, {u"$swa", {server_command_switch_assist, proxy_command_switch_assist, u"Usage:\nswa"}}, {u"$type", {server_command_lobby_type, nullptr, u"Usage:\ntype "}}, {u"$warp", {server_command_warp, proxy_command_warp, u"Usage:\nwarp "}}, diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 40572df7..99daaa3d 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1963,7 +1963,10 @@ struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry template struct C_CreateGame { - parray unused; + // menu_id and item_id are only used for the E7 (create spectator team) form + // of this command + le_uint32_t menu_id; + le_uint32_t item_id; ptext name; ptext password; uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3) diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index bc879699..f7fd5be3 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -2119,6 +2119,9 @@ void Server::handle_6xB3x40_map_list_request(const string& data) { G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0}); w.write(list_data); send_command(l, 0x6C, 0x00, w.str()); + for (auto watcher_l : l->watcher_lobbies) { + send_command(watcher_l, 0x6C, 0x00, w.str()); + } if (l->battle_record && l->battle_record->writable()) { l->battle_record->add_command( @@ -2139,6 +2142,9 @@ void Server::handle_6xB3x41_map_request(const string& data) { 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); send_command(l, 0x6C, 0x00, out_cmd); + for (auto watcher_l : l->watcher_lobbies) { + send_command(watcher_l, 0x6C, 0x00, out_cmd); + } if (l->battle_record && l->battle_record->writable()) { l->battle_record->add_command( diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 69c5ae47..e2016132 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -963,7 +963,7 @@ static bool start_ep3_tournament_match_if_pending( // TODO: We don't know if this works with multiple players. Test it. uint32_t flags = Lobby::Flag::NON_V1_ONLY | Lobby::Flag::EPISODE_3_ONLY; - auto game = create_game_generic(s, c, u"", u"", 0xFF, 0, flags); + auto game = create_game_generic(s, c, decode_sjis(tourn->get_name()), u"", 0xFF, 0, flags); game->tournament_match = match; for (auto game_c : game_clients) { send_command_t(game_c, 0xC9, 0x00, state_cmd); @@ -1174,10 +1174,10 @@ static void on_ep3_tournament_control(shared_ptr s, shared_ptr s, shared_ptr c, } static void on_game_list_request(shared_ptr s, shared_ptr c, - uint16_t, uint32_t, const string& data) { // 08 + uint16_t command, uint32_t, const string& data) { // 08 E6 check_size_v(data.size(), 0); - send_game_menu(c, s); + send_game_menu(c, s, (command == 0xE6), false); } static void on_info_menu_request_dc_pc(shared_ptr s, @@ -2796,6 +2796,7 @@ shared_ptr create_game_generic( uint8_t episode, uint8_t difficulty, uint32_t flags, + shared_ptr watched_lobby, shared_ptr battle_player) { // A player's actual level is their displayed level - 1, so the minimums for @@ -2857,6 +2858,10 @@ shared_ptr create_game_generic( game->max_clients = (game->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 12 : 4; game->min_level = min_level; game->max_level = 0xFFFFFFFF; + if (watched_lobby) { + game->watched_lobby = watched_lobby; + watched_lobby->watcher_lobbies.emplace(game); + } bool is_solo = (game->flags & Lobby::Flag::SOLO_MODE); @@ -2939,12 +2944,12 @@ static void on_create_game_pc(shared_ptr s, shared_ptr c, } static void on_create_game_dc_v3(shared_ptr s, shared_ptr c, - uint16_t command, uint32_t, const string& data) { // 0C C1 EC (EC Ep3 only) + uint16_t command, uint32_t, const string& data) { // 0C C1 E7 EC (E7/EC are Ep3 only) const auto& cmd = check_size_t(data); - // Only allow EC from Ep3 clients + // Only allow E7/EC from Ep3 clients bool client_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3); - if ((command == 0xEC) != client_is_ep3) { + if (((command & 0xF0) == 0xE0) != client_is_ep3) { return; } @@ -2975,8 +2980,22 @@ static void on_create_game_dc_v3(shared_ptr s, shared_ptr c flags |= Lobby::Flag::CHALLENGE_MODE; } } + + shared_ptr watched_lobby; + if (command == 0xE7) { + if (cmd.menu_id != MenuID::GAME) { + throw runtime_error("incorrect menu ID"); + } + watched_lobby = s->find_lobby(cmd.item_id); + if (watched_lobby->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) { + send_lobby_message_box(c, u"$C6This game does not\nallow spectators"); + return; + } + flags |= Lobby::Flag::IS_SPECTATOR_TEAM; + } + auto game = create_game_generic( - s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, flags); + s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, flags, watched_lobby); s->change_client_lobby(c, game); c->flags |= Client::Flag::LOADING; } @@ -3702,8 +3721,8 @@ static on_command_t handlers[0x100][6] = { /* E3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_player_preview_request_bb, }, /* E3 */ /* E4 */ {nullptr, nullptr, nullptr, on_ep3_battle_table_state, nullptr, nullptr, }, /* E4 */ /* E5 */ {nullptr, nullptr, nullptr, on_ep3_battle_table_confirm, nullptr, on_create_character_bb, }, /* E5 */ - /* E6 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* E6 */ - /* E7 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_return_player_data_bb, }, /* E7 */ + /* E6 */ {nullptr, nullptr, nullptr, on_game_list_request, nullptr, nullptr, }, /* E6 */ + /* E7 */ {nullptr, nullptr, nullptr, on_create_game_dc_v3, nullptr, on_return_player_data_bb, }, /* E7 */ /* E8 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_client_checksum_bb, }, /* E8 */ /* E9 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* E9 */ /* EA */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_team_command_bb, }, /* EA */ diff --git a/src/ReceiveCommands.hh b/src/ReceiveCommands.hh index dca4512b..7f9890c5 100644 --- a/src/ReceiveCommands.hh +++ b/src/ReceiveCommands.hh @@ -14,6 +14,7 @@ std::shared_ptr create_game_generic( uint8_t episode, uint8_t difficulty, uint32_t flags, + std::shared_ptr watched_lobby = nullptr, std::shared_ptr battle_player = nullptr); void on_connect(std::shared_ptr s, std::shared_ptr c); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index d2bc7579..ec1c5a51 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -63,6 +63,12 @@ const CmdT& check_size_sc( +static const unordered_set watcher_subcommands({ + 0x07, // Symbol chat + 0x74, // Word select + 0xBD, // Word select during battle (with private_flags) +}); + static void forward_subcommand(shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { @@ -98,8 +104,15 @@ static void forward_subcommand(shared_ptr l, shared_ptr c, send_command_excluding_client(l, c, command, flag, data, size); } - for (const auto& watcher_lobby : l->watcher_lobbies) { - forward_subcommand(watcher_lobby, c, command, flag, data, size); + // Before battle, forward only chat commands to watcher lobbies; during + // battle, forward everything to watcher lobbies. + if (size && + (watcher_subcommands.count(*reinterpret_cast(data) || + (l->ep3_server_base && + l->ep3_server_base->server->setup_phase != Episode3::SetupPhase::REGISTRATION)))) { + for (const auto& watcher_lobby : l->watcher_lobbies) { + forward_subcommand(watcher_lobby, c, command, flag, data, size); + } } if (l->battle_record && l->battle_record->battle_in_progress()) { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index f42672ff..df9cc3f6 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1084,7 +1084,11 @@ void send_menu(shared_ptr c, const u16string& menu_name, template -void send_game_menu_t(shared_ptr c, shared_ptr s) { +void send_game_menu_t( + shared_ptr c, + shared_ptr s, + bool is_spectator_team_list, + bool is_tournament_game_list) { vector> entries; { auto& e = entries.emplace_back(); @@ -1100,6 +1104,7 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { if (!l->is_game() || (l->version != c->version())) { continue; } + bool l_is_ep3 = !!(l->flags & Lobby::Flag::EPISODE_3_ONLY); bool c_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3); if (l_is_ep3 != c_is_ep3) { @@ -1109,6 +1114,15 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { continue; } + bool l_is_spectator_team = !!(l->flags & Lobby::Flag::IS_SPECTATOR_TEAM); + if (l_is_spectator_team != is_spectator_team_list) { + continue; + } + bool l_is_tournament_game = !!l->tournament_match; + if (l_is_tournament_game != is_tournament_game_list) { + continue; + } + auto& e = entries.emplace_back(); e.menu_id = MenuID::GAME; e.game_id = l->lobby_id; @@ -1136,16 +1150,20 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { e.name = l->name; } - send_command_vt(c, 0x08, entries.size() - 1, entries); + send_command_vt(c, is_spectator_team_list ? 0xE6 : 0x08, entries.size() - 1, entries); } -void send_game_menu(shared_ptr c, shared_ptr s) { +void send_game_menu( + shared_ptr c, + shared_ptr s, + bool is_spectator_team_list, + bool is_tournament_game_list) { if ((c->version() == GameVersion::DC) || (c->version() == GameVersion::GC) || (c->version() == GameVersion::XB)) { - send_game_menu_t(c, s); + send_game_menu_t(c, s, is_spectator_team_list, is_tournament_game_list); } else { - send_game_menu_t(c, s); + send_game_menu_t(c, s, is_spectator_team_list, is_tournament_game_list); } } @@ -2064,6 +2082,7 @@ void send_ep3_game_details(shared_ptr c, shared_ptr l) { entry.team_name = teams[z]->name; } cmd.num_bracket_entries = teams.size(); + cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1; if (primary_lobby) { auto serial_number_to_client = primary_lobby->clients_by_serial_number(); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index fb1b1d81..d5ab7d9c 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -234,7 +234,11 @@ void send_guild_card( void send_guild_card(std::shared_ptr c, std::shared_ptr source); void send_menu(std::shared_ptr c, const std::u16string& menu_name, uint32_t menu_id, const std::vector& items, bool is_info_menu = false); -void send_game_menu(std::shared_ptr c, std::shared_ptr s); +void send_game_menu( + std::shared_ptr c, + std::shared_ptr s, + bool is_spectator_team_list, + bool is_tournament_game_list); void send_quest_menu(std::shared_ptr c, uint32_t menu_id, const std::vector>& quests, bool is_download_menu); void send_quest_menu(std::shared_ptr c, uint32_t menu_id,