persist tournament state across server restarts
This commit is contained in:
@@ -42,7 +42,6 @@ Current known issues / missing features / things to do:
|
||||
- Encapsulate BB server-side random state and make replays deterministic.
|
||||
- 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).
|
||||
- Implement a way to persist tournament state across server restarts.
|
||||
|
||||
## 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.)
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -792,6 +792,41 @@ Rules::Rules() {
|
||||
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() {
|
||||
this->clear();
|
||||
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++) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
return this->com_decks[random_object<size_t>() % this->com_decks.size()];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/JSON.hh>
|
||||
|
||||
#include "../Text.hh"
|
||||
|
||||
@@ -639,6 +640,8 @@ struct Rules {
|
||||
parray<uint8_t, 3> unused;
|
||||
|
||||
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;
|
||||
void clear();
|
||||
@@ -838,6 +841,7 @@ public:
|
||||
|
||||
size_t num_com_decks() 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;
|
||||
|
||||
const uint32_t behavior_flags;
|
||||
@@ -856,6 +860,7 @@ private:
|
||||
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
|
||||
|
||||
std::vector<std::shared_ptr<COMDeckDefinition>> com_decks;
|
||||
std::unordered_map<std::string, std::shared_ptr<COMDeckDefinition>> com_decks_by_name;
|
||||
};
|
||||
|
||||
|
||||
|
||||
+206
-15
@@ -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) {
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
// Create all the teams and initial matches
|
||||
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);
|
||||
this->zero_round_matches.emplace_back(make_shared<Match>(
|
||||
this->shared_from_this(), t));
|
||||
vector<size_t> team_index_to_rounds_cleared;
|
||||
|
||||
bool is_registration_complete;
|
||||
if (this->source_json) {
|
||||
auto& dict = this->source_json->as_dict();
|
||||
this->name = dict.at("name")->as_string();
|
||||
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()
|
||||
// will auto-resolve all-CPU matches in the first round)
|
||||
for (auto m : this->zero_round_matches) {
|
||||
this->pending_matches.emplace(m);
|
||||
// Create the match structure
|
||||
while (this->zero_round_matches.size() < this->num_teams) {
|
||||
this->zero_round_matches.emplace_back(make_shared<Match>(
|
||||
this->shared_from_this(), this->teams[this->zero_round_matches.size()]));
|
||||
}
|
||||
|
||||
// Create the bracket matches
|
||||
@@ -276,6 +324,109 @@ void Tournament::init() {
|
||||
current_round_matches = 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;
|
||||
|
||||
// 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 {
|
||||
@@ -382,7 +533,7 @@ void Tournament::start() {
|
||||
throw runtime_error("not enough COM decks to complete team");
|
||||
}
|
||||
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>> ret;
|
||||
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<const DataIndex> data_index,
|
||||
const string& name,
|
||||
shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
@@ -466,7 +656,8 @@ shared_ptr<Tournament> TournamentIndex::create_tournament(
|
||||
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();
|
||||
this->tournaments[number] = t;
|
||||
return t;
|
||||
|
||||
+18
-10
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <phosg/Strings.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> {
|
||||
public:
|
||||
enum class State {
|
||||
@@ -35,7 +33,7 @@ public:
|
||||
size_t index;
|
||||
size_t max_players;
|
||||
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 password;
|
||||
size_t num_rounds_cleared;
|
||||
@@ -55,10 +53,6 @@ public:
|
||||
};
|
||||
|
||||
struct Match : public std::enable_shared_from_this<Match> {
|
||||
enum class WinnerTeam {
|
||||
A = 0,
|
||||
B = 1,
|
||||
};
|
||||
std::weak_ptr<Tournament> tournament;
|
||||
std::shared_ptr<Match> preceding_a;
|
||||
std::shared_ptr<Match> preceding_b;
|
||||
@@ -78,6 +72,7 @@ public:
|
||||
bool resolve_if_no_players();
|
||||
void on_winner_team_set();
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -89,9 +84,15 @@ public:
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
bool is_2v2);
|
||||
Tournament(
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint8_t number,
|
||||
std::shared_ptr<const JSONObject> json);
|
||||
~Tournament() = default;
|
||||
void init();
|
||||
|
||||
std::shared_ptr<JSONObject> json() const;
|
||||
|
||||
std::shared_ptr<const DataIndex> get_data_index() const;
|
||||
uint8_t get_number() const;
|
||||
const std::string& get_name() const;
|
||||
@@ -115,6 +116,7 @@ private:
|
||||
PrefixedLogger log;
|
||||
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
std::shared_ptr<const JSONObject> source_json;
|
||||
uint8_t number;
|
||||
std::string name;
|
||||
std::shared_ptr<const DataIndex::MapEntry> map;
|
||||
@@ -141,13 +143,17 @@ private:
|
||||
|
||||
class TournamentIndex {
|
||||
public:
|
||||
TournamentIndex() = default;
|
||||
explicit TournamentIndex(
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
const std::string& state_filename,
|
||||
bool skip_load_state = false);
|
||||
~TournamentIndex() = default;
|
||||
|
||||
void save() const;
|
||||
|
||||
std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
|
||||
|
||||
std::shared_ptr<Tournament> create_tournament(
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
const std::string& name,
|
||||
std::shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
@@ -161,6 +167,8 @@ public:
|
||||
uint32_t serial_number) const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
std::string state_filename;
|
||||
std::shared_ptr<Tournament> tournaments[0x20];
|
||||
};
|
||||
|
||||
|
||||
+11
@@ -914,6 +914,17 @@ int main(int argc, char** argv) {
|
||||
state->ep3_data_index.reset(new Episode3::DataIndex(
|
||||
"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");
|
||||
state->quest_index.reset(new QuestIndex("system/quests"));
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
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());
|
||||
}
|
||||
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,
|
||||
@@ -1891,6 +1893,8 @@ the lobby along with your partner (if any) and\n\
|
||||
opponent(s).", tourn->get_name().c_str());
|
||||
send_ep3_timed_message_box(c->channel, 240, message.c_str());
|
||||
|
||||
s->ep3_tournament_index->save();
|
||||
|
||||
} catch (const exception& e) {
|
||||
string message = string_printf("Cannot join team:\n%s", e.what());
|
||||
send_lobby_message_box(c, decode_sjis(message));
|
||||
|
||||
+28
-3
@@ -129,10 +129,17 @@ Server commands:\n\
|
||||
dialogue=ON/OFF: Enable/disable dialogue\n\
|
||||
dice-exchange=ATK/DEF/NONE: Set dice exchange mode\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\
|
||||
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\
|
||||
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\
|
||||
Proxy commands (these will only work when exactly one client is connected):\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");
|
||||
}
|
||||
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());
|
||||
|
||||
} 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") {
|
||||
string name = get_quoted_string(command_args);
|
||||
auto tourn = this->state->ep3_tournament_index->get_tournament(name);
|
||||
if (tourn) {
|
||||
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());
|
||||
fprintf(stderr, "tournament started\n");
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,6 @@ ServerState::ServerState()
|
||||
catch_handler_exceptions(true),
|
||||
ep3_behavior_flags(0),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT),
|
||||
ep3_tournament_index(new Episode3::TournamentIndex()),
|
||||
ep3_card_auction_points(0),
|
||||
ep3_card_auction_min_size(0),
|
||||
ep3_card_auction_max_size(0),
|
||||
|
||||
Reference in New Issue
Block a user