persist tournament state across server restarts

This commit is contained in:
Martin Michelsen
2022-12-10 00:13:49 -08:00
parent b0a32600be
commit fb4aa0df22
9 changed files with 315 additions and 33 deletions
+1 -4
View File
@@ -42,7 +42,6 @@ Current known issues / missing features / things to do:
- Encapsulate BB server-side random state and make replays deterministic. - Encapsulate BB server-side random state and make replays deterministic.
- The internal menu abstraction is ugly and hard to work with. Rewrite it. - The internal menu abstraction is ugly and hard to work with. Rewrite it.
- Add default values for all commands (like we use for Episode 3 battle commands). - Add default values for all commands (like we use for Episode 3 battle commands).
- Implement a way to persist tournament state across server restarts.
## Compatibility ## Compatibility
@@ -85,11 +84,9 @@ The following Episode 3 features are implemented, but only partially tested:
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament, but this doesn't schedule any matches. Instead, players who are scheduled for a match can all stand at a battle table in a CARD lobby, and the tournament match will start automatically. (This also means that, for example, not all matches in round 1 must be complete before round 2 can begin - only the matches preceding each individual match must be complete for that match to be playable.) Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament, but this doesn't schedule any matches. Instead, players who are scheduled for a match can all stand at a battle table in a CARD lobby, and the tournament match will start automatically. (This also means that, for example, not all matches in round 1 must be complete before round 2 can begin - only the matches preceding each individual match must be complete for that match to be playable.)
Note that tournament state is not persisted when the server restarts, so all tournaments will be in that case. This will be fixed in the future.
Because newserv gives all players 1000000 meseta, there is no reward for winning a tournament. This may change in the future. Because newserv gives all players 1000000 meseta, there is no reward for winning a tournament. This may change in the future.
COM decks for tournaments are defined in system/ep3/com-decks.json. The default decks in that file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time. COM decks for tournaments are defined in system/ep3/com-decks.json. The default decks in that file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time. Tournament state is stored in system/ep3/tournament-state.json; this file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
## Usage ## Usage
+42
View File
@@ -792,6 +792,41 @@ Rules::Rules() {
this->clear(); this->clear();
} }
Rules::Rules(shared_ptr<const JSONObject> json) {
auto dict = json->as_dict();
this->overall_time_limit = dict.at("overall_time_limit")->as_int();
this->phase_time_limit = dict.at("phase_time_limit")->as_int();
this->allowed_cards = static_cast<AllowedCards>(dict.at("allowed_cards")->as_int());
this->min_dice = dict.at("min_dice")->as_int();
this->max_dice = dict.at("max_dice")->as_int();
this->disable_deck_shuffle = dict.at("disable_deck_shuffle")->as_int();
this->disable_deck_loop = dict.at("disable_deck_loop")->as_int();
this->char_hp = dict.at("char_hp")->as_int();
this->hp_type = static_cast<HPType>(dict.at("hp_type")->as_int());
this->no_assist_cards = dict.at("no_assist_cards")->as_int();
this->disable_dialogue = dict.at("disable_dialogue")->as_int();
this->dice_exchange_mode = static_cast<DiceExchangeMode>(dict.at("dice_exchange_mode")->as_int());
this->disable_dice_boost = dict.at("disable_dice_boost")->as_int();
}
shared_ptr<JSONObject> Rules::json() const {
unordered_map<string, shared_ptr<JSONObject>> dict;
dict.emplace("overall_time_limit", make_json_int(this->overall_time_limit));
dict.emplace("phase_time_limit", make_json_int(this->phase_time_limit));
dict.emplace("allowed_cards", make_json_int(static_cast<uint8_t>(this->allowed_cards)));
dict.emplace("min_dice", make_json_int(this->min_dice));
dict.emplace("max_dice", make_json_int(this->max_dice));
dict.emplace("disable_deck_shuffle", make_json_int(this->disable_deck_shuffle));
dict.emplace("disable_deck_loop", make_json_int(this->disable_deck_loop));
dict.emplace("char_hp", make_json_int(this->char_hp));
dict.emplace("hp_type", make_json_int(static_cast<uint8_t>(this->hp_type)));
dict.emplace("no_assist_cards", make_json_int(this->no_assist_cards));
dict.emplace("disable_dialogue", make_json_int(this->disable_dialogue));
dict.emplace("dice_exchange_mode", make_json_int(static_cast<uint8_t>(this->dice_exchange_mode)));
dict.emplace("disable_dice_boost", make_json_int(this->disable_dice_boost));
return shared_ptr<JSONObject>(new JSONObject(move(dict)));
}
void Rules::set_defaults() { void Rules::set_defaults() {
this->clear(); this->clear();
this->overall_time_limit = 24; // 2 hours this->overall_time_limit = 24; // 2 hours
@@ -1364,6 +1399,9 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
for (size_t z = 0; z < 0x1F; z++) { for (size_t z = 0; z < 0x1F; z++) {
def->card_ids[z] = card_ids_json.at(z)->as_int(); def->card_ids[z] = card_ids_json.at(z)->as_int();
} }
if (!this->com_decks_by_name.emplace(def->deck_name, def).second) {
throw runtime_error("duplicate COM deck name: " + def->deck_name);
}
} }
} catch (const exception& e) { } catch (const exception& e) {
static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what()); static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what());
@@ -1498,6 +1536,10 @@ shared_ptr<const COMDeckDefinition> DataIndex::com_deck(size_t which) const {
return this->com_decks.at(which); return this->com_decks.at(which);
} }
shared_ptr<const COMDeckDefinition> DataIndex::com_deck(const string& which) const {
return this->com_decks_by_name.at(which);
}
shared_ptr<const COMDeckDefinition> DataIndex::random_com_deck() const { shared_ptr<const COMDeckDefinition> DataIndex::random_com_deck() const {
return this->com_decks[random_object<size_t>() % this->com_decks.size()]; return this->com_decks[random_object<size_t>() % this->com_decks.size()];
} }
+5
View File
@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
#include <phosg/Encoding.hh> #include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include "../Text.hh" #include "../Text.hh"
@@ -639,6 +640,8 @@ struct Rules {
parray<uint8_t, 3> unused; parray<uint8_t, 3> unused;
Rules(); Rules();
explicit Rules(std::shared_ptr<const JSONObject> json);
std::shared_ptr<JSONObject> json() const;
bool operator==(const Rules& other) const; bool operator==(const Rules& other) const;
bool operator!=(const Rules& other) const; bool operator!=(const Rules& other) const;
void clear(); void clear();
@@ -838,6 +841,7 @@ public:
size_t num_com_decks() const; size_t num_com_decks() const;
std::shared_ptr<const COMDeckDefinition> com_deck(size_t which) const; std::shared_ptr<const COMDeckDefinition> com_deck(size_t which) const;
std::shared_ptr<const COMDeckDefinition> com_deck(const std::string& name) const;
std::shared_ptr<const COMDeckDefinition> random_com_deck() const; std::shared_ptr<const COMDeckDefinition> random_com_deck() const;
const uint32_t behavior_flags; const uint32_t behavior_flags;
@@ -856,6 +860,7 @@ private:
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name; std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
std::vector<std::shared_ptr<COMDeckDefinition>> com_decks; std::vector<std::shared_ptr<COMDeckDefinition>> com_decks;
std::unordered_map<std::string, std::shared_ptr<COMDeckDefinition>> com_decks_by_name;
}; };
+206 -15
View File
@@ -179,7 +179,7 @@ void Tournament::Match::on_winner_team_set() {
} }
} }
void Tournament::Match::set_winner_team(shared_ptr<Team> team) { void Tournament::Match::set_winner_team_without_triggers(shared_ptr<Team> team) {
if (!this->preceding_a || !this->preceding_b) { if (!this->preceding_a || !this->preceding_b) {
throw logic_error("set_winner_team called on zero-round match"); throw logic_error("set_winner_team called on zero-round match");
} }
@@ -196,7 +196,10 @@ void Tournament::Match::set_winner_team(shared_ptr<Team> team) {
} else { } else {
this->preceding_a->winner_team->is_active = false; this->preceding_a->winner_team->is_active = false;
} }
}
void Tournament::Match::set_winner_team(shared_ptr<Team> team) {
this->set_winner_team_without_triggers(team);
this->on_winner_team_set(); this->on_winner_team_set();
} }
@@ -244,20 +247,65 @@ Tournament::Tournament(
} }
} }
Tournament::Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
std::shared_ptr<const JSONObject> json)
: log(string_printf("[Tournament/%02hhX] ", number)),
data_index(data_index),
source_json(json),
number(number),
current_state(State::REGISTRATION) { }
void Tournament::init() { void Tournament::init() {
// Create all the teams and initial matches vector<size_t> team_index_to_rounds_cleared;
while (this->teams.size() < this->num_teams) {
auto t = make_shared<Team>( bool is_registration_complete;
this->shared_from_this(), this->teams.size(), this->is_2v2 ? 2 : 1); if (this->source_json) {
this->teams.emplace_back(t); auto& dict = this->source_json->as_dict();
this->zero_round_matches.emplace_back(make_shared<Match>( this->name = dict.at("name")->as_string();
this->shared_from_this(), t)); this->map = this->data_index->definition_for_map_number(dict.at("map_number")->as_int());
this->rules = Rules(dict.at("rules"));
this->is_2v2 = dict.at("is_2v2")->as_bool();
is_registration_complete = dict.at("is_registration_complete")->as_bool();
for (const auto& team_json : dict.at("teams")->as_list()) {
auto& team_dict = team_json->as_dict();
auto& team = this->teams.emplace_back(new Team(
this->shared_from_this(),
this->teams.size(),
team_dict.at("max_players")->as_int()));
team->name = team_dict.at("name")->as_string();
team->password = team_dict.at("password")->as_string();
team_index_to_rounds_cleared.emplace_back(team_dict.at("num_rounds_cleared")->as_int());
for (const auto& serial_number_json : team_dict.at("player_serial_numbers")->as_list()) {
uint32_t serial_number = serial_number_json->as_int();
team->player_serial_numbers.emplace(serial_number);
this->all_player_serial_numbers.emplace(serial_number);
}
for (const auto& com_deck_name_json : team_dict.at("com_deck_names")->as_list()) {
team->com_decks.emplace_back(this->data_index->com_deck(
com_deck_name_json->as_string()));
}
}
this->num_teams = this->teams.size();
this->source_json.reset();
} else {
// Create empty teams
while (this->teams.size() < this->num_teams) {
auto t = make_shared<Team>(
this->shared_from_this(), this->teams.size(), this->is_2v2 ? 2 : 1);
this->teams.emplace_back(t);
}
is_registration_complete = false;
} }
// Make all the zero round matches pending (this is needed so that start() // Create the match structure
// will auto-resolve all-CPU matches in the first round) while (this->zero_round_matches.size() < this->num_teams) {
for (auto m : this->zero_round_matches) { this->zero_round_matches.emplace_back(make_shared<Match>(
this->pending_matches.emplace(m); this->shared_from_this(), this->teams[this->zero_round_matches.size()]));
} }
// Create the bracket matches // Create the bracket matches
@@ -276,6 +324,109 @@ void Tournament::init() {
current_round_matches = move(next_round_matches); current_round_matches = move(next_round_matches);
} }
this->final_match = current_round_matches.at(0); 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;
// Start with all first-round matches in the match queue
unordered_set<shared_ptr<Match>> match_queue;
for (auto match : this->zero_round_matches) {
match_queue.emplace(match->following.lock());
}
if (match_queue.count(nullptr)) {
throw logic_error("null match in match queue");
}
// For each match in the queue, either resolve it from the previous state or
// mark it as unresolvable (hence it should be pending when we're done)
while (!match_queue.empty()) {
auto match_it = match_queue.begin();
auto match = *match_it;
match_queue.erase(match_it);
if (!match->preceding_a->winner_team || !match->preceding_b->winner_team) {
throw logic_error("preceding matches are not resolved");
}
size_t& a_rounds_cleared = team_index_to_rounds_cleared[
match->preceding_a->winner_team->index];
size_t& b_rounds_cleared = team_index_to_rounds_cleared[
match->preceding_b->winner_team->index];
if (a_rounds_cleared && b_rounds_cleared) {
throw runtime_error("both teams won the same match");
}
if (!a_rounds_cleared && !b_rounds_cleared) {
this->pending_matches.emplace(match); // Neither team has won yet
} else {
if (a_rounds_cleared) {
a_rounds_cleared--;
match->set_winner_team_without_triggers(match->preceding_a->winner_team);
} else {
b_rounds_cleared--;
match->set_winner_team_without_triggers(match->preceding_b->winner_team);
}
// If both preceding matches of the following match are resolved, put
// the following match on the queue since it may be resolvable as well
auto following = match->following.lock();
if (following &&
following->preceding_a->winner_team &&
following->preceding_b->winner_team) {
match_queue.emplace(following);
}
}
}
if (!this->final_match->winner_team == this->pending_matches.empty()) {
throw logic_error("there must be pending matches if and only if the final match is not resolved");
}
// If all matches are resolved, then the tournament is complete
if (this->final_match->winner_team) {
this->current_state = State::COMPLETE;
}
} else {
// Make all the zero round matches pending (this is needed so that start()
// will auto-resolve all-CPU matches in the first round)
for (auto m : this->zero_round_matches) {
this->pending_matches.emplace(m);
}
this->current_state = State::REGISTRATION;
}
}
std::shared_ptr<JSONObject> Tournament::json() const {
unordered_map<string, shared_ptr<JSONObject>> dict;
dict.emplace("name", make_json_str(this->name));
dict.emplace("map_number", make_json_int(this->map->map.map_number));
dict.emplace("rules", this->rules.json());
dict.emplace("is_2v2", make_json_bool(this->is_2v2));
dict.emplace("is_registration_complete", make_json_bool(
this->current_state != State::REGISTRATION));
vector<shared_ptr<JSONObject>> teams_list;
for (auto team : this->teams) {
unordered_map<string, shared_ptr<JSONObject>> team_dict;
team_dict.emplace("max_players", make_json_int(team->max_players));
vector<shared_ptr<JSONObject>> player_serial_numbers_list;
for (uint32_t player_serial_number : team->player_serial_numbers) {
player_serial_numbers_list.emplace_back(make_json_int(player_serial_number));
}
team_dict.emplace("player_serial_numbers", make_json_list(move(player_serial_numbers_list)));
vector<shared_ptr<JSONObject>> com_deck_names_list;
for (auto com_deck : team->com_decks) {
com_deck_names_list.emplace_back(make_json_str(com_deck->deck_name));
}
team_dict.emplace("com_deck_names", make_json_list(move(com_deck_names_list)));
team_dict.emplace("name", make_json_str(team->name));
team_dict.emplace("password", make_json_str(team->password));
team_dict.emplace("num_rounds_cleared", make_json_int(team->num_rounds_cleared));
teams_list.emplace_back(new JSONObject(move(team_dict)));
}
dict.emplace("teams", make_json_list(move(teams_list)));
return shared_ptr<JSONObject>(new JSONObject(move(dict)));
} }
std::shared_ptr<const DataIndex> Tournament::get_data_index() const { std::shared_ptr<const DataIndex> Tournament::get_data_index() const {
@@ -382,7 +533,7 @@ void Tournament::start() {
throw runtime_error("not enough COM decks to complete team"); throw runtime_error("not enough COM decks to complete team");
} }
while (t->player_serial_numbers.size() + t->com_decks.size() < t->max_players) { while (t->player_serial_numbers.size() + t->com_decks.size() < t->max_players) {
t->com_decks.emplace(this->data_index->random_com_deck()); t->com_decks.emplace_back(this->data_index->random_com_deck());
} }
} }
@@ -438,6 +589,46 @@ void Tournament::print_bracket(FILE* stream) const {
TournamentIndex::TournamentIndex(
shared_ptr<const DataIndex> data_index,
const string& state_filename,
bool skip_load_state)
: data_index(data_index), state_filename(state_filename) {
if (this->state_filename.empty() || skip_load_state) {
return;
}
auto json = JSONObject::parse(load_file(this->state_filename));
auto& list = json->as_list();
if (list.size() != 0x20) {
throw runtime_error("tournament JSON list length is incorrect");
}
for (size_t z = 0; z < 0x20; z++) {
if (!list.at(z)->is_null()) {
this->tournaments[z].reset(new Tournament(this->data_index, z, list[z]));
this->tournaments[z]->init();
}
}
}
void TournamentIndex::save() const {
if (this->state_filename.empty()) {
return;
}
vector<shared_ptr<JSONObject>> list;
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z]) {
list.emplace_back(this->tournaments[z]->json());
} else {
list.emplace_back(make_json_null());
}
}
auto json = make_json_list(move(list));
save_file(this->state_filename, json->format());
}
vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const { vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
vector<shared_ptr<Tournament>> ret; vector<shared_ptr<Tournament>> ret;
for (size_t z = 0; z < 0x20; z++) { for (size_t z = 0; z < 0x20; z++) {
@@ -449,7 +640,6 @@ vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
} }
shared_ptr<Tournament> TournamentIndex::create_tournament( shared_ptr<Tournament> TournamentIndex::create_tournament(
shared_ptr<const DataIndex> data_index,
const string& name, const string& name,
shared_ptr<const DataIndex::MapEntry> map, shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules, const Rules& rules,
@@ -466,7 +656,8 @@ shared_ptr<Tournament> TournamentIndex::create_tournament(
throw runtime_error("all tournament slots are full"); throw runtime_error("all tournament slots are full");
} }
auto t = make_shared<Tournament>(data_index, number, name, map, rules, num_teams, is_2v2); auto t = make_shared<Tournament>(
this->data_index, number, name, map, rules, num_teams, is_2v2);
t->init(); t->init();
this->tournaments[number] = t; this->tournaments[number] = t;
return t; return t;
+18 -10
View File
@@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include <unordered_set> #include <unordered_set>
#include <string> #include <string>
#include <phosg/JSON.hh>
#include <phosg/Strings.hh> #include <phosg/Strings.hh>
#include "../Player.hh" #include "../Player.hh"
@@ -19,9 +20,6 @@ namespace Episode3 {
// TODO: We should build a way to save tournament state to a file, so it can
// persist across server restarts.
class Tournament : public std::enable_shared_from_this<Tournament> { class Tournament : public std::enable_shared_from_this<Tournament> {
public: public:
enum class State { enum class State {
@@ -35,7 +33,7 @@ public:
size_t index; size_t index;
size_t max_players; size_t max_players;
std::set<uint32_t> player_serial_numbers; std::set<uint32_t> player_serial_numbers;
std::set<std::shared_ptr<const COMDeckDefinition>> com_decks; std::vector<std::shared_ptr<const COMDeckDefinition>> com_decks;
std::string name; std::string name;
std::string password; std::string password;
size_t num_rounds_cleared; size_t num_rounds_cleared;
@@ -55,10 +53,6 @@ public:
}; };
struct Match : public std::enable_shared_from_this<Match> { struct Match : public std::enable_shared_from_this<Match> {
enum class WinnerTeam {
A = 0,
B = 1,
};
std::weak_ptr<Tournament> tournament; std::weak_ptr<Tournament> tournament;
std::shared_ptr<Match> preceding_a; std::shared_ptr<Match> preceding_a;
std::shared_ptr<Match> preceding_b; std::shared_ptr<Match> preceding_b;
@@ -78,6 +72,7 @@ public:
bool resolve_if_no_players(); bool resolve_if_no_players();
void on_winner_team_set(); void on_winner_team_set();
void set_winner_team(std::shared_ptr<Team> team); void set_winner_team(std::shared_ptr<Team> team);
void set_winner_team_without_triggers(std::shared_ptr<Team> team);
std::shared_ptr<Team> opponent_team_for_team(std::shared_ptr<Team> team) const; std::shared_ptr<Team> opponent_team_for_team(std::shared_ptr<Team> team) const;
}; };
@@ -89,9 +84,15 @@ public:
const Rules& rules, const Rules& rules,
size_t num_teams, size_t num_teams,
bool is_2v2); bool is_2v2);
Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
std::shared_ptr<const JSONObject> json);
~Tournament() = default; ~Tournament() = default;
void init(); void init();
std::shared_ptr<JSONObject> json() const;
std::shared_ptr<const DataIndex> get_data_index() const; std::shared_ptr<const DataIndex> get_data_index() const;
uint8_t get_number() const; uint8_t get_number() const;
const std::string& get_name() const; const std::string& get_name() const;
@@ -115,6 +116,7 @@ private:
PrefixedLogger log; PrefixedLogger log;
std::shared_ptr<const DataIndex> data_index; std::shared_ptr<const DataIndex> data_index;
std::shared_ptr<const JSONObject> source_json;
uint8_t number; uint8_t number;
std::string name; std::string name;
std::shared_ptr<const DataIndex::MapEntry> map; std::shared_ptr<const DataIndex::MapEntry> map;
@@ -141,13 +143,17 @@ private:
class TournamentIndex { class TournamentIndex {
public: public:
TournamentIndex() = default; explicit TournamentIndex(
std::shared_ptr<const DataIndex> data_index,
const std::string& state_filename,
bool skip_load_state = false);
~TournamentIndex() = default; ~TournamentIndex() = default;
void save() const;
std::vector<std::shared_ptr<Tournament>> all_tournaments() const; std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
std::shared_ptr<Tournament> create_tournament( std::shared_ptr<Tournament> create_tournament(
std::shared_ptr<const DataIndex> data_index,
const std::string& name, const std::string& name,
std::shared_ptr<const DataIndex::MapEntry> map, std::shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules, const Rules& rules,
@@ -161,6 +167,8 @@ public:
uint32_t serial_number) const; uint32_t serial_number) const;
private: private:
std::shared_ptr<const DataIndex> data_index;
std::string state_filename;
std::shared_ptr<Tournament> tournaments[0x20]; std::shared_ptr<Tournament> tournaments[0x20];
}; };
+11
View File
@@ -914,6 +914,17 @@ int main(int argc, char** argv) {
state->ep3_data_index.reset(new Episode3::DataIndex( state->ep3_data_index.reset(new Episode3::DataIndex(
"system/ep3", state->ep3_behavior_flags)); "system/ep3", state->ep3_behavior_flags));
const string& tournament_state_filename = "system/ep3/tournament-state.json";
try {
state->ep3_tournament_index.reset(new Episode3::TournamentIndex(
state->ep3_data_index, tournament_state_filename));
config_log.info("Loaded Episode 3 tournament state");
} catch (const exception& e) {
config_log.warning("Cannot load Episode 3 tournament state: %s", e.what());
state->ep3_tournament_index.reset(new Episode3::TournamentIndex(
state->ep3_data_index, tournament_state_filename, true));
}
config_log.info("Collecting quest metadata"); config_log.info("Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests")); state->quest_index.reset(new QuestIndex("system/quests"));
+4
View File
@@ -1120,6 +1120,7 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
if (tourn && (tourn->get_state() == Episode3::Tournament::State::COMPLETE)) { if (tourn && (tourn->get_state() == Episode3::Tournament::State::COMPLETE)) {
s->ep3_tournament_index->delete_tournament(tourn->get_number()); s->ep3_tournament_index->delete_tournament(tourn->get_number());
} }
s->ep3_tournament_index->save();
} }
} }
@@ -1132,6 +1133,7 @@ static void on_tournament_complete(
send_ep3_text_message_printf(s, "$C6%s$C7\nwon the tournament\n$C6%s", team->name.c_str(), tourn->get_name().c_str()); 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_number());
s->ep3_tournament_index->save();
} }
static void on_ep3_tournament_control(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_ep3_tournament_control(shared_ptr<ServerState> s, shared_ptr<Client> c,
@@ -1891,6 +1893,8 @@ the lobby along with your partner (if any) and\n\
opponent(s).", tourn->get_name().c_str()); opponent(s).", tourn->get_name().c_str());
send_ep3_timed_message_box(c->channel, 240, message.c_str()); send_ep3_timed_message_box(c->channel, 240, message.c_str());
s->ep3_tournament_index->save();
} catch (const exception& e) { } catch (const exception& e) {
string message = string_printf("Cannot join team:\n%s", e.what()); string message = string_printf("Cannot join team:\n%s", e.what());
send_lobby_message_box(c, decode_sjis(message)); send_lobby_message_box(c, decode_sjis(message));
+28 -3
View File
@@ -129,10 +129,17 @@ Server commands:\n\
dialogue=ON/OFF: Enable/disable dialogue\n\ dialogue=ON/OFF: Enable/disable dialogue\n\
dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\ dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\
dice-boost=ON/OFF: Enable/disable dice boost\n\ dice-boost=ON/OFF: Enable/disable dice boost\n\
delete-tournament \"Tournament Name\"\n\
Delete a tournament. The quotes are required unless the tournament name\n\
contains no spaces.\n\
list-tournaments\n\
List the names and numbers of all existing tournaments.\n\
start-tournament \"Tournament Name\"\n\ start-tournament \"Tournament Name\"\n\
End registration for a tournament and allow matches to begin.\n\ End registration for a tournament and allow matches to begin. The quotes\n\
are required unless the tournament name contains no spaces.\n\
tournament-state \"Tournament Name\"\n\ tournament-state \"Tournament Name\"\n\
Print the current state of a tournament.\n\ Print the current state of a tournament. The quotes are required unless the\n\
tournament name contains no spaces.\n\
\n\ \n\
Proxy commands (these will only work when exactly one client is connected):\n\ Proxy commands (these will only work when exactly one client is connected):\n\
sc <data>\n\ sc <data>\n\
@@ -403,14 +410,32 @@ Proxy commands (these will only work when exactly one client is connected):\n\
fprintf(stderr, "warning: some rules were invalid and reset to defaults\n"); fprintf(stderr, "warning: some rules were invalid and reset to defaults\n");
} }
auto tourn = this->state->ep3_tournament_index->create_tournament( auto tourn = this->state->ep3_tournament_index->create_tournament(
this->state->ep3_data_index, name, map, rules, num_teams, is_2v2); name, map, rules, num_teams, is_2v2);
this->state->ep3_tournament_index->save();
fprintf(stderr, "created tournament %02hhX\n", tourn->get_number()); fprintf(stderr, "created tournament %02hhX\n", tourn->get_number());
} 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();
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());
}
} else if (command_name == "start-tournament") { } else if (command_name == "start-tournament") {
string name = get_quoted_string(command_args); string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name); auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) { if (tourn) {
tourn->start(); tourn->start();
this->state->ep3_tournament_index->save();
send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str()); 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"); fprintf(stderr, "tournament started\n");
} else { } else {
-1
View File
@@ -26,7 +26,6 @@ ServerState::ServerState()
catch_handler_exceptions(true), catch_handler_exceptions(true),
ep3_behavior_flags(0), ep3_behavior_flags(0),
run_shell_behavior(RunShellBehavior::DEFAULT), run_shell_behavior(RunShellBehavior::DEFAULT),
ep3_tournament_index(new Episode3::TournamentIndex()),
ep3_card_auction_points(0), ep3_card_auction_points(0),
ep3_card_auction_min_size(0), ep3_card_auction_min_size(0),
ep3_card_auction_max_size(0), ep3_card_auction_max_size(0),