From 1d45c18ce8880109303411fd00752cbd0c85e453 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 15 Sep 2023 22:09:31 -0700 Subject: [PATCH] keep tournament state consistent on clients --- src/Episode3/Tournament.cc | 247 ++++++++++++++++++++----------- src/Episode3/Tournament.hh | 56 +++++-- src/Main.cc | 1 + src/ReceiveCommands.cc | 37 +---- src/SendCommands.cc | 11 +- src/Server.cc | 18 +-- src/Server.hh | 1 - src/ServerShell.cc | 17 +-- src/ServerState.cc | 5 +- src/ServerState.hh | 8 +- tests/GC-Episode3Battle.test.txt | 164 ++++++++++++++++++++ 11 files changed, 404 insertions(+), 161 deletions(-) diff --git a/src/Episode3/Tournament.cc b/src/Episode3/Tournament.cc index beeed4c5..14aca400 100644 --- a/src/Episode3/Tournament.cc +++ b/src/Episode3/Tournament.cc @@ -10,8 +10,11 @@ using namespace std; namespace Episode3 { Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number) - : serial_number(serial_number), - com_deck() {} + : serial_number(serial_number) {} + +Tournament::PlayerEntry::PlayerEntry(shared_ptr c) + : serial_number(c->license->serial_number), + client(c) {} Tournament::PlayerEntry::PlayerEntry( shared_ptr com_deck) @@ -57,7 +60,7 @@ string Tournament::Team::str() const { } void Tournament::Team::register_player( - uint32_t serial_number, + shared_ptr c, const string& team_name, const string& password) { if (this->players.size() >= this->max_players) { @@ -72,17 +75,17 @@ void Tournament::Team::register_player( if (!tournament) { throw runtime_error("tournament has been deleted"); } - if (!tournament->all_player_serial_numbers.emplace(serial_number).second) { + if (!tournament->all_player_serial_numbers.emplace(c->license->serial_number).second) { throw runtime_error("player already registered in same tournament"); } for (const auto& player : this->players) { - if (player.is_human() && (player.serial_number == serial_number)) { + if (player.is_human() && (player.serial_number == c->license->serial_number)) { throw logic_error("player already registered in team but not in tournament"); } } - this->players.emplace_back(serial_number); + this->players.emplace_back(c); if (this->name.empty()) { this->name = team_name; @@ -290,24 +293,23 @@ shared_ptr Tournament::Match::opponent_team_for_team( Tournament::Tournament( shared_ptr map_index, shared_ptr com_deck_index, - uint8_t number, const string& name, shared_ptr map, const Rules& rules, size_t num_teams, bool is_2v2, bool has_com_teams) - : log(string_printf("[Tournament/%02hhX] ", number)), + : log(string_printf("[Tournament/%s] ", name.c_str())), map_index(map_index), com_deck_index(com_deck_index), - number(number), name(name), map(map), rules(rules), num_teams(num_teams), is_2v2(is_2v2), has_com_teams(has_com_teams), - current_state(State::REGISTRATION) { + current_state(State::REGISTRATION), + menu_item_id(0xFFFFFFFF) { if (this->num_teams < 4) { throw invalid_argument("team count must be 4 or more"); } @@ -322,13 +324,11 @@ Tournament::Tournament( Tournament::Tournament( shared_ptr map_index, shared_ptr com_deck_index, - uint8_t number, const JSON& json) - : log(string_printf("[Tournament/%02hhX] ", number)), + : log(string_printf("[Tournament/%s] ", json.get_string("name").c_str())), map_index(map_index), com_deck_index(com_deck_index), source_json(json), - number(number), current_state(State::REGISTRATION) {} void Tournament::init() { @@ -380,27 +380,27 @@ void Tournament::init() { this->shared_from_this(), this->teams[this->zero_round_matches.size()])); } - // Create the bracket matches - vector> current_round_matches = this->zero_round_matches; - while (current_round_matches.size() > 1) { - vector> next_round_matches; - for (size_t z = 0; z < current_round_matches.size(); z += 2) { - auto m = make_shared( - this->shared_from_this(), - current_round_matches[z], - current_round_matches[z + 1]); - current_round_matches[z]->following = m; - current_round_matches[z + 1]->following = m; - next_round_matches.emplace_back(std::move(m)); - } - current_round_matches = std::move(next_round_matches); - } - this->final_match = current_round_matches.at(0); - // Compute the match state from the teams' states if (is_registration_complete) { this->current_state = State::IN_PROGRESS; + // Create the bracket matches + vector> current_round_matches = this->zero_round_matches; + while (current_round_matches.size() > 1) { + vector> next_round_matches; + for (size_t z = 0; z < current_round_matches.size(); z += 2) { + auto m = make_shared( + this->shared_from_this(), + current_round_matches[z], + current_round_matches[z + 1]); + current_round_matches[z]->following = m; + current_round_matches[z + 1]->following = m; + next_round_matches.emplace_back(std::move(m)); + } + current_round_matches = std::move(next_round_matches); + } + this->final_match = current_round_matches.at(0); + // Start with all first-round matches in the match queue unordered_set> match_queue; for (auto match : this->zero_round_matches) { @@ -501,6 +501,9 @@ shared_ptr Tournament::get_winner_team() const { if (this->current_state != State::COMPLETE) { return nullptr; } + if (!this->final_match) { + throw logic_error("tournament is complete but final match is missing"); + } if (!this->final_match->winner_team) { throw logic_error("tournament is complete but winner is not set"); } @@ -604,6 +607,23 @@ void Tournament::start() { } } +void Tournament::send_all_state_updates(shared_ptr s) const { + for (const auto& team : this->teams) { + for (const auto& player : team->players) { + auto c = player.client.lock(); + // Note: The last check here is to make sure the client is still linked + // with this instance of the tournament - an intervening shell command + // `reload ep3` could have changed the client's linkage + if (c && + (c->flags & Client::Flag::IS_EPISODE_3) && + !(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) && + (c->ep3_tournament_team.lock() == team)) { + send_ep3_confirm_tournament_entry(s, c, this->shared_from_this()); + } + } + } +} + void Tournament::print_bracket(FILE* stream) const { function, size_t)> print_match = [&](shared_ptr m, size_t indent_level) -> void { for (size_t z = 0; z < indent_level; z++) { @@ -619,7 +639,7 @@ void Tournament::print_bracket(FILE* stream) const { print_match(m->preceding_b, indent_level + 1); } }; - fprintf(stream, "Tournament %02hhX: %s\n", this->number, this->name.c_str()); + fprintf(stream, "Tournament \"%s\"\n", this->name.c_str()); string map_name = this->map->map.name; fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str()); string rules_str = this->rules.str(); @@ -639,8 +659,10 @@ void Tournament::print_bracket(FILE* stream) const { fprintf(stream, " State: UNKNOWN\n"); break; } - fprintf(stream, " Standings:\n"); - print_match(this->final_match, 2); + if (this->final_match) { + fprintf(stream, " Standings:\n"); + print_match(this->final_match, 2); + } fprintf(stream, " Pending matches:\n"); for (const auto& match : this->pending_matches) { string match_str = match->str(); @@ -667,14 +689,38 @@ TournamentIndex::TournamentIndex( json = JSON::list(); } - if (json.size() > 0x20) { - throw runtime_error("tournament JSON list length is incorrect"); - } - for (size_t z = 0; z < min(json.size(), 0x20); z++) { - if (!json.at(z).is_null()) { - this->tournaments[z].reset(new Tournament(this->map_index, this->com_deck_index, z, json.at(z))); - this->tournaments[z]->init(); + if (json.is_list()) { + if (json.size() > 0x20) { + throw runtime_error("tournament JSON list length is incorrect"); } + for (size_t z = 0; z < min(json.size(), 0x20); z++) { + if (!json.at(z).is_null()) { + shared_ptr tourn(new Tournament(this->map_index, this->com_deck_index, json.at(z))); + tourn->init(); + if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) { + throw runtime_error("multiple tournaments have the same name: " + tourn->get_name()); + } + tourn->set_menu_item_id(this->menu_item_id_to_tournament.size()); + this->menu_item_id_to_tournament.emplace_back(tourn); + } + } + } else if (json.is_dict()) { + if (json.size() > 0x20) { + throw runtime_error("tournament JSON dict length is incorrect"); + } + for (const auto& it : json.as_dict()) { + shared_ptr tourn(new Tournament(this->map_index, this->com_deck_index, *it.second)); + tourn->init(); + if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) { + // This is logic_error instead of runtime_error because JSON dicts are + // supposed to already have unique keys + throw logic_error("multiple tournaments have the same name: " + tourn->get_name()); + } + tourn->set_menu_item_id(this->menu_item_id_to_tournament.size()); + this->menu_item_id_to_tournament.emplace_back(tourn); + } + } else { + throw runtime_error("tournament state root JSON is not a list or dict"); } } @@ -683,25 +729,11 @@ void TournamentIndex::save() const { return; } - auto list = JSON::list(); - for (size_t z = 0; z < 0x20; z++) { - if (this->tournaments[z]) { - list.emplace_back(this->tournaments[z]->json()); - } else { - list.emplace_back(nullptr); - } + auto json = JSON::dict(); + for (const auto& it : this->name_to_tournament) { + json.emplace(it.second->get_name(), it.second->json()); } - save_file(this->state_filename, list.serialize(JSON::SerializeOption::FORMAT)); -} - -vector> TournamentIndex::all_tournaments() const { - vector> ret; - for (size_t z = 0; z < 0x20; z++) { - if (this->tournaments[z]) { - ret.emplace_back(this->tournaments[z]); - } - } - return ret; + save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT)); } shared_ptr TournamentIndex::create_tournament( @@ -711,48 +743,52 @@ shared_ptr TournamentIndex::create_tournament( size_t num_teams, bool is_2v2, bool has_com_teams) { - // Find an unused tournament number - uint8_t number; - for (number = 0; number < 0x20; number++) { - if (!this->tournaments[number]) { - break; - } - } - if (number >= 0x20) { - throw runtime_error("all tournament slots are full"); + if (this->name_to_tournament.size() >= 0x20) { + throw runtime_error("there can be at most 32 tournaments at a time"); } auto t = make_shared( - this->map_index, this->com_deck_index, number, name, map, rules, num_teams, is_2v2, has_com_teams); + this->map_index, this->com_deck_index, name, map, rules, num_teams, is_2v2, has_com_teams); t->init(); - this->tournaments[number] = t; + this->name_to_tournament.emplace(t->get_name(), t); + + size_t z; + for (z = 0; z < this->menu_item_id_to_tournament.size(); z++) { + if (!this->menu_item_id_to_tournament[z]) { + t->set_menu_item_id(z); + this->menu_item_id_to_tournament[z] = t; + break; + } + } + if (z == this->menu_item_id_to_tournament.size()) { + t->set_menu_item_id(this->menu_item_id_to_tournament.size()); + this->menu_item_id_to_tournament.emplace_back(t); + } + + this->save(); return t; } -void TournamentIndex::delete_tournament(uint8_t number) { - this->tournaments[number].reset(); -} - -shared_ptr TournamentIndex::get_tournament(uint8_t number) const { - return this->tournaments[number]; -} - -shared_ptr TournamentIndex::get_tournament(const string& name) const { - for (size_t z = 0; z < 0x20; z++) { - if (this->tournaments[z] && (this->tournaments[z]->get_name() == name)) { - return this->tournaments[z]; +bool TournamentIndex::delete_tournament(const string& name) { + auto it = this->name_to_tournament.find(name); + if (it == this->name_to_tournament.end()) { + return false; + } + for (size_t z = 0; z < this->menu_item_id_to_tournament.size(); z++) { + if (this->menu_item_id_to_tournament[z] == it->second) { + this->menu_item_id_to_tournament[z] = nullptr; + it->second->set_menu_item_id(0xFFFFFFFF); } } - return nullptr; + this->name_to_tournament.erase(it); + this->save(); + return true; } -shared_ptr TournamentIndex::team_for_serial_number( - uint32_t serial_number) const { - for (size_t z = 0; z < 0x20; z++) { - if (!this->tournaments[z]) { - continue; - } - auto team = this->tournaments[z]->team_for_serial_number(serial_number); +shared_ptr TournamentIndex::team_for_serial_number(uint32_t serial_number) const { + for (const auto& it : this->name_to_tournament) { + const auto& tourn = it.second; + auto team = tourn->team_for_serial_number(serial_number); if (team) { return team; } @@ -760,4 +796,37 @@ shared_ptr TournamentIndex::team_for_serial_number( return nullptr; } +void TournamentIndex::link_client(shared_ptr s, shared_ptr c) { + if (!(c->flags & Client::Flag::IS_EPISODE_3)) { + return; + } + + auto team = this->team_for_serial_number(c->license->serial_number); + auto tourn = team ? team->tournament.lock() : nullptr; + if (team && tourn) { + for (auto& player : team->players) { + if (player.serial_number == c->license->serial_number) { + c->ep3_tournament_team = team; + player.client = c; + if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) { + send_ep3_confirm_tournament_entry(s, c, tourn); + } + return; + } + } + throw logic_error("tournament team found for player, but player not found on team"); + } else { + c->ep3_tournament_team.reset(); + if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) { + send_ep3_confirm_tournament_entry(s, c, nullptr); + } + } +} + +void TournamentIndex::link_all_clients(std::shared_ptr s) { + for (const auto& c_it : s->channel_to_client) { + this->link_client(s, c_it.second); + } +} + } // namespace Episode3 diff --git a/src/Episode3/Tournament.hh b/src/Episode3/Tournament.hh index 21d114c1..142742d2 100644 --- a/src/Episode3/Tournament.hh +++ b/src/Episode3/Tournament.hh @@ -13,6 +13,8 @@ #include "../Player.hh" struct Lobby; +struct Client; +struct ServerState; namespace Episode3 { @@ -32,7 +34,11 @@ public: uint32_t serial_number; std::shared_ptr com_deck; + // client is valid if serial_number is nonzero and the client is connected + std::weak_ptr client; + explicit PlayerEntry(uint32_t serial_number); + explicit PlayerEntry(std::shared_ptr c); explicit PlayerEntry(std::shared_ptr com_deck); bool is_com() const; @@ -57,7 +63,7 @@ public: std::string str() const; void register_player( - uint32_t serial_number, + std::shared_ptr c, const std::string& team_name, const std::string& password); bool unregister_player(uint32_t serial_number); @@ -94,7 +100,6 @@ public: Tournament( std::shared_ptr map_index, std::shared_ptr com_deck_index, - uint8_t number, const std::string& name, std::shared_ptr map, const Rules& rules, @@ -104,16 +109,12 @@ public: Tournament( std::shared_ptr map_index, std::shared_ptr com_deck_index, - uint8_t number, const JSON& json); ~Tournament() = default; void init(); JSON json() const; - inline uint8_t get_number() const { - return this->number; - } inline const std::string& get_name() const { return this->name; } @@ -135,9 +136,15 @@ public: inline const std::vector>& all_teams() const { return this->teams; } - std::shared_ptr get_team(size_t index) const { + inline std::shared_ptr get_team(size_t index) const { return this->teams.at(index); } + inline uint32_t get_menu_item_id() const { + return this->menu_item_id; + } + inline void set_menu_item_id(uint32_t menu_item_id) { + this->menu_item_id = menu_item_id; + } std::shared_ptr get_winner_team() const; std::shared_ptr next_match_for_team(std::shared_ptr team) const; @@ -147,6 +154,8 @@ public: void start(); + void send_all_state_updates(std::shared_ptr s) const; + void print_bracket(FILE* stream) const; private: @@ -155,7 +164,6 @@ private: std::shared_ptr map_index; std::shared_ptr com_deck_index; JSON source_json; - uint8_t number; std::string name; std::shared_ptr map; Rules rules; @@ -163,6 +171,7 @@ private: bool is_2v2; bool has_com_teams; State current_state; + uint32_t menu_item_id; std::set all_player_serial_numbers; std::unordered_set> pending_matches; @@ -191,7 +200,23 @@ public: void save() const; - std::vector> all_tournaments() const; + inline const std::unordered_map>& all_tournaments() const { + return this->name_to_tournament; + } + inline std::shared_ptr get_tournament(uint32_t menu_item_id) const { + try { + return this->menu_item_id_to_tournament.at(menu_item_id); + } catch (const std::out_of_range&) { + return nullptr; + } + } + inline std::shared_ptr get_tournament(const std::string& name) const { + try { + return this->name_to_tournament.at(name); + } catch (const std::out_of_range&) { + return nullptr; + } + } std::shared_ptr create_tournament( const std::string& name, @@ -200,18 +225,19 @@ public: size_t num_teams, bool is_2v2, bool has_com_teams); - void delete_tournament(uint8_t number); - std::shared_ptr get_tournament(uint8_t number) const; - std::shared_ptr get_tournament(const std::string& name) const; + bool delete_tournament(const std::string& name); - std::shared_ptr team_for_serial_number( - uint32_t serial_number) const; + std::shared_ptr team_for_serial_number(uint32_t serial_number) const; + + void link_client(std::shared_ptr s, std::shared_ptr c); + void link_all_clients(std::shared_ptr s); private: std::shared_ptr map_index; std::shared_ptr com_deck_index; std::string state_filename; - std::shared_ptr tournaments[0x20]; + std::unordered_map> name_to_tournament; + std::vector> menu_item_id_to_tournament; }; } // namespace Episode3 diff --git a/src/Main.cc b/src/Main.cc index b66aaf1c..cb35827c 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1655,6 +1655,7 @@ int main(int argc, char** argv) { shared_ptr base(event_base_new(), event_base_free); shared_ptr state(new ServerState(config_filename, is_replay)); + state->init(); shared_ptr dns_server; if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 4309b93f..53179827 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -231,9 +231,7 @@ static void send_main_menu(shared_ptr s, shared_ptr c) { void on_login_complete(shared_ptr s, shared_ptr c) { if (c->flags & Client::Flag::IS_EPISODE_3) { - auto team = s->ep3_tournament_index->team_for_serial_number(c->license->serial_number); - auto tourn = team ? team->tournament.lock() : nullptr; - c->ep3_tournament_team = team; + s->ep3_tournament_index->link_client(s, c); } // On the BB data server, this function is called only on the last connection @@ -1227,25 +1225,7 @@ static void on_DC_Ep3(shared_ptr s, shared_ptr c, static void on_tournament_bracket_updated( shared_ptr s, shared_ptr tourn) { - const auto& serial_numbers = tourn->get_all_player_serial_numbers(); - - for (const auto& l : s->all_lobbies()) { - for (const auto& c : l->clients) { - if (!c || - !c->license || - !serial_numbers.count(c->license->serial_number) || - c->ep3_tournament_team.expired() || - (c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) { - continue; - } - send_ep3_confirm_tournament_entry(s, c, tourn); - } - } - - if (tourn && (tourn->get_state() == Episode3::Tournament::State::COMPLETE)) { - s->ep3_tournament_index->delete_tournament(tourn->get_number()); - } - + tourn->send_all_state_updates(s); if (tourn->get_state() == Episode3::Tournament::State::COMPLETE) { auto team = tourn->get_winner_team(); if (!team->has_any_human_players()) { @@ -1253,10 +1233,10 @@ static void on_tournament_bracket_updated( } else { send_ep3_text_message_printf(s, "$C6%s$C7\nwon the tournament\n$C6%s", team->name.c_str(), tourn->get_name().c_str()); } - s->ep3_tournament_index->delete_tournament(tourn->get_number()); + s->ep3_tournament_index->delete_tournament(tourn->get_name()); + } else { + s->ep3_tournament_index->save(); } - - s->ep3_tournament_index->save(); } static void on_CA_Ep3(shared_ptr s, shared_ptr c, @@ -2060,12 +2040,9 @@ static void on_10(shared_ptr s, shared_ptr c, auto team = tourn->get_team(team_index); if (team) { try { - team->register_player( - c->license->serial_number, - encode_sjis(team_name), - encode_sjis(password)); + team->register_player(c, encode_sjis(team_name), encode_sjis(password)); c->ep3_tournament_team = team; - send_ep3_confirm_tournament_entry(s, c, tourn); + tourn->send_all_state_updates(s); string message = string_printf("$C7You are registered in $C6%s$C7.\n\ \n\ After registration ends, start your matches by\n\ diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 4e4bfc1f..a1175ea4 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2285,7 +2285,8 @@ void send_ep3_tournament_list( bool is_for_spectator_team_create) { S_TournamentList_GC_Ep3_E0 cmd; size_t z = 0; - for (const auto& tourn : s->ep3_tournament_index->all_tournaments()) { + for (const auto& it : s->ep3_tournament_index->all_tournaments()) { + const auto& tourn = it.second; if (z >= 0x20) { throw logic_error("more than 32 tournaments exist"); } @@ -2293,7 +2294,7 @@ void send_ep3_tournament_list( entry.menu_id = is_for_spectator_team_create ? MenuID::TOURNAMENTS_FOR_SPEC : MenuID::TOURNAMENTS; - entry.item_id = tourn->get_number(); + entry.item_id = tourn->get_menu_item_id(); // TODO: What does it mean for a tournament to be locked? Should we support // that? // TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is @@ -2333,7 +2334,7 @@ void send_ep3_tournament_entry_list( } auto& entry = cmd.entries[z]; entry.menu_id = MenuID::TOURNAMENT_ENTRIES; - entry.item_id = (tourn->get_number() << 16) | z; + entry.item_id = (tourn->get_menu_item_id() << 16) | z; entry.unknown_a2 = team->num_rounds_cleared; entry.locked = team->password.empty() ? 0 : 1; if (tourn->get_state() != Episode3::Tournament::State::REGISTRATION) { @@ -2597,8 +2598,8 @@ void send_ep3_tournament_match_result( send_command_t(l, 0xC9, 0x00, cmd); if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { - send_text_message_printf(l, "$C5TOURN/%02hhX/%zu WIN %c", - tourn->get_number(), match->round_num, + send_text_message_printf(l, "$C5TOURN/%" PRIX32 "/%zu WIN %c", + tourn->get_menu_item_id(), match->round_num, match->winner_team == match->preceding_a->winner_team ? 'A' : 'B'); } } diff --git a/src/Server.cc b/src/Server.cc index 3523a3f7..60efa81e 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -38,7 +38,7 @@ void Server::disconnect_client(shared_ptr c) { c->id, bufferevent_getfd(c->channel.bev.get())); } - this->channel_to_client.erase(&c->channel); + this->state->channel_to_client.erase(&c->channel); c->channel.disconnect(); try { @@ -118,7 +118,7 @@ void Server::on_listen_accept(struct evconnlistener* listener, c->channel.on_command_received = Server::on_client_input; c->channel.on_error = Server::on_client_error; c->channel.context_obj = this; - this->channel_to_client.emplace(&c->channel, c); + this->state->channel_to_client.emplace(&c->channel, c); server_log.info("Client connected: C-%" PRIX64 " on fd %d via %d (%s)", c->id, fd, listen_fd, listening_socket->addr_str.c_str()); @@ -148,7 +148,7 @@ void Server::connect_client( name_for_version(version), name_for_server_behavior(initial_state)); - this->channel_to_client.emplace(&c->channel, c); + this->state->channel_to_client.emplace(&c->channel, c); // Manually set the remote address, since the bufferevent has no fd and the // Channel constructor can't figure out the virtual remote address @@ -174,7 +174,7 @@ void Server::on_listen_error(struct evconnlistener* listener) { void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) { Server* server = reinterpret_cast(ch.context_obj); - shared_ptr c = server->channel_to_client.at(&ch); + shared_ptr c = server->state->channel_to_client.at(&ch); if (c->should_disconnect) { server->disconnect_client(c); @@ -197,7 +197,7 @@ void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std:: void Server::on_client_error(Channel& ch, short events) { Server* server = reinterpret_cast(ch.context_obj); - shared_ptr c = server->channel_to_client.at(&ch); + shared_ptr c = server->state->channel_to_client.at(&ch); if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); @@ -271,13 +271,13 @@ void Server::add_socket( } shared_ptr Server::get_client() const { - if (this->channel_to_client.empty()) { + if (this->state->channel_to_client.empty()) { throw runtime_error("no clients on game server"); } - if (this->channel_to_client.size() > 1) { + if (this->state->channel_to_client.size() > 1) { throw runtime_error("multiple clients on game server"); } - return this->channel_to_client.begin()->second; + return this->state->channel_to_client.begin()->second; } vector> Server::get_clients_by_identifier(const string& ident) const { @@ -296,7 +296,7 @@ vector> Server::get_clients_by_identifier(const string& ident // TODO: It's kind of not great that we do a linear search here, but this is // only used in the shell, so it should be pretty rare. vector> results; - for (const auto& it : this->channel_to_client) { + for (const auto& it : this->state->channel_to_client) { auto c = it.second; if (c->license && c->license->serial_number == serial_number_dec) { results.emplace_back(std::move(c)); diff --git a/src/Server.hh b/src/Server.hh index a97ef921..e80e413a 100644 --- a/src/Server.hh +++ b/src/Server.hh @@ -52,7 +52,6 @@ private: ServerBehavior behavior); }; std::unordered_map listening_sockets; - std::unordered_map> channel_to_client; std::unordered_set> clients_to_destroy; std::shared_ptr state; diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 030cb8f9..7a3e198a 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -182,7 +182,7 @@ Server commands:\n\ start-tournament TOURNAMENT-NAME\n\ End registration for a tournament and allow matches to begin. Quotes are\n\ required around the tournament name unless the name contains no spaces.\n\ - tournament-state TOURNAMENT-NAME\n\ + describe-tournament TOURNAMENT-NAME\n\ Show the current state of a tournament. Quotes are required around the\n\ tournament name unless the name contains no spaces.\n\ \n\ @@ -540,23 +540,19 @@ Proxy session commands:\n\ } auto tourn = this->state->ep3_tournament_index->create_tournament( name, map, rules, num_teams, is_2v2, has_com_teams); - this->state->ep3_tournament_index->save(); - fprintf(stderr, "created tournament %02hhX\n", tourn->get_number()); + fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str()); } else if (command_name == "delete-tournament") { string name = get_quoted_string(command_args); - auto tourn = this->state->ep3_tournament_index->get_tournament(name); - if (tourn) { - this->state->ep3_tournament_index->delete_tournament(tourn->get_number()); - this->state->ep3_tournament_index->save(); + if (this->state->ep3_tournament_index->delete_tournament(name)) { fprintf(stderr, "tournament deleted\n"); } else { fprintf(stderr, "no such tournament exists\n"); } } else if (command_name == "list-tournaments") { - for (const auto& tourn : this->state->ep3_tournament_index->all_tournaments()) { - fprintf(stderr, " %s\n", tourn->get_name().c_str()); + for (const auto& it : this->state->ep3_tournament_index->all_tournaments()) { + fprintf(stderr, " %s\n", it.second->get_name().c_str()); } } else if (command_name == "start-tournament") { @@ -565,13 +561,14 @@ Proxy session commands:\n\ if (tourn) { tourn->start(); this->state->ep3_tournament_index->save(); + tourn->send_all_state_updates(this->state); send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str()); fprintf(stderr, "tournament started\n"); } else { fprintf(stderr, "no such tournament exists\n"); } - } else if (command_name == "tournament-status") { + } else if (command_name == "describe-tournament") { string name = get_quoted_string(command_args); auto tourn = this->state->ep3_tournament_index->get_tournament(name); if (tourn) { diff --git a/src/ServerState.cc b/src/ServerState.cc index 1331a1d4..a38f242c 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -41,7 +41,9 @@ ServerState::ServerState(const char* config_filename, bool is_replay) local_address(0), external_address(0), proxy_allow_save_files(true), - proxy_enable_login_options(false) { + proxy_enable_login_options(false) {} + +void ServerState::init() { vector> non_v1_only_lobbies; vector> ep3_only_lobbies; @@ -875,6 +877,7 @@ void ServerState::load_ep3_data() { const string& tournament_state_filename = "system/ep3/tournament-state.json"; this->ep3_tournament_index.reset(new Episode3::TournamentIndex( this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename)); + this->ep3_tournament_index->link_all_clients(this->shared_from_this()); config_log.info("Loaded Episode 3 tournament state"); } diff --git a/src/ServerState.hh b/src/ServerState.hh index c423a882..55c76842 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -33,7 +33,7 @@ struct PortConfiguration { ServerBehavior behavior; }; -struct ServerState { +struct ServerState : public std::enable_shared_from_this { enum class RunShellBehavior { DEFAULT = 0, ALWAYS, @@ -129,6 +129,7 @@ struct ServerState { std::u16string pc_patch_server_message; std::u16string bb_patch_server_message; + std::unordered_map> channel_to_client; std::map> id_to_lobby; std::vector> public_lobby_search_order_v1; std::vector> public_lobby_search_order_non_v1; @@ -148,6 +149,11 @@ struct ServerState { std::shared_ptr game_server; ServerState(const char* config_filename, bool is_replay); + ServerState(const ServerState&) = delete; + ServerState(ServerState&&) = delete; + ServerState& operator=(const ServerState&) = delete; + ServerState& operator=(ServerState&&) = delete; + void init(); void add_client_to_available_lobby(std::shared_ptr c); void remove_client_from_lobby(std::shared_ptr c); diff --git a/tests/GC-Episode3Battle.test.txt b/tests/GC-Episode3Battle.test.txt index 935f4f3b..39e47a43 100644 --- a/tests/GC-Episode3Battle.test.txt +++ b/tests/GC-Episode3Battle.test.txt @@ -61,6 +61,88 @@ I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=04 f 0010 | 0E 89 2A 49 0A 03 02 00 30 45 53 33 00 00 00 00 | *I 0ES3 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=B7 flag=00) +0000 | CC 00 0C 05 00 00 00 00 00 00 00 00 00 00 00 00 | +0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +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 00 00 00 00 00 00 00 00 00 00 | +0110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +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 00 00 00 00 00 00 00 00 00 00 | +0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01C0 | 00 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 00 00 00 00 | +01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0200 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0210 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0220 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0230 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0240 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0250 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0260 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0270 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0280 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0290 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0300 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0310 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0320 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0330 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0340 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0350 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0360 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0370 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0380 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0390 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0400 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0410 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0420 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0430 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0440 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0450 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0460 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0470 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0480 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0490 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0500 | 00 00 00 00 00 00 00 00 00 00 00 00 | +I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=B7 flag=00) 0000 | B7 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0010 | 00 00 00 00 40 42 0F 00 40 42 0F 00 FF FF FF FF | I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=95 flag=00) @@ -2705,6 +2787,88 @@ I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=04 f 0000 | 04 00 2C 00 00 00 01 00 11 11 11 11 39 98 AC 82 | , 9 0010 | 0E 89 2A 49 0A 44 82 00 30 45 53 33 00 00 00 00 | *I D 0ES3 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | +I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=04 flag=00) +0000 | CC 00 0C 05 00 00 00 00 00 00 00 00 00 00 00 00 | +0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +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 00 00 00 00 00 00 00 00 00 00 | +0110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +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 00 00 00 00 00 00 00 00 00 00 | +0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01C0 | 00 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 00 00 00 00 | +01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0200 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0210 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0220 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0230 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0240 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0250 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0260 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0270 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0280 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0290 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +02F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0300 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0310 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0320 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0330 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0340 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0350 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0360 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0370 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0380 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0390 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +03F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0400 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0410 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0420 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0430 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0440 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0450 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0460 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0470 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0480 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0490 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +04F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0500 | 00 00 00 00 00 00 00 00 00 00 00 00 | I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=83 flag=14) 0000 | 83 14 F4 00 33 00 00 33 01 00 00 00 00 00 00 00 | 3 3 0010 | 33 00 00 33 02 00 00 00 00 00 00 00 33 00 00 33 | 3 3 3 3