implement episode 3 tournaments

This commit is contained in:
Martin Michelsen
2022-12-06 00:16:13 -08:00
parent 8c2ea48b80
commit 9a1ba56982
24 changed files with 1721 additions and 191 deletions
+1
View File
@@ -59,6 +59,7 @@ add_executable(newserv
src/Episode3/PlayerStateSubordinates.cc src/Episode3/PlayerStateSubordinates.cc
src/Episode3/RulerServer.cc src/Episode3/RulerServer.cc
src/Episode3/Server.cc src/Episode3/Server.cc
src/Episode3/Tournament.cc
src/FileContentsCache.cc src/FileContentsCache.cc
src/FunctionCompiler.cc src/FunctionCompiler.cc
src/GSLArchive.cc src/GSLArchive.cc
+21 -1
View File
@@ -63,11 +63,31 @@ newserv supports several versions of PSO. Specifically:
*Notes:* *Notes:*
1. *DC support has only been tested with the US versions of PSO DC. Other versions probably don't work, but will be easy to add. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.* 1. *DC support has only been tested with the US versions of PSO DC. Other versions probably don't work, but will be easy to add. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.*
2. *This version only supports the modem adapter, which Dolphin does not currently emulate, so it's difficult to test.* 2. *This version only supports the modem adapter, which Dolphin does not currently emulate, so it's difficult to test.*
3. *Episode 3 players can download quests, join lobbies, create and join games, trade cards, and participate in card auctions. CARD battles are implemented but are not well-tested. Spectator teams are partially implemented, but are entirely untested. Tournaments are not implemented.* 3. *See the following section about Episode 3 functionality.*
4. *newserv's implementation of PSOX is based on disassembly of the client executable; it has never been tested with a real client and most likely doesn't work.* 4. *newserv's implementation of PSOX is based on disassembly of the client executable; it has never been tested with a real client and most likely doesn't work.*
5. *Some basic features are not implemented in Blue Burst games, so the games are not very playable. A lot of work has to be done to get BB games to a playable state.* 5. *Some basic features are not implemented in Blue Burst games, so the games are not very playable. A lot of work has to be done to get BB games to a playable state.*
6. *Support for PSO Dreamcast Trial Edition is very incomplete and probably never will be complete. This is really just exploring a curiosity that sheds some light on early network engineering done by Sega, not an actual attempt at supporting this version of the game.* 6. *Support for PSO Dreamcast Trial Edition is very incomplete and probably never will be complete. This is really just exploring a curiosity that sheds some light on early network engineering done by Sega, not an actual attempt at supporting this version of the game.*
### Episode 3
The following Episode 3 features are well-tested and work normally:
* Downloading quests.
* Creating and joining games.
* Trading cards.
* Participating in card auctions. (The auction contents must be configured in config.json.)
The following Episode 3 features are implemented, but only partially tested:
* CARD battles. If you find a feature or card ability that doesn't work, please make a GitHub issue and describe the situation (including the attacking card(s), defending card(s), and ability that didn't work).
* Spectator teams are partially implemented, but are entirely untested.
* Battle replays sometimes cause the client to crash during the replay. Using the $playrec command is therefore not recommended.
* Tournaments.
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.)
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.
## Usage ## Usage
Currently newserv should build on macOS and Ubuntu. It will likely work on other Linux flavors too. It should work on Windows as well, but I haven't tested it recently - the build process could be very manual. Cygwin is likely the easiest Windows environment in which to build newserv. Currently newserv should build on macOS and Ubuntu. It will likely work on other Linux flavors too. It should work on Windows as well, but I haven't tested it recently - the build process could be very manual. Cygwin is likely the easiest Windows environment in which to build newserv.
+9
View File
@@ -78,6 +78,15 @@ Client::Client(
} }
} }
Client::~Client() {
if (!this->disconnect_hooks.empty()) {
this->log.warning("Disconnect hooks pending at client destruction time:");
for (const auto& it : this->disconnect_hooks) {
this->log.warning(" %s", it.first.c_str());
}
}
}
void Client::set_license(shared_ptr<const License> l) { void Client::set_license(shared_ptr<const License> l) {
this->license = l; this->license = l;
this->game_data.guild_card_number = this->license->serial_number; this->game_data.guild_card_number = this->license->serial_number;
+4
View File
@@ -15,6 +15,7 @@
#include "PSOProtocol.hh" #include "PSOProtocol.hh"
#include "Text.hh" #include "Text.hh"
#include "Episode3/BattleRecord.hh" #include "Episode3/BattleRecord.hh"
#include "Episode3/Tournament.hh"
@@ -96,6 +97,7 @@ struct Client {
bool should_send_to_proxy_server; bool should_send_to_proxy_server;
uint32_t proxy_destination_address; uint32_t proxy_destination_address;
uint16_t proxy_destination_port; uint16_t proxy_destination_port;
std::unordered_map<std::string, std::function<void()>> disconnect_hooks;
// Patch server // Patch server
std::vector<PatchFileChecksumRequest> patch_file_checksum_requests; std::vector<PatchFileChecksumRequest> patch_file_checksum_requests;
@@ -113,6 +115,7 @@ struct Client {
std::unique_ptr<struct event, void(*)(struct event*)> save_game_data_event; std::unique_ptr<struct event, void(*)(struct event*)> save_game_data_event;
int16_t card_battle_table_number; int16_t card_battle_table_number;
uint8_t card_battle_table_seat_number; uint8_t card_battle_table_seat_number;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
// Miscellaneous (used by chat commands) // Miscellaneous (used by chat commands)
uint32_t next_exp_value; // next EXP value to give uint32_t next_exp_value; // next EXP value to give
@@ -137,6 +140,7 @@ struct Client {
std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file; std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file;
Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior); Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior);
~Client();
inline GameVersion version() const { inline GameVersion version() const {
return this->channel.version; return this->channel.version;
+38 -32
View File
@@ -2318,8 +2318,8 @@ struct S_TournamentList_GC_Ep3_E0 {
struct Entry { struct Entry {
le_uint32_t menu_id = 0; le_uint32_t menu_id = 0;
le_uint32_t item_id = 0; le_uint32_t item_id = 0;
uint8_t unknown_a1; uint8_t unknown_a1 = 0;
uint8_t locked; // If nonzero, the lock icon appears in the menu uint8_t locked = 0; // If nonzero, the lock icon appears in the menu
// Values for the state field: // Values for the state field:
// 00 = Preparing // 00 = Preparing
// 01 = 1st Round // 01 = 1st Round
@@ -2336,8 +2336,8 @@ struct S_TournamentList_GC_Ep3_E0 {
// appear that are obviously not intended to appear in the tournament list, // appear that are obviously not intended to appear in the tournament list,
// like "View the board" and "Board: Write". (In fact, some of the strings // like "View the board" and "Board: Write". (In fact, some of the strings
// listed above may be unintended for this menu as well.) // listed above may be unintended for this menu as well.)
uint8_t state; uint8_t state = 0;
uint8_t unknown_a2; uint8_t unknown_a2 = 0;
le_uint32_t start_time = 0; // In seconds since Unix epoch le_uint32_t start_time = 0; // In seconds since Unix epoch
ptext<char, 0x20> name; ptext<char, 0x20> name;
le_uint16_t num_teams = 0; le_uint16_t num_teams = 0;
@@ -2349,13 +2349,13 @@ struct S_TournamentList_GC_Ep3_E0 {
// E0 (C->S): Request team and key config (BB) // E0 (C->S): Request team and key config (BB)
// E1 (S->C): Battle information (Episode 3) // E1 (S->C): Game information (Episode 3)
struct S_Unknown_GC_Ep3_E1 { struct S_GameInformation_GC_Ep3_E1 {
/* 0004 */ parray<uint8_t, 0x20> game_name; /* 0004 */ ptext<char, 0x20> game_name;
struct Entry { struct Entry {
ptext<char, 0x10> name; ptext<char, 0x10> name; // From disp.name
ptext<char, 0x20> description; ptext<char, 0x20> description; // Usually something like "FOmarl CLv30 J"
} __packed__; } __packed__;
/* 0024 */ parray<Entry, 4> entries; /* 0024 */ parray<Entry, 4> entries;
/* 00E4 */ parray<uint8_t, 0x20> unknown_a3; /* 00E4 */ parray<uint8_t, 0x20> unknown_a3;
@@ -2384,24 +2384,24 @@ struct S_Unknown_GC_Ep3_E1 {
// command. // command.
struct S_TournamentEntryList_GC_Ep3_E2 { struct S_TournamentEntryList_GC_Ep3_E2 {
le_uint16_t unknown_a1 = 0; le_uint16_t players_per_team = 0;
le_uint16_t unknown_a2 = 0; le_uint16_t unused = 0;
struct Entry { struct Entry {
le_uint32_t menu_id = 0; le_uint32_t menu_id = 0;
le_uint32_t item_id = 0; le_uint32_t item_id = 0;
uint8_t unknown_a1; uint8_t unknown_a1 = 0;
// If locked is nonzero, a lock icon appears next to this team and the // If locked is nonzero, a lock icon appears next to this team and the
// player is prompted for a password if they select this team. // player is prompted for a password if they select this team.
uint8_t locked; uint8_t locked = 0;
// State values: // State values:
// 00 = empty (team_name is ignored; entry is selectable) // 00 = empty (team_name is ignored; entry is selectable)
// 01 = present, joinable (team_name renders in white) // 01 = present, joinable (team_name renders in white)
// 02 = present, finalized (team_name renders in yellow) // 02 = present, finalized (team_name renders in yellow)
// If state is any other value, the entry renders as if its state were 02, // If state is any other value, the entry renders as if its state were 02,
// but cannot be selected at all (the menu cursor simply skips over it). // but cannot be selected at all (the menu cursor simply skips over it).
uint8_t state; uint8_t state = 0;
uint8_t unknown_a2; uint8_t unknown_a2 = 0;
ptext<char, 0x20> team_name; ptext<char, 0x20> name;
} __packed__; } __packed__;
parray<Entry, 0x20> entries; parray<Entry, 0x20> entries;
} __packed__; } __packed__;
@@ -5214,12 +5214,12 @@ struct G_Unknown_GC_Ep3_6xB5x3C {
parray<uint8_t, 3> unused; parray<uint8_t, 3> unused;
} __packed__; } __packed__;
// 6xB4x3D: Unknown // 6xB4x3D: Set tournament player decks
// TODO: Document this from Episode 3 client/server disassembly // This is sent before the counter sequence in a tournament game, to reserve the
// This may be tournament metadata. // player and COM slots and set the map number.
struct G_Unknown_GC_Ep3_6xB4x3D { struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0};
Episode3::Rules rules; Episode3::Rules rules;
parray<uint8_t, 4> unknown_a1; parray<uint8_t, 4> unknown_a1;
struct Entry { struct Entry {
@@ -5228,13 +5228,14 @@ struct G_Unknown_GC_Ep3_6xB4x3D {
ptext<char, 0x10> deck_name; // Seems to only be used for COM players ptext<char, 0x10> deck_name; // Seems to only be used for COM players
parray<uint8_t, 5> unknown_a1; parray<uint8_t, 5> unknown_a1;
parray<le_uint16_t, 0x1F> card_ids; parray<le_uint16_t, 0x1F> card_ids;
parray<uint8_t, 2> unused; uint8_t client_id = 0; // Unused for COMs
uint8_t unknown_a4 = 0;
le_uint16_t unknown_a2 = 0; le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0; le_uint16_t unknown_a3 = 0;
} __packed__; } __packed__;
parray<Entry, 4> entries; parray<Entry, 4> entries;
le_uint32_t map_number = 0; le_uint32_t map_number = 0;
uint8_t unknown_a2 = 0; uint8_t player_slot = 0; // Which slot is editable by the client
uint8_t unknown_a3 = 0; uint8_t unknown_a3 = 0;
uint8_t unknown_a4 = 0; uint8_t unknown_a4 = 0;
uint8_t unknown_a5 = 0; uint8_t unknown_a5 = 0;
@@ -5481,20 +5482,25 @@ struct G_SetTrapTileLocations_GC_Ep3_6xB4x50 {
parray<uint8_t, 2> unused; parray<uint8_t, 2> unused;
} __packed__; } __packed__;
// 6xB4x51: Tournament match info // 6xB4x51: Tournament match result
// This is sent as soon as the battle result is determined (before the battle
// results screen). If the client is in tournament mode (tournament_flag is 1 in
// the StateFlags struct), then it will use this information to show the
// tournament match result screen before the battle results screen.
struct G_TournamentMatchInfo_GC_Ep3_6xB4x51 { struct G_TournamentMatchResult_GC_Ep3_6xB4x51 {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchInfo_GC_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0}; G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchResult_GC_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0};
ptext<char, 0x40> match_description; ptext<char, 0x40> match_description;
struct Entry { struct NamesEntry {
ptext<char, 0x20> team_name; ptext<char, 0x20> team_name;
parray<ptext<char, 0x10>, 2> player_names; parray<ptext<char, 0x10>, 2> player_names;
} __packed__; } __packed__;
parray<Entry, 2> teams; parray<NamesEntry, 2> names_entries;
le_uint16_t unknown_a1 = 0; struct ResultEntry {
le_uint16_t unknown_a2 = 0; le_uint16_t num_players;
le_uint16_t unknown_a3 = 0; le_uint16_t is_winner_team;
le_uint16_t unknown_a4 = 0; } __packed__;
parray<ResultEntry, 2> result_entries;
le_uint32_t meseta_amount = 0; le_uint32_t meseta_amount = 0;
// This field apparently is supposed to contain a %s token (as for printf) // This field apparently is supposed to contain a %s token (as for printf)
// that is replaced with meseta_amount. // that is replaced with meseta_amount.
+44 -2
View File
@@ -5,6 +5,7 @@
#include <array> #include <array>
#include <deque> #include <deque>
#include <phosg/Filesystem.hh> #include <phosg/Filesystem.hh>
#include <phosg/Random.hh>
#include <phosg/Time.hh> #include <phosg/Time.hh>
#include "../Loggers.hh" #include "../Loggers.hh"
@@ -791,6 +792,15 @@ Rules::Rules() {
this->clear(); this->clear();
} }
void Rules::set_defaults() {
this->clear();
this->overall_time_limit = 24; // 2 hours
this->phase_time_limit = 30;
this->min_dice = 1;
this->max_dice = 6;
this->char_hp = 15;
}
void Rules::clear() { void Rules::clear() {
this->overall_time_limit = 0; this->overall_time_limit = 0;
this->phase_time_limit = 0; this->phase_time_limit = 0;
@@ -1083,8 +1093,6 @@ string MapDefinition::str(const DataIndex* data_index) const {
return join(lines, "\n"); return join(lines, "\n");
} }
bool Rules::check_invalid_fields() const { bool Rules::check_invalid_fields() const {
Rules t = *this; Rules t = *this;
return t.check_and_reset_invalid_fields(); return t.check_and_reset_invalid_fields();
@@ -1333,6 +1341,7 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
if (!this->maps.emplace(entry->map.map_number, entry).second) { if (!this->maps.emplace(entry->map.map_number, entry).second) {
throw runtime_error("duplicate map number"); throw runtime_error("duplicate map number");
} }
this->maps_by_name.emplace(entry->map.name, entry);
string name = entry->map.name; string name = entry->map.name;
static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)", static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
filename.c_str(), entry->map.map_number.load(), name.c_str()); filename.c_str(), entry->map.map_number.load(), name.c_str());
@@ -1343,6 +1352,22 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
filename.c_str(), e.what()); filename.c_str(), e.what());
} }
} }
try {
auto json = JSONObject::parse(load_file(directory + "/com-decks.json"));
for (const auto& def_json : json->as_list()) {
auto& def = this->com_decks.emplace_back(new COMDeckDefinition());
def->index = this->com_decks.size() - 1;
def->player_name = def_json->at(0)->as_string();
def->deck_name = def_json->at(1)->as_string();
auto card_ids_json = def_json->at(2)->as_list();
for (size_t z = 0; z < 0x1F; z++) {
def->card_ids[z] = card_ids_json.at(z)->as_int();
}
}
} catch (const exception& e) {
static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what());
}
} }
DataIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) { } DataIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) { }
@@ -1452,6 +1477,11 @@ shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_number(uint3
return this->maps.at(id); return this->maps.at(id);
} }
shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_name(
const string& name) const {
return this->maps_by_name.at(name);
}
set<uint32_t> DataIndex::all_map_ids() const { set<uint32_t> DataIndex::all_map_ids() const {
set<uint32_t> ret; set<uint32_t> ret;
for (const auto& it : this->maps) { for (const auto& it : this->maps) {
@@ -1460,6 +1490,18 @@ set<uint32_t> DataIndex::all_map_ids() const {
return ret; return ret;
} }
size_t DataIndex::num_com_decks() const {
return this->com_decks.size();
}
shared_ptr<const COMDeckDefinition> DataIndex::com_deck(size_t which) const {
return this->com_decks.at(which);
}
shared_ptr<const COMDeckDefinition> DataIndex::random_com_deck() const {
return this->com_decks[random_object<size_t>() % this->com_decks.size()];
}
} // namespace Episode3 } // namespace Episode3
+19
View File
@@ -642,6 +642,7 @@ struct Rules {
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();
void set_defaults();
bool check_invalid_fields() const; bool check_invalid_fields() const;
bool check_and_reset_invalid_fields(); bool check_and_reset_invalid_fields();
@@ -791,6 +792,15 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
struct COMDeckDefinition {
size_t index;
std::string player_name;
std::string deck_name;
parray<le_uint16_t, 0x1F> card_ids;
};
class DataIndex { class DataIndex {
public: public:
DataIndex(const std::string& directory, uint32_t behavior_flags); DataIndex(const std::string& directory, uint32_t behavior_flags);
@@ -822,8 +832,14 @@ public:
const std::string& get_compressed_map_list() const; const std::string& get_compressed_map_list() const;
std::shared_ptr<const MapEntry> definition_for_map_number(uint32_t id) const; std::shared_ptr<const MapEntry> definition_for_map_number(uint32_t id) const;
std::shared_ptr<const MapEntry> definition_for_map_name(
const std::string& name) const;
std::set<uint32_t> all_map_ids() const; std::set<uint32_t> all_map_ids() const;
size_t num_com_decks() const;
std::shared_ptr<const COMDeckDefinition> com_deck(size_t which) const;
std::shared_ptr<const COMDeckDefinition> random_com_deck() const;
const uint32_t behavior_flags; const uint32_t behavior_flags;
private: private:
@@ -837,6 +853,9 @@ private:
// compressed map list at load time. // compressed map list at load time.
mutable std::string compressed_map_list; mutable std::string compressed_map_list;
std::map<uint32_t, std::shared_ptr<MapEntry>> maps; std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
std::vector<std::shared_ptr<COMDeckDefinition>> com_decks;
}; };
+2 -2
View File
@@ -43,8 +43,8 @@ PlayerState::PlayerState(uint8_t client_id, shared_ptr<Server> server)
void PlayerState::init() { void PlayerState::init() {
if (this->server()->player_states[this->client_id].get() != this) { if (this->server()->player_states[this->client_id].get() != this) {
// TODO: The original code handles this, but we don't. Figure out if this is // Note: The original code handles this, but we don't. This appears not to
// actually needed and implement it if so. // ever happen, so we didn't bother implementing it.
throw logic_error("replacing a player state object is not permitted"); throw logic_error("replacing a player state object is not permitted");
} }
+60 -25
View File
@@ -34,10 +34,12 @@ void ServerBase::PresenceEntry::clear() {
ServerBase::ServerBase( ServerBase::ServerBase(
shared_ptr<Lobby> lobby, shared_ptr<Lobby> lobby,
shared_ptr<const DataIndex> data_index, shared_ptr<const DataIndex> data_index,
uint32_t random_seed) uint32_t random_seed,
bool is_tournament)
: lobby(lobby), : lobby(lobby),
data_index(data_index), data_index(data_index),
random_seed(random_seed) { } random_seed(random_seed),
is_tournament(is_tournament) { }
void ServerBase::init() { void ServerBase::init() {
this->reset(); this->reset();
@@ -96,7 +98,7 @@ Server::Server(shared_ptr<ServerBase> base)
team_num_ally_fcs_destroyed(0), team_num_ally_fcs_destroyed(0),
team_num_cards_destroyed(0), team_num_cards_destroyed(0),
hard_reset_flag(false), hard_reset_flag(false),
tournament_flag(0), tournament_flag(base->is_tournament ? 1 : 0),
num_trap_tiles_of_type(0), num_trap_tiles_of_type(0),
chosen_trap_tile_index_of_type(0), chosen_trap_tile_index_of_type(0),
has_done_pb(0), has_done_pb(0),
@@ -142,6 +144,39 @@ shared_ptr<const ServerBase> Server::base() const {
return s; return s;
} }
int8_t Server::get_winner_team_id() const {
parray<size_t, 2> team_player_counts(0);
parray<size_t, 2> team_win_flag_counts(0);
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
uint8_t team_id = ps->get_team_id();
team_player_counts[team_id]++;
if (ps->assist_flags & 4) {
team_win_flag_counts[team_id]++;
}
}
if (!team_player_counts[0] || !team_player_counts[1]) {
throw logic_error("at least one team has no players");
}
if (team_win_flag_counts[0] && team_win_flag_counts[1]) {
throw logic_error("both teams have winning players");
}
for (int8_t z = 0; z < 2; z++) {
if (!team_win_flag_counts[z]) {
continue;
}
if (team_win_flag_counts[z] != team_player_counts[z]) {
throw logic_error("only some players on team 0 have won");
}
return z;
}
return -1; // No team has won (yet)
}
void Server::send(const void* data, size_t size) const { void Server::send(const void* data, size_t size) const {
auto l = this->base()->lobby.lock(); auto l = this->base()->lobby.lock();
if (!l) { if (!l) {
@@ -418,7 +453,7 @@ bool Server::check_for_battle_end() {
} }
} else { // Both teams defeated?? I guess this is technically possible } else { // Both teams defeated?? I guess this is technically possible
ret = true; ret = true;
this->unknown_8023D4E0(0x4000); this->compute_losing_team_id_and_add_winner_flags(0x4000);
} }
} else { // Not DEFEAT_TEAM } else { // Not DEFEAT_TEAM
@@ -449,7 +484,7 @@ bool Server::check_for_battle_end() {
} }
} else { } else {
ret = true; ret = true;
this->unknown_8023D4E0(0x4000); this->compute_losing_team_id_and_add_winner_flags(0x4000);
} }
} }
@@ -649,7 +684,7 @@ void Server::draw_phase_after() {
} }
} }
if (unknown_v1) { if (unknown_v1) {
this->unknown_8023D4E0(0); this->compute_losing_team_id_and_add_winner_flags(0);
} }
this->round_num--; this->round_num--;
this->set_battle_ended(); this->set_battle_ended();
@@ -2136,7 +2171,7 @@ void Server::handle_6xB3x49_card_counts(const string& data) {
decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis); decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis);
} }
void Server::unknown_8023D4E0(uint32_t flags) { void Server::compute_losing_team_id_and_add_winner_flags(uint32_t flags) {
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z]; auto ps = this->player_states[z];
if (ps) { if (ps) {
@@ -2146,8 +2181,8 @@ void Server::unknown_8023D4E0(uint32_t flags) {
uint32_t flags_to_add = flags | 0x804; uint32_t flags_to_add = flags | 0x804;
// First, check which team has fewer surviving SCs // First, check which team has more dead SCs
int8_t team_id = -1; int8_t losing_team_id = -1;
uint32_t team_counts[2] = {0, 0}; uint32_t team_counts[2] = {0, 0};
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z]; auto ps = this->player_states[z];
@@ -2160,13 +2195,13 @@ void Server::unknown_8023D4E0(uint32_t flags) {
} }
} }
if (team_counts[1] < team_counts[0]) { if (team_counts[1] < team_counts[0]) {
team_id = 0; losing_team_id = 0;
} else if (team_counts[0] < team_counts[1]) { } else if (team_counts[0] < team_counts[1]) {
team_id = 1; losing_team_id = 1;
} }
// If the SC counts match, break ties by remaining SC HP // If the SC counts match, break ties by remaining SC HP
if (team_id == -1) { if (losing_team_id == -1) {
team_counts[0] = 0; team_counts[0] = 0;
team_counts[1] = 0; team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
@@ -2180,14 +2215,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
} }
} }
if (team_counts[0] < team_counts[1]) { if (team_counts[0] < team_counts[1]) {
team_id = 0; losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) { } else if (team_counts[1] < team_counts[0]) {
team_id = 1; losing_team_id = 1;
} }
} }
// If still tied, break ties by number of opponent cards destroyed // If still tied, break ties by number of opponent cards destroyed
if (team_id == -1) { if (losing_team_id == -1) {
team_counts[0] = 0; team_counts[0] = 0;
team_counts[1] = 0; team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
@@ -2198,14 +2233,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[ps->get_team_id()] += ps->stats.num_opponent_cards_destroyed; team_counts[ps->get_team_id()] += ps->stats.num_opponent_cards_destroyed;
} }
if (team_counts[0] < team_counts[1]) { if (team_counts[0] < team_counts[1]) {
team_id = 0; losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) { } else if (team_counts[1] < team_counts[0]) {
team_id = 1; losing_team_id = 1;
} }
} }
// If still tied, break ties by amount of damage given // If still tied, break ties by amount of damage given
if (team_id == -1) { if (losing_team_id == -1) {
team_counts[0] = 0; team_counts[0] = 0;
team_counts[1] = 0; team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
@@ -2216,15 +2251,15 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[ps->get_team_id()] += ps->stats.damage_given; team_counts[ps->get_team_id()] += ps->stats.damage_given;
} }
if (team_counts[0] < team_counts[1]) { if (team_counts[0] < team_counts[1]) {
team_id = 0; losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) { } else if (team_counts[1] < team_counts[0]) {
team_id = 1; losing_team_id = 1;
} }
} }
// If STILL tied, roll dice and arbitrarily make one team the winner // If STILL tied, roll dice and arbitrarily make one team the winner
if (team_id == -1) { if (losing_team_id == -1) {
while (team_id == -1) { while (losing_team_id == -1) {
team_counts[1] = 0; team_counts[1] = 0;
team_counts[0] = 0; team_counts[0] = 0;
for (size_t z = 0; z < 4; z++) { for (size_t z = 0; z < 4; z++) {
@@ -2237,9 +2272,9 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[0] *= this->team_client_count[1]; team_counts[0] *= this->team_client_count[1];
team_counts[1] *= this->team_client_count[0]; team_counts[1] *= this->team_client_count[0];
if (team_counts[0] < team_counts[1]) { if (team_counts[0] < team_counts[1]) {
team_id = 0; losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) { } else if (team_counts[1] < team_counts[0]) {
team_id = 1; losing_team_id = 1;
} }
} }
flags_to_add = flags | 0x1004; flags_to_add = flags | 0x1004;
@@ -2250,7 +2285,7 @@ void Server::unknown_8023D4E0(uint32_t flags) {
if (!ps) { if (!ps) {
continue; continue;
} }
if (team_id != ps->get_team_id()) { if (losing_team_id != ps->get_team_id()) {
ps->assist_flags |= flags_to_add; ps->assist_flags |= flags_to_add;
} }
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
+6 -2
View File
@@ -58,7 +58,8 @@ public:
ServerBase( ServerBase(
std::shared_ptr<Lobby> lobby, std::shared_ptr<Lobby> lobby,
std::shared_ptr<const DataIndex> data_index, std::shared_ptr<const DataIndex> data_index,
uint32_t random_seed); uint32_t random_seed,
bool is_tournament);
void init(); void init();
void reset(); void reset();
void recreate_server(); void recreate_server();
@@ -74,6 +75,7 @@ public:
std::weak_ptr<Lobby> lobby; std::weak_ptr<Lobby> lobby;
std::shared_ptr<const DataIndex> data_index; std::shared_ptr<const DataIndex> data_index;
uint32_t random_seed; uint32_t random_seed;
bool is_tournament;
std::shared_ptr<MapAndRulesState> map_and_rules1; std::shared_ptr<MapAndRulesState> map_and_rules1;
std::shared_ptr<MapAndRulesState> map_and_rules2; std::shared_ptr<MapAndRulesState> map_and_rules2;
@@ -94,6 +96,8 @@ public:
std::shared_ptr<ServerBase> base(); std::shared_ptr<ServerBase> base();
std::shared_ptr<const ServerBase> base() const; std::shared_ptr<const ServerBase> base() const;
int8_t get_winner_team_id() const;
template <typename T> template <typename T>
void send(const T& cmd) const { void send(const T& cmd) const {
if (cmd.header.size != sizeof(cmd) / 4) { if (cmd.header.size != sizeof(cmd) / 4) {
@@ -200,7 +204,7 @@ public:
void handle_6xB3x41_map_request(const std::string& data); void handle_6xB3x41_map_request(const std::string& data);
void handle_6xB3x48_end_turn(const std::string& data); void handle_6xB3x48_end_turn(const std::string& data);
void handle_6xB3x49_card_counts(const std::string& data); void handle_6xB3x49_card_counts(const std::string& data);
void unknown_8023D4E0(uint32_t flags); void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
uint32_t get_team_exp(uint8_t team_id) const; uint32_t get_team_exp(uint8_t team_id) const;
uint32_t send_6xB4x06_if_card_ref_invalid( uint32_t send_6xB4x06_if_card_ref_invalid(
uint16_t card_ref, int16_t negative_value); uint16_t card_ref, int16_t negative_value);
+449
View File
@@ -0,0 +1,449 @@
#include "Tournament.hh"
#include <phosg/Random.hh>
#include "../CommandFormats.hh"
#include "../SendCommands.hh"
using namespace std;
namespace Episode3 {
Tournament::Team::Team(
shared_ptr<Tournament> tournament, size_t index, size_t max_players)
: tournament(tournament),
index(index),
max_players(max_players),
name(""),
password(""),
num_rounds_cleared(0),
is_active(true) { }
string Tournament::Team::str() const {
string ret = string_printf("[Team/%zu %s %zu/%zuP name=%s pass=%s rounds=%zu",
this->index, this->is_active ? "active" : "inactive",
this->player_serial_numbers.size(), this->max_players, this->name.c_str(),
this->password.c_str(), this->num_rounds_cleared);
for (uint32_t serial_number : this->player_serial_numbers) {
ret += string_printf(" %08" PRIX32, serial_number);
}
return ret + "]";
}
void Tournament::Team::register_player(
uint32_t serial_number,
const string& team_name,
const string& password) {
if (this->player_serial_numbers.size() >= this->max_players) {
throw runtime_error("team is full");
}
if (!this->name.empty() && (password != this->password)) {
throw runtime_error("incorrect password");
}
auto tournament = this->tournament.lock();
if (!tournament) {
throw runtime_error("tournament has been deleted");
}
if (!tournament->all_player_serial_numbers.emplace(serial_number).second) {
throw runtime_error("player already registered in same tournament");
}
if (!this->player_serial_numbers.emplace(serial_number).second) {
throw logic_error("player already registered in team but not in tournament");
}
if (this->name.empty()) {
this->name = team_name;
this->password = password;
}
}
bool Tournament::Team::unregister_player(uint32_t serial_number) {
if (this->player_serial_numbers.erase(serial_number)) {
if (this->player_serial_numbers.empty()) {
this->name.clear();
this->password.clear();
}
auto tournament = this->tournament.lock();
if (!tournament) {
return false;
}
// If the tournament has already started, make the team forfeit their game.
// If any player withdraws from a team after the registration phase, the
// entire team essentially forfeits their entry.
if (tournament->get_state() != Tournament::State::REGISTRATION) {
// Look through the pending matches to see if this team is involved in any
// of them
for (auto match : tournament->pending_matches) {
if (!match->preceding_a || !match->preceding_b) {
throw logic_error("zero-round match is pending after tournament registration phase");
}
if (match->preceding_a->winner_team.get() == this) {
match->set_winner_team(match->preceding_b->winner_team);
break;
} else if (match->preceding_b->winner_team.get() == this) {
match->set_winner_team(match->preceding_a->winner_team);
break;
}
}
// If the tournament has not started yet, just remove the player from the
// team
} else {
if (!tournament->all_player_serial_numbers.erase(serial_number)) {
throw logic_error("player removed from team but not from tournament");
}
}
return true;
} else {
return false;
}
}
Tournament::Match::Match(
shared_ptr<Tournament> tournament,
shared_ptr<Match> preceding_a,
shared_ptr<Match> preceding_b)
: tournament(tournament),
preceding_a(preceding_a),
preceding_b(preceding_b),
winner_team(nullptr),
round_num(0) {
if (this->preceding_a->round_num != this->preceding_b->round_num) {
throw logic_error("preceding matches have different round numbers");
}
this->round_num = this->preceding_a->round_num;
}
Tournament::Match::Match(
shared_ptr<Tournament> tournament,
shared_ptr<Team> winner_team)
: tournament(tournament),
preceding_a(nullptr),
preceding_b(nullptr),
winner_team(winner_team),
round_num(0) { }
string Tournament::Match::str() const {
string winner_str = this->winner_team ? this->winner_team->str() : "(none)";
return "[Match winner=" + winner_str + "]";
}
bool Tournament::Match::resolve_if_no_players() {
// If both matches before this one are resolved and neither winner team has
// any humans on it, skip this match entirely and just make one team advance
// arbitrarily
if (!this->winner_team &&
this->preceding_a->winner_team &&
this->preceding_b->winner_team &&
this->preceding_a->winner_team->player_serial_numbers.empty() &&
this->preceding_b->winner_team->player_serial_numbers.empty()) {
this->set_winner_team((random_object<uint8_t>() & 1)
? this->preceding_b->winner_team : this->preceding_a->winner_team);
return true;
} else {
return false;
}
}
void Tournament::Match::resolve_following_matches() {
auto tournament = this->tournament.lock();
if (!tournament) {
return;
}
tournament->pending_matches.erase(this->shared_from_this());
// Resolve all matches up the chain until we can't anymore (this
// automatically skips CPU-only matches)
auto following = this->following.lock();
while (following && following->resolve_if_no_players()) {
tournament->pending_matches.erase(following);
following = following->following.lock();
}
// If there's a following match that wasn't resolved, mark it pending
if (following) {
tournament->pending_matches.emplace(following);
}
// If there are no pending matches, then the tournament is complete
if (tournament->pending_matches.empty()) {
tournament->current_state = Tournament::State::COMPLETE;
}
}
void Tournament::Match::set_winner_team(shared_ptr<Team> team) {
if (!this->preceding_a || !this->preceding_b) {
throw logic_error("set_winner_team called on zero-round match");
}
if ((team != this->preceding_a->winner_team) &&
(team != this->preceding_b->winner_team)) {
throw logic_error("winner team did not participate in match");
}
this->winner_team = team;
this->winner_team->num_rounds_cleared++;
if (this->winner_team == this->preceding_a->winner_team) {
this->preceding_b->winner_team->is_active = false;
} else {
this->preceding_a->winner_team->is_active = false;
}
this->resolve_following_matches();
}
shared_ptr<Tournament::Team> Tournament::Match::opponent_team_for_team(
shared_ptr<Team> team) const {
if (!this->preceding_a || !this->preceding_b) {
throw logic_error("zero-round matches do not have opponents");
}
if (team == this->preceding_a->winner_team) {
return this->preceding_b->winner_team;
} else if (team == this->preceding_b->winner_team) {
return this->preceding_a->winner_team;
} else {
throw logic_error("team is not registered for this match");
}
}
Tournament::Tournament(
shared_ptr<const DataIndex> data_index,
uint8_t number,
const string& name,
shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2)
: log(string_printf("[Tournament/%02hhX] ", number)),
data_index(data_index),
number(number),
name(name),
map(map),
rules(rules),
num_teams(num_teams),
is_2v2(is_2v2),
current_state(State::REGISTRATION) {
if (this->num_teams < 4) {
throw invalid_argument("team count must be 4 or more");
}
if (this->num_teams > 32) {
throw invalid_argument("team count must be 32 or fewer");
}
if (this->num_teams & (this->num_teams - 1)) {
throw invalid_argument("team count must be a power of 2");
}
}
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));
}
// 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 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(move(m));
}
current_round_matches = move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
}
std::shared_ptr<const DataIndex> Tournament::get_data_index() const {
return this->data_index;
}
uint8_t Tournament::get_number() const {
return this->number;
}
const string& Tournament::get_name() const {
return this->name;
}
shared_ptr<const DataIndex::MapEntry> Tournament::get_map() const {
return this->map;
}
const Rules& Tournament::get_rules() const {
return this->rules;
}
bool Tournament::get_is_2v2() const {
return this->is_2v2;
}
Tournament::State Tournament::get_state() const {
return this->current_state;
}
const vector<shared_ptr<Tournament::Team>>& Tournament::all_teams() const {
return this->teams;
}
shared_ptr<Tournament::Team> Tournament::get_team(size_t index) const {
return this->teams.at(index);
}
shared_ptr<Tournament::Team> Tournament::get_winner_team() const {
if (this->current_state != State::COMPLETE) {
return nullptr;
}
if (!this->final_match->winner_team) {
throw logic_error("tournament is complete but winner is not set");
}
return this->final_match->winner_team;
}
shared_ptr<Tournament::Match> Tournament::next_match_for_team(
shared_ptr<Team> team) const {
if (this->current_state == Tournament::State::REGISTRATION) {
return nullptr;
}
for (auto match : this->pending_matches) {
if (!match->preceding_a || !match->preceding_b) {
throw logic_error("zero-round match is pending after tournament registration phase");
}
if ((team == match->preceding_a->winner_team) ||
(team == match->preceding_b->winner_team)) {
return match;
}
}
return nullptr;
}
void Tournament::start() {
if (this->current_state != State::REGISTRATION) {
throw runtime_error("tournament has already started");
}
this->current_state = State::IN_PROGRESS;
// Assign names to COM teams, and assign COM decks to all empty slots
for (size_t z = 0; z < this->zero_round_matches.size(); z++) {
auto m = this->zero_round_matches[z];
auto t = m->winner_team;
if (t->name.empty()) {
t->name = string_printf("COM:%zu", z);
}
if (this->data_index->num_com_decks() < t->max_players - t->player_serial_numbers.size()) {
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());
}
}
// Resolve all possible CPU-only matches
for (auto m : this->zero_round_matches) {
m->resolve_following_matches();
}
}
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++) {
fputc(' ', stream);
fputc(' ', stream);
}
string match_str = m->str();
fprintf(stream, "%s\n", match_str.c_str());
if (m->preceding_a) {
print_match(m->preceding_a, indent_level + 1);
}
if (m->preceding_b) {
print_match(m->preceding_b, indent_level + 1);
}
};
print_match(this->final_match, 0);
}
vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
vector<shared_ptr<Tournament>> ret;
for (size_t z = 0; z < this->tournaments.size(); z++) {
if (this->tournaments[z]) {
ret.emplace_back(this->tournaments[z]);
}
}
return ret;
}
shared_ptr<Tournament> TournamentIndex::create_tournament(
shared_ptr<const DataIndex> data_index,
const string& name,
shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2) {
// Find an unused tournament number
uint8_t number;
for (number = 0; number < this->tournaments.size(); number++) {
if (!this->tournaments[number]) {
break;
}
}
if (number >= this->tournaments.size()) {
throw runtime_error("all tournament slots are full");
}
auto t = make_shared<Tournament>(data_index, number, name, map, rules, num_teams, is_2v2);
t->init();
this->tournaments[number] = t;
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 < this->tournaments.size(); z++) {
if (this->tournaments[z] && (this->tournaments[z]->get_name() == name)) {
return this->tournaments[z];
}
}
return nullptr;
}
} // namespace Episode3
+164
View File
@@ -0,0 +1,164 @@
#pragma once
#include <stdint.h>
#include <event2/event.h>
#include <memory>
#include <vector>
#include <unordered_set>
#include <string>
#include <phosg/Strings.hh>
#include "../Player.hh"
struct Lobby;
namespace Episode3 {
// The comment in Server.hh does not apply to this file (and Tournament.cc).
// 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 {
REGISTRATION = 0,
IN_PROGRESS,
COMPLETE,
};
struct Team : public std::enable_shared_from_this<Team> {
std::weak_ptr<Tournament> tournament;
size_t index;
size_t max_players;
std::set<uint32_t> player_serial_numbers;
std::set<std::shared_ptr<const COMDeckDefinition>> com_decks;
std::string name;
std::string password;
size_t num_rounds_cleared;
bool is_active;
Team(
std::shared_ptr<Tournament> tournament,
size_t index,
size_t max_players);
std::string str() const;
void register_player(
uint32_t serial_number,
const std::string& team_name,
const std::string& password);
bool unregister_player(uint32_t serial_number);
};
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;
std::weak_ptr<Match> following;
std::shared_ptr<Team> winner_team;
size_t round_num;
Match(
std::shared_ptr<Tournament> tournament,
std::shared_ptr<Match> preceding_a,
std::shared_ptr<Match> preceding_b);
Match(
std::shared_ptr<Tournament> tournament,
std::shared_ptr<Team> winner_team);
std::string str() const;
bool resolve_if_no_players();
void resolve_following_matches();
void set_winner_team(std::shared_ptr<Team> team);
std::shared_ptr<Team> opponent_team_for_team(std::shared_ptr<Team> team) const;
};
Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
const std::string& name,
std::shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2);
~Tournament() = default;
void init();
std::shared_ptr<const DataIndex> get_data_index() const;
uint8_t get_number() const;
const std::string& get_name() const;
std::shared_ptr<const DataIndex::MapEntry> get_map() const;
const Rules& get_rules() const;
bool get_is_2v2() const;
State get_state() const;
const std::vector<std::shared_ptr<Team>>& all_teams() const;
std::shared_ptr<Team> get_team(size_t index) const;
std::shared_ptr<Team> get_winner_team() const;
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
void start();
void print_bracket(FILE* stream) const;
void print_bracket_stderr() const;
private:
PrefixedLogger log;
std::shared_ptr<const DataIndex> data_index;
uint8_t number;
std::string name;
std::shared_ptr<const DataIndex::MapEntry> map;
Rules rules;
size_t num_teams;
bool is_2v2;
State current_state;
std::set<uint32_t> all_player_serial_numbers;
std::unordered_set<std::shared_ptr<Match>> pending_matches;
// This vector contains all teams in the original starting order of the
// tournament (that is, all teams in the first round). The order within this
// vector determines which team will play against which other team in the
// first round: [0] will play against [1], [2] will play against [3], etc.
std::vector<std::shared_ptr<Team>> teams;
// The tournament begins with a "zero round", in which each team automatically
// "wins" a match, putting them into the first round. This is just to make the
// data model easier to manage, so we don't have to have a type of match with
// no preceding round.
std::vector<std::shared_ptr<Match>> zero_round_matches;
std::shared_ptr<Match> final_match;
};
class TournamentIndex {
public:
TournamentIndex() = default;
~TournamentIndex() = default;
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,
size_t num_teams,
bool is_2v2);
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;
private:
parray<std::shared_ptr<Tournament>, 0x20> tournaments;
};
} // namespace Episode3
+1
View File
@@ -95,6 +95,7 @@ struct Lobby {
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
std::shared_ptr<Episode3::BattleRecord> prev_battle_record; // Only used in primary games std::shared_ptr<Episode3::BattleRecord> prev_battle_record; // Only used in primary games
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
// Lobby stuff // Lobby stuff
uint8_t event; uint8_t event;
+2
View File
@@ -23,6 +23,8 @@ namespace MenuID {
constexpr uint32_t PROGRAMS = 0x88000088; constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099; constexpr uint32_t PATCHES = 0x99000099;
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA; constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
constexpr uint32_t TOURNAMENTS = 0xBB0000BB;
constexpr uint32_t TOURNAMENT_ENTRIES = 0xCC0000CC;
} }
namespace MainMenuItemID { namespace MainMenuItemID {
+316 -106
View File
@@ -20,11 +20,17 @@
#include "SendCommands.hh" #include "SendCommands.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
#include "Text.hh" #include "Text.hh"
#include "Episode3/Tournament.hh"
using namespace std; using namespace std;
const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier";
const char* CARD_AUCTION_DISCONNECT_HOOK_NAME = "card_auction";
vector<MenuItem> quest_categories_menu({ vector<MenuItem> quest_categories_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
@@ -861,21 +867,92 @@ static void on_ep3_battle_table_state(shared_ptr<ServerState> s,
shared_ptr<Client> c, uint16_t, uint32_t flag, const string& data) { // E4 shared_ptr<Client> c, uint16_t, uint32_t flag, const string& data) { // E4
const auto& cmd = check_size_t<C_CardBattleTableState_GC_Ep3_E4>(data); const auto& cmd = check_size_t<C_CardBattleTableState_GC_Ep3_E4>(data);
auto l = s->find_lobby(c->lobby_id); auto l = s->find_lobby(c->lobby_id);
if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
throw runtime_error("battle table command sent in non-CARD lobby");
}
if (flag) { if (flag) {
if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
throw runtime_error("battle table join command sent in non-CARD lobby");
}
c->card_battle_table_number = cmd.table_number; c->card_battle_table_number = cmd.table_number;
c->card_battle_table_seat_number = cmd.seat_number; c->card_battle_table_seat_number = cmd.seat_number;
} else {
auto team = c->ep3_tournament_team.lock();
if (team) {
auto tourn = team->tournament.lock();
if (tourn) {
auto match = tourn->next_match_for_team(team);
if (match) {
auto other_team = match->opponent_team_for_team(team);
unordered_set<uint32_t> required_serial_numbers;
for (uint32_t serial_number : team->player_serial_numbers) {
required_serial_numbers.emplace(serial_number);
}
for (uint32_t serial_number : other_team->player_serial_numbers) {
required_serial_numbers.emplace(serial_number);
}
unordered_set<shared_ptr<Client>> game_clients;
for (const auto& other_c : l->clients) {
if (!other_c) {
continue;
}
if ((other_c->card_battle_table_number == cmd.table_number) &&
required_serial_numbers.erase(other_c->license->serial_number)) {
game_clients.emplace(other_c);
}
}
if (required_serial_numbers.empty()) {
for (const auto& other_c : l->clients) {
if (other_c && (other_c->card_battle_table_number == cmd.table_number)) {
other_c->card_battle_table_number = -1;
other_c->card_battle_table_seat_number = 0;
}
}
G_SetStateFlags_GC_Ep3_6xB4x03 state_cmd;
state_cmd.state.turn_num = 1;
state_cmd.state.battle_phase = Episode3::BattlePhase::INVALID_00;
state_cmd.state.current_team_turn1 = 0xFF;
state_cmd.state.current_team_turn2 = 0xFF;
state_cmd.state.action_subphase = Episode3::ActionSubphase::ATTACK;
state_cmd.state.setup_phase = Episode3::SetupPhase::REGISTRATION;
state_cmd.state.registration_phase = Episode3::RegistrationPhase::AWAITING_NUM_PLAYERS;
state_cmd.state.team_exp.clear(0);
state_cmd.state.team_dice_boost.clear(0);
state_cmd.state.first_team_turn = 0xFF;
state_cmd.state.tournament_flag = 0x01;
state_cmd.state.client_sc_card_types.clear(Episode3::CardType::INVALID_FF);
// TODO: We don't know if this works with multiple players. Test it.
uint32_t flags = Lobby::Flag::NON_V1_ONLY | Lobby::Flag::EPISODE_3_ONLY;
auto game = create_game_generic(s, c, u"<Tournament>", u"", 0xFF, 0, flags);
game->tournament_match = match;
for (auto game_c : game_clients) {
send_command_t(game_c, 0x60, 0x00, state_cmd);
s->change_client_lobby(game_c, game, false);
send_join_lobby(game_c, game);
game_c->flags |= Client::Flag::LOADING;
}
}
}
}
}
} else { // Leaving battle table
c->card_battle_table_number = -1; c->card_battle_table_number = -1;
c->card_battle_table_seat_number = 0; c->card_battle_table_seat_number = 0;
} }
send_ep3_card_battle_table_state(l, c->card_battle_table_number); send_ep3_card_battle_table_state(l, c->card_battle_table_number);
// TODO: If a client disconnects while at the battle table, we need to send
// a table update to all the other clients at the table (if any). Use a bool should_have_disconnect_hook = (c->card_battle_table_number != -1);
// disconnect hook for this.
const char* DISCONNECT_HOOK_NAME = "battle_table_state";
if (should_have_disconnect_hook && !c->disconnect_hooks.count(DISCONNECT_HOOK_NAME)) {
c->disconnect_hooks.emplace(DISCONNECT_HOOK_NAME, [l, c]() -> void {
send_ep3_card_battle_table_state(l, c->card_battle_table_number);
});
} else if (!should_have_disconnect_hook) {
c->disconnect_hooks.erase(DISCONNECT_HOOK_NAME);
}
} }
static void on_ep3_battle_table_confirm(shared_ptr<ServerState> s, static void on_ep3_battle_table_confirm(shared_ptr<ServerState> s,
@@ -888,7 +965,7 @@ static void on_ep3_battle_table_confirm(shared_ptr<ServerState> s,
if (flag) { if (flag) {
// TODO // TODO
send_lobby_message_box(c, u"CARD battles are\nnot yet supported."); send_lobby_message_box(c, u"Battle Tables are\nnot yet supported.");
} }
} }
@@ -898,6 +975,15 @@ static void on_ep3_counter_state(shared_ptr<ServerState> s, shared_ptr<Client> c
auto l = s->find_lobby(c->lobby_id); auto l = s->find_lobby(c->lobby_id);
if (flag != 0) { if (flag != 0) {
send_command(c, 0xDC, 0x00); send_command(c, 0xDC, 0x00);
if (l->tournament_match) {
auto tourn = l->tournament_match->tournament.lock();
if (tourn) {
send_ep3_set_tournament_player_decks(l, c, l->tournament_match);
string data = Episode3::Server::prepare_6xB6x41_map_definition(
tourn->get_map());
c->channel.send(0x6C, 0x00, data);
}
}
l->flags |= Lobby::Flag::BATTLE_IN_PROGRESS; l->flags |= Lobby::Flag::BATTLE_IN_PROGRESS;
} else { } else {
l->flags &= ~Lobby::Flag::BATTLE_IN_PROGRESS; l->flags &= ~Lobby::Flag::BATTLE_IN_PROGRESS;
@@ -924,7 +1010,7 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
l->log.info("Recreating Episode 3 server state"); l->log.info("Recreating Episode 3 server state");
} }
l->ep3_server_base = make_shared<Episode3::ServerBase>( l->ep3_server_base = make_shared<Episode3::ServerBase>(
l, s->ep3_data_index, l->random_seed); l, s->ep3_data_index, l->random_seed, l->tournament_match ? true : false);
l->ep3_server_base->init(); l->ep3_server_base->init();
if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) {
@@ -960,17 +1046,80 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
} }
} }
l->ep3_server_base->server->on_server_data_input(data); l->ep3_server_base->server->on_server_data_input(data);
if (l->ep3_server_base->server->battle_finished && l->tournament_match) {
int8_t winner_team_id = l->ep3_server_base->server->get_winner_team_id();
if (winner_team_id == -1) {
throw runtime_error("match concluded, but winner team not specified");
}
if (winner_team_id == 0) {
l->tournament_match->set_winner_team(l->tournament_match->preceding_a->winner_team);
} else if (winner_team_id == 1) {
l->tournament_match->set_winner_team(l->tournament_match->preceding_b->winner_team);
} else {
throw logic_error("invalid winner team id");
}
send_ep3_tournament_match_result_result(l, l->tournament_match);
}
} }
static void on_ep3_tournament_control(shared_ptr<ServerState>, shared_ptr<Client> c, static void on_tournament_complete(
uint16_t, uint32_t, const string&) { // E2 shared_ptr<ServerState> s, shared_ptr<const Episode3::Tournament> tourn) {
// The client will set their interaction mode expecting a menu to be sent, but auto team = tourn->get_winner_team();
// since we don't implement tournaments, they will get stuck here unless we if (team->player_serial_numbers.empty()) {
// send something. An 01 (lobby message box) seems to work fine. send_ep3_text_message_printf(s, "$C7A CPU team won\nthe tournament\n$C6%s", tourn->get_name().c_str());
send_lobby_message_box(c, u"$C6Tournaments are\nnot supported."); } 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());
} }
static void on_ep3_tournament_control(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t flag, const string&) { // E2
switch (flag) {
case 0x00: // Request tournament list
send_ep3_tournament_list(s, c);
break;
case 0x01: { // Check tournament
auto team = c->ep3_tournament_team.lock();
if (team) {
auto tourn = team->tournament.lock();
if (tourn) {
send_ep3_tournament_entry_list(c, tourn);
} else {
send_lobby_message_box(c, u"$C6The tournament\nhas concluded.");
}
} else {
send_lobby_message_box(c, u"$C6You are not\nregistered in a\ntournament.");
}
break;
}
case 0x02: { // Cancel tournament entry
auto team = c->ep3_tournament_team.lock();
if (team) {
auto tourn = team->tournament.lock();
if (tourn) {
if (tourn->get_state() != Episode3::Tournament::State::COMPLETE) {
team->unregister_player(c->license->serial_number);
if (tourn->get_state() == Episode3::Tournament::State::COMPLETE) {
on_tournament_complete(s, tourn);
}
}
c->ep3_tournament_team.reset();
}
}
send_ep3_confirm_tournament_entry(s, c, nullptr);
break;
}
case 0x03: // Create tournament spectator team (get battle list)
send_lobby_message_box(c, u"$C6Not supported"); // TODO
break;
case 0x04: // Join tournament spectator team (get team list)
send_lobby_message_box(c, u"$C6Not supported"); // TODO
break;
default:
throw runtime_error("invalid tournament operation");
}
}
static void on_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // D6 uint16_t, uint32_t, const string& data) { // D6
@@ -1070,6 +1219,25 @@ static void on_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Clie
if (!game->is_game()) { if (!game->is_game()) {
send_ship_info(c, u"$C4Incorrect game ID"); send_ship_info(c, u"$C4Incorrect game ID");
} else if ((c->flags & Client::Flag::IS_EPISODE_3) &&
(game->flags & Lobby::Flag::EPISODE_3_ONLY)) {
S_GameInformation_GC_Ep3_E1 cmd;
cmd.game_name = encode_sjis(game->name);
size_t num_players = 0;
for (const auto& client : game->clients) {
if (client) {
auto player = client->game_data.player();
cmd.entries[num_players].name = player->disp.name;
cmd.entries[num_players].description = string_printf(
"%s CLv%" PRIu32 " %c",
name_for_char_class(player->disp.char_class),
player->disp.level + 1,
char_for_language_code(player->inventory.language));
num_players++;
}
}
send_command_t(c, 0xE1, 0x00, cmd);
} else { } else {
string info; string info;
for (size_t x = 0; x < game->max_clients; x++) { for (size_t x = 0; x < game->max_clients; x++) {
@@ -1163,8 +1331,55 @@ static void on_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Clie
break; break;
} }
case MenuID::TOURNAMENTS: {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
send_ship_info(c, u"Incorrect menu ID");
break;
}
auto tourn = s->ep3_tournament_index->get_tournament(cmd.item_id);
if (tourn) {
send_ep3_tournament_info(c, tourn);
}
break;
}
case MenuID::TOURNAMENT_ENTRIES: {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
send_ship_info(c, u"Incorrect menu ID");
break;
}
uint16_t tourn_num = cmd.item_id >> 16;
uint16_t team_index = cmd.item_id & 0xFFFF;
auto tourn = s->ep3_tournament_index->get_tournament(tourn_num);
if (tourn) {
auto team = tourn->get_team(team_index);
if (team) {
string message;
if (team->name.empty()) {
message = string_printf("$C7(Unnamed team)\n%zu/%zu players\n%zu wins\n%s",
team->player_serial_numbers.size(),
team->max_players,
team->num_rounds_cleared,
team->password.empty() ? "" : "Locked");
} else {
message = string_printf("$C6%s$C7\n%zu/%zu players\n%zu wins\n%s",
team->name.c_str(),
team->player_serial_numbers.size(),
team->max_players,
team->num_rounds_cleared,
team->password.empty() ? "" : "Locked");
}
send_ship_info(c, decode_sjis(message));
} else {
send_ship_info(c, u"$C7No such team");
}
} else {
send_ship_info(c, u"$C7No such tournament");
}
break;
}
default: default:
send_ship_info(c, u"Incorrect menu ID."); send_ship_info(c, u"Incorrect menu ID");
break; break;
} }
} }
@@ -1175,14 +1390,23 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint32_t menu_id; uint32_t menu_id;
uint32_t item_id; uint32_t item_id;
u16string team_name;
u16string password; u16string password;
if (data.size() > sizeof(C_MenuSelection_10_Flag00)) { if (data.size() > sizeof(C_MenuSelection_10_Flag00)) {
if (uses_unicode) { if (uses_unicode) {
// TODO: We can support the Flag03 variant here, but PC/BB probably never
// actually use it.
const auto& cmd = check_size_t<C_MenuSelection_PC_BB_10_Flag02>(data); const auto& cmd = check_size_t<C_MenuSelection_PC_BB_10_Flag02>(data);
password = cmd.password; password = cmd.password;
menu_id = cmd.basic_cmd.menu_id; menu_id = cmd.basic_cmd.menu_id;
item_id = cmd.basic_cmd.item_id; item_id = cmd.basic_cmd.item_id;
} else if (data.size() > sizeof(C_MenuSelection_DC_V3_10_Flag02)) {
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag03>(data);
team_name = decode_sjis(cmd.unknown_a1);
password = decode_sjis(cmd.password);
menu_id = cmd.basic_cmd.menu_id;
item_id = cmd.basic_cmd.item_id;
} else { } else {
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag02>(data); const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag02>(data);
password = decode_sjis(cmd.password); password = decode_sjis(cmd.password);
@@ -1503,6 +1727,9 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
(l->clients[x]->version() != GameVersion::PC) && (l->clients[x]->version() != GameVersion::PC) &&
!(l->clients[x]->flags & Client::Flag::IS_TRIAL_EDITION)) { !(l->clients[x]->flags & Client::Flag::IS_TRIAL_EDITION)) {
l->clients[x]->flags |= Client::Flag::LOADING_QUEST; l->clients[x]->flags |= Client::Flag::LOADING_QUEST;
l->clients[x]->disconnect_hooks.emplace(QUEST_BARRIER_DISCONNECT_HOOK_NAME, [l]() -> void {
send_quest_barrier_if_all_clients_ready(l);
});
} }
} }
@@ -1560,8 +1787,64 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
} }
break; break;
case MenuID::TOURNAMENTS: {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
throw runtime_error("non-Episode 3 client attempted to join tournament");
}
auto tourn = s->ep3_tournament_index->get_tournament(item_id);
if (tourn) {
send_ep3_tournament_entry_list(c, tourn);
}
break;
}
case MenuID::TOURNAMENT_ENTRIES: {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
throw runtime_error("non-Episode 3 client attempted to join tournament");
}
if (c->ep3_tournament_team.lock()) {
send_lobby_message_box(c, u"$C6You are registered\nin a different\ntournament already");
break;
}
if (team_name.empty()) {
team_name = c->game_data.player()->disp.name;
team_name += decode_sjis(string_printf("/%" PRIX32, c->license->serial_number));
}
uint16_t tourn_num = item_id >> 16;
uint16_t team_index = item_id & 0xFFFF;
auto tourn = s->ep3_tournament_index->get_tournament(tourn_num);
if (tourn) {
auto team = tourn->get_team(team_index);
if (team) {
try {
team->register_player(
c->license->serial_number,
encode_sjis(team_name),
encode_sjis(password));
c->ep3_tournament_team = team;
send_ep3_confirm_tournament_entry(s, c, tourn);
string message = string_printf("$C7You are registered in $C6%s$C7.\n\
\n\
After registration ends, you can start your\n\
first match by standing at any Battle Table in\n\
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());
} catch (const exception& e) {
string message = string_printf("Cannot join team:\n%s", e.what());
send_lobby_message_box(c, decode_sjis(message));
}
} else {
send_lobby_message_box(c, u"Team does not exist");
}
} else {
send_lobby_message_box(c, u"Tournament does\nnot exist");
}
break;
}
default: default:
send_message_box(c, u"Incorrect menu ID."); send_message_box(c, u"Incorrect menu ID");
break; break;
} }
} }
@@ -1571,7 +1854,7 @@ static void on_change_lobby(shared_ptr<ServerState> s, shared_ptr<Client> c,
const auto& cmd = check_size_t<C_LobbySelection_84>(data); const auto& cmd = check_size_t<C_LobbySelection_84>(data);
if (cmd.menu_id != MenuID::LOBBY) { if (cmd.menu_id != MenuID::LOBBY) {
send_message_box(c, u"Incorrect menu ID."); send_message_box(c, u"Incorrect menu ID");
return; return;
} }
@@ -1745,11 +2028,6 @@ static void on_quest_barrier(shared_ptr<ServerState> s, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // AC uint16_t, uint32_t, const string& data) { // AC
check_size_v(data.size(), 0); check_size_v(data.size(), 0);
auto l = s->find_lobby(c->lobby_id);
if (!l || !l->is_game()) {
return;
}
// If this client is NOT loading, they should not send an AC. Sending an AC to // If this client is NOT loading, they should not send an AC. Sending an AC to
// a client that isn't waiting to start a quest will crash the client, so we // a client that isn't waiting to start a quest will crash the client, so we
// have to be careful not to do so. // have to be careful not to do so.
@@ -1758,23 +2036,7 @@ static void on_quest_barrier(shared_ptr<ServerState> s, shared_ptr<Client> c,
} }
c->flags &= ~Client::Flag::LOADING_QUEST; c->flags &= ~Client::Flag::LOADING_QUEST;
// Check if any client is still loading send_quest_barrier_if_all_clients_ready(s->find_lobby(c->lobby_id));
// TODO: We need to handle clients disconnecting while loading. Probably
// on_client_disconnect needs to check for this case...
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (l->clients[x]->flags & Client::Flag::LOADING_QUEST) {
break;
}
}
// If they're all done, start the quest
if (x == l->max_clients) {
send_command(l, 0xAC, 0x00);
}
} }
static void on_update_quest_statistics(shared_ptr<ServerState> s, static void on_update_quest_statistics(shared_ptr<ServerState> s,
@@ -2576,10 +2838,6 @@ shared_ptr<Lobby> create_game_generic(
c->log.info("Loaded maps contain %zu entries overall", game->enemies.size()); c->log.info("Loaded maps contain %zu entries overall", game->enemies.size());
} }
s->change_client_lobby(c, game);
c->flags |= Client::Flag::LOADING;
return game; return game;
} }
@@ -2594,7 +2852,9 @@ static void on_create_game_pc(shared_ptr<ServerState> s, shared_ptr<Client> c,
if (cmd.challenge_mode) { if (cmd.challenge_mode) {
flags |= Lobby::Flag::CHALLENGE_MODE; flags |= Lobby::Flag::CHALLENGE_MODE;
} }
create_game_generic(s, c, cmd.name, cmd.password, 1, cmd.difficulty, flags); auto game = create_game_generic(s, c, cmd.name, cmd.password, 1, cmd.difficulty, flags);
s->change_client_lobby(c, game);
c->flags |= Client::Flag::LOADING;
} }
static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c,
@@ -2634,8 +2894,10 @@ static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c
flags |= Lobby::Flag::CHALLENGE_MODE; flags |= Lobby::Flag::CHALLENGE_MODE;
} }
} }
create_game_generic( auto game = create_game_generic(
s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, flags); s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, flags);
s->change_client_lobby(c, game);
c->flags |= Client::Flag::LOADING;
} }
static void on_create_game_bb(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_create_game_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
@@ -2652,8 +2914,10 @@ static void on_create_game_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
if (cmd.solo_mode) { if (cmd.solo_mode) {
flags |= Lobby::Flag::SOLO_MODE; flags |= Lobby::Flag::SOLO_MODE;
} }
create_game_generic( auto game = create_game_generic(
s, c, cmd.name, cmd.password, cmd.episode, cmd.difficulty, flags); s, c, cmd.name, cmd.password, cmd.episode, cmd.difficulty, flags);
s->change_client_lobby(c, game);
c->flags |= Client::Flag::LOADING;
} }
static void on_lobby_name_request(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_lobby_name_request(shared_ptr<ServerState> s, shared_ptr<Client> c,
@@ -2928,64 +3192,10 @@ static void on_card_auction_join(shared_ptr<ServerState> s, shared_ptr<Client> c
return; return;
} }
c->flags |= Client::Flag::AWAITING_CARD_AUCTION; c->flags |= Client::Flag::AWAITING_CARD_AUCTION;
c->disconnect_hooks.emplace(CARD_AUCTION_DISCONNECT_HOOK_NAME, [s, l]() -> void {
// Check if any client is still loading send_card_auction_if_all_clients_ready(s, l);
// TODO: We need to handle clients disconnecting during this procedure. });
// Probably on_client_disconnect needs to check for this case... send_card_auction_if_all_clients_ready(s, l);
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (!(l->clients[x]->flags & Client::Flag::AWAITING_CARD_AUCTION)) {
break;
}
}
if (x != l->max_clients) {
return;
}
for (x = 0; x < l->max_clients; x++) {
if (l->clients[x]) {
l->clients[x]->flags &= ~Client::Flag::AWAITING_CARD_AUCTION;
}
}
if ((s->ep3_card_auction_points == 0) ||
(s->ep3_card_auction_min_size == 0) ||
(s->ep3_card_auction_max_size == 0)) {
throw runtime_error("card auctions are not configured on this server");
}
uint16_t num_cards;
if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) {
num_cards = s->ep3_card_auction_min_size;
} else {
num_cards = s->ep3_card_auction_min_size +
(random_object<uint16_t>() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1));
}
num_cards = min<uint16_t>(num_cards, 0x14);
uint64_t distribution_size = 0;
for (const auto& it : s->ep3_card_auction_pool) {
distribution_size += it.second.first;
}
S_StartCardAuction_GC_Ep3_EF cmd;
cmd.points_available = s->ep3_card_auction_points;
for (size_t z = 0; z < num_cards; z++) {
uint64_t v = random_object<uint64_t>() % distribution_size;
for (const auto& it : s->ep3_card_auction_pool) {
if (v >= it.second.first) {
v -= it.second.first;
} else {
cmd.entries[z].card_id = s->ep3_data_index->definition_for_card_name(it.first)->def.card_id.load();
cmd.entries[z].min_price = it.second.second;
break;
}
}
}
send_command_t(l, 0xEF, num_cards, cmd);
} }
+354 -16
View File
@@ -21,6 +21,9 @@ using namespace std;
extern const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME;
extern const char* CARD_AUCTION_DISCONNECT_HOOK_NAME;
const unordered_set<uint32_t> v2_crypt_initial_client_commands({ const unordered_set<uint32_t> v2_crypt_initial_client_commands({
0x00260088, // (17) DCNTE license check 0x00260088, // (17) DCNTE license check
0x00B0008B, // (02) DCNTE login 0x00B0008B, // (02) DCNTE login
@@ -284,11 +287,6 @@ void send_quest_open_file_t(
void send_quest_buffer_overflow( void send_quest_buffer_overflow(
shared_ptr<ServerState> s, shared_ptr<Client> c) { shared_ptr<ServerState> s, shared_ptr<Client> c) {
// TODO: Figure out a way to share this state across sessions. Maybe we could
// e.g. modify send_1D to send a nonzero flag value, which we could use to
// know that the client already has this patch? Or just add another command in
// the login sequence?
// PSO Episode 3 USA doesn't natively support the B2 command, but we can add // PSO Episode 3 USA doesn't natively support the B2 command, but we can add
// it back to the game with some tricky commands. For details on how this // it back to the game with some tricky commands. For details on how this
// works, see system/ppc/Episode3USAQuestBufferOverflow.s. // works, see system/ppc/Episode3USAQuestBufferOverflow.s.
@@ -314,8 +312,8 @@ void send_quest_buffer_overflow(
void send_function_call( void send_function_call(
shared_ptr<Client> c, shared_ptr<Client> c,
shared_ptr<CompiledFunctionCode> code, shared_ptr<CompiledFunctionCode> code,
const std::unordered_map<std::string, uint32_t>& label_writes, const unordered_map<string, uint32_t>& label_writes,
const std::string& suffix, const string& suffix,
uint32_t checksum_addr, uint32_t checksum_addr,
uint32_t checksum_size) { uint32_t checksum_size) {
return send_function_call( return send_function_call(
@@ -332,8 +330,8 @@ void send_function_call(
Channel& ch, Channel& ch,
uint64_t client_flags, uint64_t client_flags,
shared_ptr<CompiledFunctionCode> code, shared_ptr<CompiledFunctionCode> code,
const std::unordered_map<std::string, uint32_t>& label_writes, const unordered_map<string, uint32_t>& label_writes,
const std::string& suffix, const string& suffix,
uint32_t checksum_addr, uint32_t checksum_addr,
uint32_t checksum_size) { uint32_t checksum_size) {
if (client_flags & Client::Flag::NO_SEND_FUNCTION_CALL) { if (client_flags & Client::Flag::NO_SEND_FUNCTION_CALL) {
@@ -649,6 +647,17 @@ void send_message_box(shared_ptr<Client> c, const u16string& text) {
send_text(c->channel, command, text, true); send_text(c->channel, command, text, true);
} }
void send_ep3_timed_message_box(Channel& ch, uint32_t frames, const string& message) {
StringWriter w;
w.put<S_TimedMessageBoxHeader_GC_Ep3_EA>({frames});
add_color(w, message.data(), message.size());
w.put_u8(0);
while (w.size() & 3) {
w.put_u8(0);
}
ch.send(0xEA, 0x00, w.str());
}
void send_lobby_name(shared_ptr<Client> c, const u16string& text) { void send_lobby_name(shared_ptr<Client> c, const u16string& text) {
send_text(c->channel, 0x8A, text, false); send_text(c->channel, 0x8A, text, false);
} }
@@ -670,7 +679,7 @@ void send_ship_info(Channel& ch, const u16string& text) {
send_header_text(ch, 0x11, 0, text, true); send_header_text(ch, 0x11, 0, text, true);
} }
void send_text_message(Channel& ch, const std::u16string& text) { void send_text_message(Channel& ch, const u16string& text) {
send_header_text(ch, 0xB0, 0, text, true); send_header_text(ch, 0xB0, 0, text, true);
} }
@@ -694,6 +703,22 @@ void send_text_message(shared_ptr<ServerState> s, const u16string& text) {
} }
} }
__attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
std::shared_ptr<ServerState> s, const char* format, ...) {
va_list va;
va_start(va, format);
std::string buf = string_vprintf(format, va);
va_end(va);
std::u16string decoded = decode_sjis(buf);
for (auto& it : s->id_to_lobby) {
for (auto& c : it.second->clients) {
if (c && (c->flags & Client::Flag::IS_EPISODE_3)) {
send_text_message(c, decoded);
}
}
}
}
u16string prepare_chat_message( u16string prepare_chat_message(
GameVersion version, GameVersion version,
const u16string& from_name, const u16string& from_name,
@@ -1565,8 +1590,8 @@ void send_get_player_info(shared_ptr<Client> c) {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Trade window // Trade window
void send_execute_item_trade(std::shared_ptr<Client> c, void send_execute_item_trade(shared_ptr<Client> c,
const std::vector<ItemData>& items) { const vector<ItemData>& items) {
SC_TradeItems_D0_D3 cmd; SC_TradeItems_D0_D3 cmd;
if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) { if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) {
throw logic_error("too many items in execute trade command"); throw logic_error("too many items in execute trade command");
@@ -1579,8 +1604,8 @@ void send_execute_item_trade(std::shared_ptr<Client> c,
send_command_t(c, 0xD3, 0x00, cmd); send_command_t(c, 0xD3, 0x00, cmd);
} }
void send_execute_card_trade(std::shared_ptr<Client> c, void send_execute_card_trade(shared_ptr<Client> c,
const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count) { const vector<pair<uint32_t, uint32_t>>& card_to_count) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) { if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
throw logic_error("cannot send trade cards command to non-Ep3 client"); throw logic_error("cannot send trade cards command to non-Ep3 client");
} }
@@ -1841,10 +1866,10 @@ void send_ep3_card_list_update(shared_ptr<ServerState> s, shared_ptr<Client> c)
} }
void send_ep3_media_update( void send_ep3_media_update(
std::shared_ptr<Client> c, shared_ptr<Client> c,
uint32_t type, uint32_t type,
uint32_t which, uint32_t which,
const std::string& compressed_data) { const string& compressed_data) {
StringWriter w; StringWriter w;
w.put<S_UpdateMediaHeader_GC_Ep3_B9>({type, which, compressed_data.size(), 0}); w.put<S_UpdateMediaHeader_GC_Ep3_B9>({type, which, compressed_data.size(), 0});
w.write(compressed_data); w.write(compressed_data);
@@ -1898,6 +1923,228 @@ void send_ep3_set_context_token(shared_ptr<Client> c, uint32_t context_token) {
send_command_t(c, 0xC9, 0x00, cmd); send_command_t(c, 0xC9, 0x00, cmd);
} }
void send_ep3_confirm_tournament_entry(
shared_ptr<ServerState> s,
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
S_ConfirmTournamentEntry_GC_Ep3_CC cmd;
if (tourn) {
cmd.tournament_name = tourn->get_name();
cmd.server_name = encode_sjis(s->name);
// TODO: Fill this in appropriately when we support scheduled start times
cmd.start_time = "Unknown";
auto& teams = tourn->all_teams();
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
cmd.entries[z].present = 1;
cmd.entries[z].team_name = teams[z]->name;
}
}
send_command_t(c, 0xCC, tourn ? 0x01 : 0x00, cmd);
}
void send_ep3_tournament_list(shared_ptr<ServerState> s, shared_ptr<Client> c) {
S_TournamentList_GC_Ep3_E0 cmd;
size_t z = 0;
for (const auto& tourn : s->ep3_tournament_index->all_tournaments()) {
if (z >= 0x20) {
throw logic_error("more than 32 tournaments exist");
}
auto& entry = cmd.entries[z];
entry.menu_id = MenuID::TOURNAMENTS;
entry.item_id = tourn->get_number();
// 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
// nontrivial because unlike Sega's implementation, newserv does not require
// a round to completely finish before starting matches in the next round,
// as long as the winners of the preceding matches have been determined.
entry.state =
(tourn->get_state() == Episode3::Tournament::State::REGISTRATION)
? 0x00 : 0x05;
// TODO: Fill in cmd.start_time here when we implement scheduled starts.
entry.name = tourn->get_name();
const auto& teams = tourn->all_teams();
for (auto team : teams) {
if (!team->name.empty()) {
entry.num_teams++;
}
}
entry.max_teams = teams.size();
entry.unknown_a3.clear(0xFFFF);
z++;
}
send_command_t(c, 0xE0, z, cmd);
}
void send_ep3_tournament_entry_list(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
S_TournamentEntryList_GC_Ep3_E2 cmd;
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
size_t z = 0;
for (const auto& team : tourn->all_teams()) {
if (z >= 0x20) {
throw logic_error("more than 32 teams in tournament");
}
auto& entry = cmd.entries[z];
entry.menu_id = MenuID::TOURNAMENT_ENTRIES;
entry.item_id = (tourn->get_number() << 16) | z;
entry.unknown_a2 = team->num_rounds_cleared;
entry.locked = team->password.empty() ? 0 : 1;
if (tourn->get_state() != Episode3::Tournament::State::REGISTRATION) {
entry.state = 2;
} else if (team->name.empty()) {
entry.state = 0;
} else if (team->player_serial_numbers.size() < team->max_players) {
entry.state = 1;
} else {
entry.state = 2;
}
entry.name = team->name;
z++;
}
send_command_t(c, 0xE2, z, cmd);
}
void send_ep3_tournament_info(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t) {
S_TournamentInfo_GC_Ep3_E3 cmd;
cmd.name = t->get_name();
cmd.map_name = t->get_map()->map.name;
cmd.rules = t->get_rules();
const auto& teams = t->all_teams();
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
cmd.entries[z].win_count = teams[z]->num_rounds_cleared;
cmd.entries[z].is_active = teams[z]->is_active ? 1 : 0;
cmd.entries[z].team_name = teams[z]->name;
}
cmd.max_entries = teams.size();
send_command_t(c, 0xE3, 0x02, cmd);
}
void send_ep3_set_tournament_player_decks(
std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament::Match> match) {
auto tourn = match->tournament.lock();
if (!tourn) {
throw runtime_error("tournament is deleted");
}
G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D cmd;
cmd.rules = tourn->get_rules();
cmd.map_number = tourn->get_map()->map.map_number.load();
cmd.player_slot = 0xFF;
for (size_t z = 0; z < 4; z++) {
auto& entry = cmd.entries[z];
entry.player_name.clear(0);
entry.deck_name.clear(0);
entry.unknown_a1.clear(0);
entry.card_ids.clear(0);
entry.client_id = z;
}
unordered_map<uint32_t, shared_ptr<Client>> serial_number_to_client;
for (auto client : l->clients) {
if (client) {
serial_number_to_client.emplace(client->license->serial_number, client);
}
}
size_t z = 0;
auto add_entries_for_team = [&](shared_ptr<const Episode3::Tournament::Team> team) -> void {
for (uint32_t player_serial_number : team->player_serial_numbers) {
auto& entry = cmd.entries[z];
entry.type = 1; // Human
entry.player_name = serial_number_to_client.at(player_serial_number)->game_data.player()->disp.name;
entry.unknown_a2 = 6;
if (player_serial_number == c->license->serial_number) {
cmd.player_slot = z;
}
z++;
}
for (auto com_deck : team->com_decks) {
auto& entry = cmd.entries[z];
entry.type = 2; // COM
entry.player_name = com_deck->player_name;
entry.deck_name = com_deck->deck_name;
entry.card_ids = com_deck->card_ids;
entry.unknown_a2 = 6;
z++;
}
};
add_entries_for_team(match->preceding_a->winner_team);
if (z < 1) {
throw logic_error("no entries from preceding team A");
}
if (z > 2) {
throw logic_error("too many entries from preceding team A");
}
z = 2;
add_entries_for_team(match->preceding_b->winner_team);
if (z < 3) {
throw logic_error("no entries from preceding team B");
}
if (z > 4) {
throw logic_error("too many entries from preceding team B");
}
if (!(tourn->get_data_index()->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
}
send_command_t(c, 0xC9, 0x00, cmd);
// TODO: Handle disconnection during the match (the other team should win)
}
void send_ep3_tournament_match_result_result(
shared_ptr<Lobby> l, shared_ptr<const Episode3::Tournament::Match> match) {
auto tourn = match->tournament.lock();
if (!tourn) {
return;
}
unordered_map<uint32_t, shared_ptr<Client>> serial_number_to_client;
for (auto client : l->clients) {
if (client) {
serial_number_to_client.emplace(client->license->serial_number, client);
}
}
auto write_player_names = [&](G_TournamentMatchResult_GC_Ep3_6xB4x51::NamesEntry& entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
size_t z = 0;
for (uint32_t player_serial_number : team->player_serial_numbers) {
entry.player_names[z] = serial_number_to_client.at(player_serial_number)->game_data.player()->disp.name;
z++;
}
for (auto com_deck : team->com_decks) {
entry.player_names[z] = com_deck->player_name;
z++;
}
};
G_TournamentMatchResult_GC_Ep3_6xB4x51 cmd;
cmd.match_description = string_printf("(%s) Round %zu", tourn->get_name().c_str(), match->round_num);
cmd.names_entries[0].team_name = match->preceding_a->winner_team->name;
write_player_names(cmd.names_entries[0], match->preceding_a->winner_team);
cmd.names_entries[1].team_name = match->preceding_b->winner_team->name;
write_player_names(cmd.names_entries[1], match->preceding_b->winner_team);
cmd.result_entries[0].num_players = match->preceding_a->winner_team->max_players;
cmd.result_entries[0].is_winner_team = (match->preceding_a->winner_team == match->winner_team);
cmd.result_entries[1].num_players = match->preceding_a->winner_team->max_players;
cmd.result_entries[1].is_winner_team = (match->preceding_b->winner_team == match->winner_team);
// TODO: This amount should vary depending on the match level / round number,
// but newserv doesn't currently implement meseta at all - we just always give
// the player 1000000 and never charge for anything.
cmd.meseta_amount = 100;
cmd.meseta_reward_text = "You got %s meseta!";
send_command_t(l, 0xC9, 0x00, cmd);
}
void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) { void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
if (size < 8) { if (size < 8) {
throw logic_error("Episode 3 game command is too short for masking"); throw logic_error("Episode 3 game command is too short for masking");
@@ -1992,6 +2239,97 @@ void send_quest_file(shared_ptr<Client> c, const string& quest_name,
} }
} }
void send_quest_barrier_if_all_clients_ready(shared_ptr<Lobby> l) {
if (!l || !l->is_game()) {
return;
}
// Check if any client is still loading
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (l->clients[x]->flags & Client::Flag::LOADING_QUEST) {
break;
}
}
// If they're all done, start the quest
if (x == l->max_clients) {
send_command(l, 0xAC, 0x00);
}
// Check if any client is still loading
for (x = 0; x < l->max_clients; x++) {
l->clients[x]->disconnect_hooks.erase(QUEST_BARRIER_DISCONNECT_HOOK_NAME);
}
}
void send_card_auction_if_all_clients_ready(
shared_ptr<ServerState> s, shared_ptr<Lobby> l) {
// Check if any client is still not ready
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (!(l->clients[x]->flags & Client::Flag::AWAITING_CARD_AUCTION)) {
break;
}
}
if (x != l->max_clients) {
return;
}
for (x = 0; x < l->max_clients; x++) {
if (l->clients[x]) {
l->clients[x]->flags &= ~Client::Flag::AWAITING_CARD_AUCTION;
}
}
if ((s->ep3_card_auction_points == 0) ||
(s->ep3_card_auction_min_size == 0) ||
(s->ep3_card_auction_max_size == 0)) {
throw runtime_error("card auctions are not configured on this server");
}
uint16_t num_cards;
if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) {
num_cards = s->ep3_card_auction_min_size;
} else {
num_cards = s->ep3_card_auction_min_size +
(random_object<uint16_t>() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1));
}
num_cards = min<uint16_t>(num_cards, 0x14);
uint64_t distribution_size = 0;
for (const auto& it : s->ep3_card_auction_pool) {
distribution_size += it.second.first;
}
S_StartCardAuction_GC_Ep3_EF cmd;
cmd.points_available = s->ep3_card_auction_points;
for (size_t z = 0; z < num_cards; z++) {
uint64_t v = random_object<uint64_t>() % distribution_size;
for (const auto& it : s->ep3_card_auction_pool) {
if (v >= it.second.first) {
v -= it.second.first;
} else {
cmd.entries[z].card_id = s->ep3_data_index->definition_for_card_name(it.first)->def.card_id.load();
cmd.entries[z].min_price = it.second.second;
break;
}
}
}
send_command_t(l, 0xEF, num_cards, cmd);
for (auto c : l->clients) {
if (c) {
c->disconnect_hooks.erase(CARD_AUCTION_DISCONNECT_HOOK_NAME);
}
}
}
void send_server_time(shared_ptr<Client> c) { void send_server_time(shared_ptr<Client> c) {
uint64_t t = now(); uint64_t t = now();
+28
View File
@@ -163,6 +163,7 @@ void send_enter_directory_patch(std::shared_ptr<Client> c, const std::string& di
void send_patch_file(std::shared_ptr<Client> c, std::shared_ptr<PatchFileIndex::File> f); void send_patch_file(std::shared_ptr<Client> c, std::shared_ptr<PatchFileIndex::File> f);
void send_message_box(std::shared_ptr<Client> c, const std::u16string& text); void send_message_box(std::shared_ptr<Client> c, const std::u16string& text);
void send_ep3_timed_message_box(Channel& ch, uint32_t frames, const std::string& text);
void send_lobby_name(std::shared_ptr<Client> c, const std::u16string& text); void send_lobby_name(std::shared_ptr<Client> c, const std::u16string& text);
void send_quest_info(std::shared_ptr<Client> c, const std::u16string& text, void send_quest_info(std::shared_ptr<Client> c, const std::u16string& text,
bool is_download_quest); bool is_download_quest);
@@ -211,6 +212,9 @@ __attribute__((format(printf, 2, 3))) void send_text_message_printf(
return send_text_message(t, decoded.c_str()); return send_text_message(t, decoded.c_str());
} }
__attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
std::shared_ptr<ServerState> s, const char* format, ...);
void send_info_board(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l); void send_info_board(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
void send_card_search_result( void send_card_search_result(
@@ -302,6 +306,26 @@ void send_ep3_rank_update(std::shared_ptr<Client> c);
void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number); void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number);
void send_ep3_set_context_token(std::shared_ptr<Client> c, uint32_t context_token); void send_ep3_set_context_token(std::shared_ptr<Client> c, uint32_t context_token);
void send_ep3_confirm_tournament_entry(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_tournament_list(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_ep3_tournament_entry_list(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_tournament_info(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_set_tournament_player_decks(
std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament::Match> match);
void send_ep3_tournament_match_result_result(
std::shared_ptr<Lobby> l,
std::shared_ptr<const Episode3::Tournament::Match> match);
// Pass mask_key = 0 to unmask the command // Pass mask_key = 0 to unmask the command
void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key); void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);
@@ -315,6 +339,10 @@ enum class QuestFileType {
void send_quest_file(std::shared_ptr<Client> c, const std::string& quest_name, void send_quest_file(std::shared_ptr<Client> c, const std::string& quest_name,
const std::string& basename, const std::string& contents, const std::string& basename, const std::string& contents,
QuestFileType type); QuestFileType type);
void send_quest_barrier_if_all_clients_ready(std::shared_ptr<Lobby> l);
void send_card_auction_if_all_clients_ready(
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l);
void send_server_time(std::shared_ptr<Client> c); void send_server_time(std::shared_ptr<Client> c);
+18 -2
View File
@@ -50,7 +50,8 @@ void Server::disconnect_client(shared_ptr<Client> c) {
// We can't just let c be destroyed here, since disconnect_client can be // We can't just let c be destroyed here, since disconnect_client can be
// called from within the client's channel's receive handler. So, we instead // called from within the client's channel's receive handler. So, we instead
// move it to another set, which we'll clear in an immediately-enqueued // move it to another set, which we'll clear in an immediately-enqueued
// callback after the current event. // callback after the current event. This will also call the client's
// disconnect hooks (if any).
this->clients_to_destroy.insert(move(c)); this->clients_to_destroy.insert(move(c));
} }
@@ -64,7 +65,22 @@ void Server::dispatch_destroy_clients(evutil_socket_t, short, void* ctx) {
} }
void Server::destroy_clients() { void Server::destroy_clients() {
this->clients_to_destroy.clear(); for (auto c_it = this->clients_to_destroy.begin();
c_it != this->clients_to_destroy.end();
c_it = this->clients_to_destroy.erase(c_it)) {
auto c = *c_it;
// Note: It's important to move the disconnect hooks out of the client here
// because the hooks could modify c->disconnect_hooks while it's being
// iterated here, which would invalidate these iterators.
unordered_map<string, function<void()>> hooks = move(c->disconnect_hooks);
for (auto h_it : hooks) {
try {
h_it.second();
} catch (const exception& e) {
c->log.warning("Disconnect hook %s failed: %s", h_it.first.c_str(), e.what());
}
}
}
} }
void Server::dispatch_on_listen_accept( void Server::dispatch_on_listen_accept(
+139
View File
@@ -41,6 +41,32 @@ static void set_boolean(bool* target, const string& args) {
} }
} }
static string get_quoted_string(string& s) {
string ret;
char end_char = (s.at(0) == '\"') ? '\"' : ' ';
size_t z = (s.at(0) == '\"') ? 1 : 0;
for (; (z < s.size()) && (s[z] != end_char); z++) {
if (s[z] == '\\') {
if (z + 1 < s.size()) {
ret.push_back(s[z + 1]);
} else {
throw runtime_error("incomplete escape sequence");
}
} else {
ret.push_back(s[z]);
}
}
if (end_char != ' ') {
if (z >= s.size()) {
throw runtime_error("unterminated quoted string");
}
s = s.substr(skip_whitespace(s, z + 1));
} else {
s = s.substr(skip_whitespace(s, z));
}
return ret;
}
void ServerShell::execute_command(const string& command) { void ServerShell::execute_command(const string& command) {
// find the entry in the command table and run the command // find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0); size_t command_end = skip_non_whitespace(command, 0);
@@ -89,6 +115,22 @@ Server commands:\n\
Song IDs are 0 through 51; the default song is -1.\n\ Song IDs are 0 through 51; the default song is -1.\n\
announce <message>\n\ announce <message>\n\
Send an announcement message to all players.\n\ Send an announcement message to all players.\n\
create-tournament \"Tournament Name\" \"Map Name\" <num-teams> [rules...]\n\
Create an Episode 3 tournament. Rules options:\n\
dice=MIN-MAX: Set minimum and maximum dice rolls\n\
overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\
phase-time-limit=N: Set phase time limit (in seconds)\n\
allowed-cards=ALL/N/NR/NRS: Set rarities of allowed cards\n\
deck-shuffle=ON/OFF: Enable/disable deck shuffle\n\
deck-loop=ON/OFF: Enable/disable deck loop\n\
hp=N: Set Story Character initial HP\n\
hp-type=TEAM/PLAYER/COMMON: Set team HP type\n\
allow-assists=ON/OFF: Enable/disable assist cards\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\
start-tournament \"Tournament Name\"\n\
End registration for a tournament and allow matches to begin.\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\
@@ -276,6 +318,103 @@ Proxy commands (these will only work when exactly one client is connected):\n\
u16string message16 = decode_sjis(command_args); u16string message16 = decode_sjis(command_args);
send_text_message(this->state, message16.c_str()); send_text_message(this->state, message16.c_str());
} else if (command_name == "create-tournament") {
string name = get_quoted_string(command_args);
string map_name = get_quoted_string(command_args);
auto map = this->state->ep3_data_index->definition_for_map_name(map_name);
uint32_t num_teams = stoul(get_quoted_string(command_args), nullptr, 0);
Episode3::Rules rules;
rules.set_defaults();
bool is_2v2 = false;
if (!command_args.empty()) {
auto tokens = split(command_args, ' ');
for (auto& token : tokens) {
token = tolower(token);
if (token == "2v2") {
is_2v2 = true;
} else if (starts_with(token, "dice=")) {
auto subtokens = split(token.substr(5), '-');
if (subtokens.size() != 2) {
throw runtime_error("dice option must be of the form dice=X-Y");
}
rules.min_dice = stoul(subtokens[0]);
rules.max_dice = stoul(subtokens[0]);
} else if (starts_with(token, "overall-time-limit=")) {
uint32_t limit = stoul(token.substr(19));
if (limit > 600) {
throw runtime_error("overall-time-limit must be 600 or fewer minutes");
}
if (limit % 5) {
throw runtime_error("overall-time-limit must be a multiple of 5 minutes");
}
rules.overall_time_limit = limit;
} else if (starts_with(token, "phase-time-limit=")) {
rules.phase_time_limit = stoul(token.substr(17));
} else if (starts_with(token, "hp=")) {
rules.char_hp = stoul(token.substr(3));
} else if (token == "allowed-cards=all") {
rules.allowed_cards = Episode3::AllowedCards::ALL;
} else if (token == "allowed-cards=n") {
rules.allowed_cards = Episode3::AllowedCards::N_ONLY;
} else if (token == "allowed-cards=nr") {
rules.allowed_cards = Episode3::AllowedCards::N_R_ONLY;
} else if (token == "allowed-cards=nrs") {
rules.allowed_cards = Episode3::AllowedCards::N_R_S_ONLY;
} else if (token == "deck-shuffle=on") {
rules.disable_deck_shuffle = 0;
} else if (token == "deck-shuffle=off") {
rules.disable_deck_shuffle = 1;
} else if (token == "deck-loop=on") {
rules.disable_deck_loop = 0;
} else if (token == "deck-loop=off") {
rules.disable_deck_loop = 1;
} else if (token == "allow-assists=on") {
rules.no_assist_cards = 0;
} else if (token == "allow-assists=off") {
rules.no_assist_cards = 1;
} else if (token == "dialogue=on") {
rules.disable_dialogue = 0;
} else if (token == "dialogue=off") {
rules.disable_dialogue = 1;
} else if (token == "dice-boost=on") {
rules.disable_dice_boost = 0;
} else if (token == "dice-boost=off") {
rules.disable_dice_boost = 1;
} else if (token == "hp-type=player") {
rules.hp_type = Episode3::HPType::DEFEAT_PLAYER;
} else if (token == "hp-type=team") {
rules.hp_type = Episode3::HPType::DEFEAT_TEAM;
} else if (token == "hp-type=common") {
rules.hp_type = Episode3::HPType::COMMON_HP;
} else if (token == "dice-exchange=atk") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_ATK;
} else if (token == "dice-exchange=def") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_DEF;
} else if (token == "dice-exchange=none") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::NONE;
} else {
throw runtime_error("invalid rules option: " + token);
}
}
}
if (rules.check_and_reset_invalid_fields()) {
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);
fprintf(stderr, "created tournament %02hhX\n", tourn->get_number());
} 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();
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");
}
// PROXY COMMANDS // PROXY COMMANDS
+5 -1
View File
@@ -26,6 +26,7 @@ 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),
@@ -120,7 +121,8 @@ void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
} }
} }
bool ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> new_lobby) { bool ServerState::change_client_lobby(
shared_ptr<Client> c, shared_ptr<Lobby> new_lobby, bool send_join_notification) {
uint8_t old_lobby_client_id = c->lobby_client_id; uint8_t old_lobby_client_id = c->lobby_client_id;
shared_ptr<Lobby> current_lobby = this->find_lobby(c->lobby_id); shared_ptr<Lobby> current_lobby = this->find_lobby(c->lobby_id);
@@ -141,7 +143,9 @@ bool ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> ne
send_player_leave_notification(current_lobby, old_lobby_client_id); send_player_leave_notification(current_lobby, old_lobby_client_id);
} }
} }
if (send_join_notification) {
this->send_lobby_join_notifications(new_lobby, c); this->send_lobby_join_notifications(new_lobby, c);
}
return true; return true;
} }
+5 -1
View File
@@ -9,6 +9,8 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "Episode3/DataIndex.hh"
#include "Episode3/Tournament.hh"
#include "Client.hh" #include "Client.hh"
#include "FunctionCompiler.hh" #include "FunctionCompiler.hh"
#include "GSLArchive.hh" #include "GSLArchive.hh"
@@ -66,6 +68,8 @@ struct ServerState {
std::shared_ptr<const GSLArchive> bb_data_gsl; std::shared_ptr<const GSLArchive> bb_data_gsl;
std::shared_ptr<const RareItemSet> rare_item_set; std::shared_ptr<const RareItemSet> rare_item_set;
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
uint16_t ep3_card_auction_points; uint16_t ep3_card_auction_points;
uint16_t ep3_card_auction_min_size; uint16_t ep3_card_auction_min_size;
uint16_t ep3_card_auction_max_size; uint16_t ep3_card_auction_max_size;
@@ -115,7 +119,7 @@ struct ServerState {
void add_client_to_available_lobby(std::shared_ptr<Client> c); void add_client_to_available_lobby(std::shared_ptr<Client> c);
void remove_client_from_lobby(std::shared_ptr<Client> c); void remove_client_from_lobby(std::shared_ptr<Client> c);
bool change_client_lobby(std::shared_ptr<Client> c, bool change_client_lobby(std::shared_ptr<Client> c,
std::shared_ptr<Lobby> new_lobby); std::shared_ptr<Lobby> new_lobby, bool send_join_notification = true);
void send_lobby_join_notifications(std::shared_ptr<Lobby> l, void send_lobby_join_notifications(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> joining_client); std::shared_ptr<Client> joining_client);
+19
View File
@@ -327,6 +327,25 @@ char abbreviation_for_difficulty(uint8_t difficulty) {
char char_for_language_code(uint8_t language) {
switch (language) {
case 0:
return 'J';
case 1:
return 'E';
case 2:
return 'G';
case 3:
return 'F';
case 4:
return 'S';
default:
return '?';
}
}
size_t stack_size_for_item(uint8_t data0, uint8_t data1) { size_t stack_size_for_item(uint8_t data0, uint8_t data1) {
if (data0 == 4) { if (data0 == 4) {
return 999999; return 999999;
+2
View File
@@ -47,4 +47,6 @@ const char* abbreviation_for_char_class(uint8_t cls);
const char* name_for_difficulty(uint8_t difficulty); const char* name_for_difficulty(uint8_t difficulty);
char abbreviation_for_difficulty(uint8_t difficulty); char abbreviation_for_difficulty(uint8_t difficulty);
char char_for_language_code(uint8_t language);
std::string name_for_item(const ItemData& item, bool include_color_codes); std::string name_for_item(const ItemData& item, bool include_color_codes);
+14
View File
@@ -0,0 +1,14 @@
[
// Episode 3 tournament COM decks. These are randomly chosen for each COM
// player in a tournament.
// [PlayerName, DeckName, [CardID, ...]]
["COM:D02", "Tremble", [0x0007, 0x006F, 0x006F, 0x006F, 0x0070, 0x0070, 0x01DC, 0x01DC, 0x01DC, 0x01DD, 0x01F1, 0x01F1, 0x01F1, 0x020B, 0x020B, 0x020E, 0x020E, 0x00ED, 0x00ED, 0x00ED, 0x00C6, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0220, 0x0220, 0x0107]],
["COM:D08", "Earthquake", [0x011E, 0x01E6, 0x01E6, 0x01E6, 0x01FB, 0x01FB, 0x01FB, 0x00C5, 0x00C5, 0x00C5, 0x00CA, 0x00CA, 0x00CA, 0x00CB, 0x00CB, 0x00CC, 0x00CC, 0x022C, 0x022C, 0x022C, 0x00E4, 0x00E4, 0x00E4, 0x00EF, 0x00EF, 0x00EF, 0x0215, 0x0215, 0x0215, 0x00ED, 0x00ED]],
["COM:D10", "ONIGAMI", [0x011E, 0x01F1, 0x01F1, 0x01F1, 0x0078, 0x0078, 0x0078, 0x0079, 0x0079, 0x0079, 0x005B, 0x01E7, 0x01E7, 0x01E7, 0x0215, 0x0215, 0x0215, 0x00CF, 0x00CF, 0x00CF, 0x00C6, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA]],
["COM:D15", "GAGIGAGI!", [0x0005, 0x0155, 0x0155, 0x0155, 0x000A, 0x000A, 0x015C, 0x015C, 0x015C, 0x01A3, 0x01A3, 0x01A3, 0x0016, 0x0016, 0x0016, 0x008C, 0x008C, 0x00C6, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x012E, 0x012E, 0x012E]],
["COM:H01", "Rumble!", [0x0002, 0x0279, 0x0279, 0x0184, 0x0184, 0x0184, 0x000C, 0x000C, 0x000C, 0x0284, 0x0284, 0x0016, 0x0016, 0x0016, 0x0214, 0x0214, 0x00ED, 0x00ED, 0x00ED, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0232, 0x0232, 0x0107, 0x0107, 0x0107]],
["COM:H06", "Helpless!", [0x0112, 0x0169, 0x0169, 0x0169, 0x0256, 0x0256, 0x0041, 0x0041, 0x0041, 0x018F, 0x018F, 0x018F, 0x00D8, 0x00D9, 0x00D9, 0x00DA, 0x00DF, 0x00DF, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0234, 0x0234, 0x00F6, 0x012B, 0x012B, 0x012B]],
["COM:H07", "Storm", [0x0004, 0x00C5, 0x00C5, 0x00C5, 0x00C7, 0x00C7, 0x00CA, 0x00CA, 0x00CF, 0x00CF, 0x00D4, 0x00D4, 0x0215, 0x0215, 0x0215, 0x00EF, 0x00EF, 0x00EF, 0x00F0, 0x00ED, 0x00D9, 0x00D9, 0x00DA, 0x00DA, 0x01A4, 0x01A4, 0x01A4, 0x0017, 0x0017, 0x0017, 0x01A8]],
["COM:H09", "Blow", [0x0004, 0x016B, 0x016B, 0x016B, 0x0171, 0x0171, 0x01A3, 0x01A3, 0x0017, 0x0017, 0x0017, 0x01A9, 0x01A9, 0x01A9, 0x0016, 0x0016, 0x0215, 0x0215, 0x0215, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x00CF, 0x00CF, 0x00CF]],
["COM:H16", "Struggle", [0x0110, 0x000A, 0x000A, 0x000A, 0x015C, 0x015C, 0x015C, 0x01A3, 0x01A3, 0x01A3, 0x0017, 0x0017, 0x0017, 0x0016, 0x0016, 0x0016, 0x008B, 0x008B, 0x0093, 0x0093, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0104, 0x0104]],
]