keep tournament state consistent on clients

This commit is contained in:
Martin Michelsen
2023-09-15 22:09:31 -07:00
parent 5caa21bccb
commit 1d45c18ce8
11 changed files with 404 additions and 161 deletions
+158 -89
View File
@@ -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<Client> c)
: serial_number(c->license->serial_number),
client(c) {}
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
@@ -57,7 +60,7 @@ string Tournament::Team::str() const {
}
void Tournament::Team::register_player(
uint32_t serial_number,
shared_ptr<Client> 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::Team> Tournament::Match::opponent_team_for_team(
Tournament::Tournament(
shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const string& name,
shared_ptr<const MapIndex::MapEntry> 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<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> 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<shared_ptr<Match>> current_round_matches = this->zero_round_matches;
while (current_round_matches.size() > 1) {
vector<shared_ptr<Match>> next_round_matches;
for (size_t z = 0; z < current_round_matches.size(); z += 2) {
auto m = make_shared<Match>(
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<shared_ptr<Match>> current_round_matches = this->zero_round_matches;
while (current_round_matches.size() > 1) {
vector<shared_ptr<Match>> next_round_matches;
for (size_t z = 0; z < current_round_matches.size(); z += 2) {
auto m = make_shared<Match>(
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<shared_ptr<Match>> match_queue;
for (auto match : this->zero_round_matches) {
@@ -501,6 +501,9 @@ shared_ptr<Tournament::Team> 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<ServerState> 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<void(shared_ptr<Match>, size_t)> print_match = [&](shared_ptr<Match> 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<size_t>(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<size_t>(json.size(), 0x20); z++) {
if (!json.at(z).is_null()) {
shared_ptr<Tournament> 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<Tournament> 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<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
vector<shared_ptr<Tournament>> 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<Tournament> TournamentIndex::create_tournament(
@@ -711,48 +743,52 @@ shared_ptr<Tournament> 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<Tournament>(
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<Tournament> TournamentIndex::get_tournament(uint8_t number) const {
return this->tournaments[number];
}
shared_ptr<Tournament> 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<Tournament::Team> 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<Tournament::Team> 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<Tournament::Team> TournamentIndex::team_for_serial_number(
return nullptr;
}
void TournamentIndex::link_client(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s) {
for (const auto& c_it : s->channel_to_client) {
this->link_client(s, c_it.second);
}
}
} // namespace Episode3
+41 -15
View File
@@ -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<const COMDeckDefinition> com_deck;
// client is valid if serial_number is nonzero and the client is connected
std::weak_ptr<Client> client;
explicit PlayerEntry(uint32_t serial_number);
explicit PlayerEntry(std::shared_ptr<Client> c);
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> 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<Client> 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<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules,
@@ -104,16 +109,12 @@ public:
Tournament(
std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> 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<std::shared_ptr<Team>>& all_teams() const {
return this->teams;
}
std::shared_ptr<Team> get_team(size_t index) const {
inline std::shared_ptr<Team> 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<Team> get_winner_team() const;
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
@@ -147,6 +154,8 @@ public:
void start();
void send_all_state_updates(std::shared_ptr<ServerState> s) const;
void print_bracket(FILE* stream) const;
private:
@@ -155,7 +164,6 @@ private:
std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index;
JSON source_json;
uint8_t number;
std::string name;
std::shared_ptr<const MapIndex::MapEntry> 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<uint32_t> all_player_serial_numbers;
std::unordered_set<std::shared_ptr<Match>> pending_matches;
@@ -191,7 +200,23 @@ public:
void save() const;
std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
inline const std::unordered_map<std::string, std::shared_ptr<Tournament>>& all_tournaments() const {
return this->name_to_tournament;
}
inline std::shared_ptr<Tournament> 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<Tournament> 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<Tournament> 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<Tournament> get_tournament(uint8_t number) const;
std::shared_ptr<Tournament> get_tournament(const std::string& name) const;
bool delete_tournament(const std::string& name);
std::shared_ptr<Tournament::Team> team_for_serial_number(
uint32_t serial_number) const;
std::shared_ptr<Tournament::Team> team_for_serial_number(uint32_t serial_number) const;
void link_client(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void link_all_clients(std::shared_ptr<ServerState> s);
private:
std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index;
std::string state_filename;
std::shared_ptr<Tournament> tournaments[0x20];
std::unordered_map<std::string, std::shared_ptr<Tournament>> name_to_tournament;
std::vector<std::shared_ptr<Tournament>> menu_item_id_to_tournament;
};
} // namespace Episode3