implement episode 3 tournaments
This commit is contained in:
@@ -59,6 +59,7 @@ add_executable(newserv
|
||||
src/Episode3/PlayerStateSubordinates.cc
|
||||
src/Episode3/RulerServer.cc
|
||||
src/Episode3/Server.cc
|
||||
src/Episode3/Tournament.cc
|
||||
src/FileContentsCache.cc
|
||||
src/FunctionCompiler.cc
|
||||
src/GSLArchive.cc
|
||||
|
||||
@@ -63,11 +63,31 @@ newserv supports several versions of PSO. Specifically:
|
||||
*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.*
|
||||
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.*
|
||||
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.*
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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) {
|
||||
this->license = l;
|
||||
this->game_data.guild_card_number = this->license->serial_number;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "PSOProtocol.hh"
|
||||
#include "Text.hh"
|
||||
#include "Episode3/BattleRecord.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +97,7 @@ struct Client {
|
||||
bool should_send_to_proxy_server;
|
||||
uint32_t proxy_destination_address;
|
||||
uint16_t proxy_destination_port;
|
||||
std::unordered_map<std::string, std::function<void()>> disconnect_hooks;
|
||||
|
||||
// Patch server
|
||||
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;
|
||||
int16_t card_battle_table_number;
|
||||
uint8_t card_battle_table_seat_number;
|
||||
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
|
||||
|
||||
// Miscellaneous (used by chat commands)
|
||||
uint32_t next_exp_value; // next EXP value to give
|
||||
@@ -137,6 +140,7 @@ struct Client {
|
||||
std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file;
|
||||
|
||||
Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior);
|
||||
~Client();
|
||||
|
||||
inline GameVersion version() const {
|
||||
return this->channel.version;
|
||||
|
||||
+38
-32
@@ -2318,8 +2318,8 @@ struct S_TournamentList_GC_Ep3_E0 {
|
||||
struct Entry {
|
||||
le_uint32_t menu_id = 0;
|
||||
le_uint32_t item_id = 0;
|
||||
uint8_t unknown_a1;
|
||||
uint8_t locked; // If nonzero, the lock icon appears in the menu
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t locked = 0; // If nonzero, the lock icon appears in the menu
|
||||
// Values for the state field:
|
||||
// 00 = Preparing
|
||||
// 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,
|
||||
// like "View the board" and "Board: Write". (In fact, some of the strings
|
||||
// listed above may be unintended for this menu as well.)
|
||||
uint8_t state;
|
||||
uint8_t unknown_a2;
|
||||
uint8_t state = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
le_uint32_t start_time = 0; // In seconds since Unix epoch
|
||||
ptext<char, 0x20> name;
|
||||
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)
|
||||
|
||||
// E1 (S->C): Battle information (Episode 3)
|
||||
// E1 (S->C): Game information (Episode 3)
|
||||
|
||||
struct S_Unknown_GC_Ep3_E1 {
|
||||
/* 0004 */ parray<uint8_t, 0x20> game_name;
|
||||
struct S_GameInformation_GC_Ep3_E1 {
|
||||
/* 0004 */ ptext<char, 0x20> game_name;
|
||||
struct Entry {
|
||||
ptext<char, 0x10> name;
|
||||
ptext<char, 0x20> description;
|
||||
ptext<char, 0x10> name; // From disp.name
|
||||
ptext<char, 0x20> description; // Usually something like "FOmarl CLv30 J"
|
||||
} __packed__;
|
||||
/* 0024 */ parray<Entry, 4> entries;
|
||||
/* 00E4 */ parray<uint8_t, 0x20> unknown_a3;
|
||||
@@ -2384,24 +2384,24 @@ struct S_Unknown_GC_Ep3_E1 {
|
||||
// command.
|
||||
|
||||
struct S_TournamentEntryList_GC_Ep3_E2 {
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
le_uint16_t players_per_team = 0;
|
||||
le_uint16_t unused = 0;
|
||||
struct Entry {
|
||||
le_uint32_t menu_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
|
||||
// player is prompted for a password if they select this team.
|
||||
uint8_t locked;
|
||||
uint8_t locked = 0;
|
||||
// State values:
|
||||
// 00 = empty (team_name is ignored; entry is selectable)
|
||||
// 01 = present, joinable (team_name renders in white)
|
||||
// 02 = present, finalized (team_name renders in yellow)
|
||||
// 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).
|
||||
uint8_t state;
|
||||
uint8_t unknown_a2;
|
||||
ptext<char, 0x20> team_name;
|
||||
uint8_t state = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
ptext<char, 0x20> name;
|
||||
} __packed__;
|
||||
parray<Entry, 0x20> entries;
|
||||
} __packed__;
|
||||
@@ -5214,12 +5214,12 @@ struct G_Unknown_GC_Ep3_6xB5x3C {
|
||||
parray<uint8_t, 3> unused;
|
||||
} __packed__;
|
||||
|
||||
// 6xB4x3D: Unknown
|
||||
// TODO: Document this from Episode 3 client/server disassembly
|
||||
// This may be tournament metadata.
|
||||
// 6xB4x3D: Set tournament player decks
|
||||
// This is sent before the counter sequence in a tournament game, to reserve the
|
||||
// player and COM slots and set the map number.
|
||||
|
||||
struct G_Unknown_GC_Ep3_6xB4x3D {
|
||||
G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0};
|
||||
struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D {
|
||||
G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0};
|
||||
Episode3::Rules rules;
|
||||
parray<uint8_t, 4> unknown_a1;
|
||||
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
|
||||
parray<uint8_t, 5> unknown_a1;
|
||||
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_a3 = 0;
|
||||
} __packed__;
|
||||
parray<Entry, 4> entries;
|
||||
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_a4 = 0;
|
||||
uint8_t unknown_a5 = 0;
|
||||
@@ -5481,20 +5482,25 @@ struct G_SetTrapTileLocations_GC_Ep3_6xB4x50 {
|
||||
parray<uint8_t, 2> unused;
|
||||
} __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 {
|
||||
G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchInfo_GC_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0};
|
||||
struct G_TournamentMatchResult_GC_Ep3_6xB4x51 {
|
||||
G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchResult_GC_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0};
|
||||
ptext<char, 0x40> match_description;
|
||||
struct Entry {
|
||||
struct NamesEntry {
|
||||
ptext<char, 0x20> team_name;
|
||||
parray<ptext<char, 0x10>, 2> player_names;
|
||||
} __packed__;
|
||||
parray<Entry, 2> teams;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
le_uint16_t unknown_a3 = 0;
|
||||
le_uint16_t unknown_a4 = 0;
|
||||
parray<NamesEntry, 2> names_entries;
|
||||
struct ResultEntry {
|
||||
le_uint16_t num_players;
|
||||
le_uint16_t is_winner_team;
|
||||
} __packed__;
|
||||
parray<ResultEntry, 2> result_entries;
|
||||
le_uint32_t meseta_amount = 0;
|
||||
// This field apparently is supposed to contain a %s token (as for printf)
|
||||
// that is replaced with meseta_amount.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "../Loggers.hh"
|
||||
@@ -791,6 +792,15 @@ Rules::Rules() {
|
||||
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() {
|
||||
this->overall_time_limit = 0;
|
||||
this->phase_time_limit = 0;
|
||||
@@ -1083,8 +1093,6 @@ string MapDefinition::str(const DataIndex* data_index) const {
|
||||
return join(lines, "\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Rules::check_invalid_fields() const {
|
||||
Rules t = *this;
|
||||
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) {
|
||||
throw runtime_error("duplicate map number");
|
||||
}
|
||||
this->maps_by_name.emplace(entry->map.name, entry);
|
||||
string name = entry->map.name;
|
||||
static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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) { }
|
||||
@@ -1452,6 +1477,11 @@ shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_number(uint3
|
||||
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> ret;
|
||||
for (const auto& it : this->maps) {
|
||||
@@ -1460,6 +1490,18 @@ set<uint32_t> DataIndex::all_map_ids() const {
|
||||
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
|
||||
|
||||
@@ -642,6 +642,7 @@ struct Rules {
|
||||
bool operator==(const Rules& other) const;
|
||||
bool operator!=(const Rules& other) const;
|
||||
void clear();
|
||||
void set_defaults();
|
||||
|
||||
bool check_invalid_fields() const;
|
||||
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 {
|
||||
public:
|
||||
DataIndex(const std::string& directory, uint32_t behavior_flags);
|
||||
@@ -822,8 +832,14 @@ public:
|
||||
|
||||
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_name(
|
||||
const std::string& name) 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;
|
||||
|
||||
private:
|
||||
@@ -837,6 +853,9 @@ private:
|
||||
// compressed map list at load time.
|
||||
mutable std::string compressed_map_list;
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ PlayerState::PlayerState(uint8_t client_id, shared_ptr<Server> server)
|
||||
|
||||
void PlayerState::init() {
|
||||
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
|
||||
// actually needed and implement it if so.
|
||||
// Note: The original code handles this, but we don't. This appears not to
|
||||
// ever happen, so we didn't bother implementing it.
|
||||
throw logic_error("replacing a player state object is not permitted");
|
||||
}
|
||||
|
||||
|
||||
+60
-25
@@ -34,10 +34,12 @@ void ServerBase::PresenceEntry::clear() {
|
||||
ServerBase::ServerBase(
|
||||
shared_ptr<Lobby> lobby,
|
||||
shared_ptr<const DataIndex> data_index,
|
||||
uint32_t random_seed)
|
||||
uint32_t random_seed,
|
||||
bool is_tournament)
|
||||
: lobby(lobby),
|
||||
data_index(data_index),
|
||||
random_seed(random_seed) { }
|
||||
random_seed(random_seed),
|
||||
is_tournament(is_tournament) { }
|
||||
|
||||
void ServerBase::init() {
|
||||
this->reset();
|
||||
@@ -96,7 +98,7 @@ Server::Server(shared_ptr<ServerBase> base)
|
||||
team_num_ally_fcs_destroyed(0),
|
||||
team_num_cards_destroyed(0),
|
||||
hard_reset_flag(false),
|
||||
tournament_flag(0),
|
||||
tournament_flag(base->is_tournament ? 1 : 0),
|
||||
num_trap_tiles_of_type(0),
|
||||
chosen_trap_tile_index_of_type(0),
|
||||
has_done_pb(0),
|
||||
@@ -142,6 +144,39 @@ shared_ptr<const ServerBase> Server::base() const {
|
||||
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 {
|
||||
auto l = this->base()->lobby.lock();
|
||||
if (!l) {
|
||||
@@ -418,7 +453,7 @@ bool Server::check_for_battle_end() {
|
||||
}
|
||||
} else { // Both teams defeated?? I guess this is technically possible
|
||||
ret = true;
|
||||
this->unknown_8023D4E0(0x4000);
|
||||
this->compute_losing_team_id_and_add_winner_flags(0x4000);
|
||||
}
|
||||
|
||||
} else { // Not DEFEAT_TEAM
|
||||
@@ -449,7 +484,7 @@ bool Server::check_for_battle_end() {
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
this->unknown_8023D4E0(0);
|
||||
this->compute_losing_team_id_and_add_winner_flags(0);
|
||||
}
|
||||
this->round_num--;
|
||||
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);
|
||||
}
|
||||
|
||||
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++) {
|
||||
auto ps = this->player_states[z];
|
||||
if (ps) {
|
||||
@@ -2146,8 +2181,8 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
|
||||
uint32_t flags_to_add = flags | 0x804;
|
||||
|
||||
// First, check which team has fewer surviving SCs
|
||||
int8_t team_id = -1;
|
||||
// First, check which team has more dead SCs
|
||||
int8_t losing_team_id = -1;
|
||||
uint32_t team_counts[2] = {0, 0};
|
||||
for (size_t z = 0; z < 4; 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]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} 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 (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
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]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 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 (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
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;
|
||||
}
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 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 (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
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;
|
||||
}
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 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 (team_id == -1) {
|
||||
while (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
while (losing_team_id == -1) {
|
||||
team_counts[1] = 0;
|
||||
team_counts[0] = 0;
|
||||
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[1] *= this->team_client_count[0];
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
}
|
||||
flags_to_add = flags | 0x1004;
|
||||
@@ -2250,7 +2285,7 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
if (!ps) {
|
||||
continue;
|
||||
}
|
||||
if (team_id != ps->get_team_id()) {
|
||||
if (losing_team_id != ps->get_team_id()) {
|
||||
ps->assist_flags |= flags_to_add;
|
||||
}
|
||||
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
|
||||
@@ -58,7 +58,8 @@ public:
|
||||
ServerBase(
|
||||
std::shared_ptr<Lobby> lobby,
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint32_t random_seed);
|
||||
uint32_t random_seed,
|
||||
bool is_tournament);
|
||||
void init();
|
||||
void reset();
|
||||
void recreate_server();
|
||||
@@ -74,6 +75,7 @@ public:
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
uint32_t random_seed;
|
||||
bool is_tournament;
|
||||
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules1;
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules2;
|
||||
@@ -94,6 +96,8 @@ public:
|
||||
std::shared_ptr<ServerBase> base();
|
||||
std::shared_ptr<const ServerBase> base() const;
|
||||
|
||||
int8_t get_winner_team_id() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
@@ -200,7 +204,7 @@ public:
|
||||
void handle_6xB3x41_map_request(const std::string& data);
|
||||
void handle_6xB3x48_end_turn(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 send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t negative_value);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -95,6 +95,7 @@ struct Lobby {
|
||||
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::BattleRecordPlayer> battle_player; // Only used in replay games
|
||||
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
|
||||
|
||||
// Lobby stuff
|
||||
uint8_t event;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace MenuID {
|
||||
constexpr uint32_t PROGRAMS = 0x88000088;
|
||||
constexpr uint32_t PATCHES = 0x99000099;
|
||||
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
|
||||
constexpr uint32_t TOURNAMENTS = 0xBB0000BB;
|
||||
constexpr uint32_t TOURNAMENT_ENTRIES = 0xCC0000CC;
|
||||
}
|
||||
|
||||
namespace MainMenuItemID {
|
||||
|
||||
+316
-106
@@ -20,11 +20,17 @@
|
||||
#include "SendCommands.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
|
||||
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({
|
||||
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),
|
||||
@@ -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
|
||||
const auto& cmd = check_size_t<C_CardBattleTableState_GC_Ep3_E4>(data);
|
||||
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 (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_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_seat_number = 0;
|
||||
}
|
||||
|
||||
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
|
||||
// disconnect hook for this.
|
||||
|
||||
bool should_have_disconnect_hook = (c->card_battle_table_number != -1);
|
||||
|
||||
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,
|
||||
@@ -888,7 +965,7 @@ static void on_ep3_battle_table_confirm(shared_ptr<ServerState> s,
|
||||
|
||||
if (flag) {
|
||||
// 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);
|
||||
if (flag != 0) {
|
||||
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;
|
||||
} else {
|
||||
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->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();
|
||||
|
||||
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);
|
||||
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,
|
||||
uint16_t, uint32_t, const string&) { // E2
|
||||
// The client will set their interaction mode expecting a menu to be sent, but
|
||||
// since we don't implement tournaments, they will get stuck here unless we
|
||||
// send something. An 01 (lobby message box) seems to work fine.
|
||||
send_lobby_message_box(c, u"$C6Tournaments are\nnot supported.");
|
||||
static void on_tournament_complete(
|
||||
shared_ptr<ServerState> s, shared_ptr<const Episode3::Tournament> tourn) {
|
||||
auto team = tourn->get_winner_team();
|
||||
if (team->player_serial_numbers.empty()) {
|
||||
send_ep3_text_message_printf(s, "$C7A CPU team won\nthe tournament\n$C6%s", tourn->get_name().c_str());
|
||||
} 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,
|
||||
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()) {
|
||||
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 {
|
||||
string info;
|
||||
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;
|
||||
}
|
||||
|
||||
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:
|
||||
send_ship_info(c, u"Incorrect menu ID.");
|
||||
send_ship_info(c, u"Incorrect menu ID");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1175,14 +1390,23 @@ static void on_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
|
||||
uint32_t menu_id;
|
||||
uint32_t item_id;
|
||||
u16string team_name;
|
||||
u16string password;
|
||||
|
||||
if (data.size() > sizeof(C_MenuSelection_10_Flag00)) {
|
||||
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);
|
||||
password = cmd.password;
|
||||
menu_id = cmd.basic_cmd.menu_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 {
|
||||
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag02>(data);
|
||||
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]->flags & Client::Flag::IS_TRIAL_EDITION)) {
|
||||
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;
|
||||
|
||||
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:
|
||||
send_message_box(c, u"Incorrect menu ID.");
|
||||
send_message_box(c, u"Incorrect menu ID");
|
||||
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);
|
||||
|
||||
if (cmd.menu_id != MenuID::LOBBY) {
|
||||
send_message_box(c, u"Incorrect menu ID.");
|
||||
send_message_box(c, u"Incorrect menu ID");
|
||||
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
|
||||
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
|
||||
// a client that isn't waiting to start a quest will crash the client, so we
|
||||
// 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;
|
||||
|
||||
// Check if any client is still loading
|
||||
// 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);
|
||||
}
|
||||
send_quest_barrier_if_all_clients_ready(s->find_lobby(c->lobby_id));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
s->change_client_lobby(c, game);
|
||||
c->flags |= Client::Flag::LOADING;
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
@@ -2594,7 +2852,9 @@ static void on_create_game_pc(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
if (cmd.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,
|
||||
@@ -2634,8 +2894,10 @@ static void on_create_game_dc_v3(shared_ptr<ServerState> s, shared_ptr<Client> c
|
||||
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->change_client_lobby(c, game);
|
||||
c->flags |= Client::Flag::LOADING;
|
||||
}
|
||||
|
||||
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) {
|
||||
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->change_client_lobby(c, game);
|
||||
c->flags |= Client::Flag::LOADING;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
c->flags |= Client::Flag::AWAITING_CARD_AUCTION;
|
||||
|
||||
// Check if any client is still loading
|
||||
// TODO: We need to handle clients disconnecting during this procedure.
|
||||
// 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::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);
|
||||
c->disconnect_hooks.emplace(CARD_AUCTION_DISCONNECT_HOOK_NAME, [s, l]() -> void {
|
||||
send_card_auction_if_all_clients_ready(s, l);
|
||||
});
|
||||
send_card_auction_if_all_clients_ready(s, l);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+354
-16
@@ -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({
|
||||
0x00260088, // (17) DCNTE license check
|
||||
0x00B0008B, // (02) DCNTE login
|
||||
@@ -284,11 +287,6 @@ void send_quest_open_file_t(
|
||||
|
||||
void send_quest_buffer_overflow(
|
||||
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
|
||||
// it back to the game with some tricky commands. For details on how this
|
||||
// works, see system/ppc/Episode3USAQuestBufferOverflow.s.
|
||||
@@ -314,8 +312,8 @@ void send_quest_buffer_overflow(
|
||||
void send_function_call(
|
||||
shared_ptr<Client> c,
|
||||
shared_ptr<CompiledFunctionCode> code,
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes,
|
||||
const std::string& suffix,
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const string& suffix,
|
||||
uint32_t checksum_addr,
|
||||
uint32_t checksum_size) {
|
||||
return send_function_call(
|
||||
@@ -332,8 +330,8 @@ void send_function_call(
|
||||
Channel& ch,
|
||||
uint64_t client_flags,
|
||||
shared_ptr<CompiledFunctionCode> code,
|
||||
const std::unordered_map<std::string, uint32_t>& label_writes,
|
||||
const std::string& suffix,
|
||||
const unordered_map<string, uint32_t>& label_writes,
|
||||
const string& suffix,
|
||||
uint32_t checksum_addr,
|
||||
uint32_t checksum_size) {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
GameVersion version,
|
||||
const u16string& from_name,
|
||||
@@ -1565,8 +1590,8 @@ void send_get_player_info(shared_ptr<Client> c) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Trade window
|
||||
|
||||
void send_execute_item_trade(std::shared_ptr<Client> c,
|
||||
const std::vector<ItemData>& items) {
|
||||
void send_execute_item_trade(shared_ptr<Client> c,
|
||||
const vector<ItemData>& items) {
|
||||
SC_TradeItems_D0_D3 cmd;
|
||||
if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) {
|
||||
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);
|
||||
}
|
||||
|
||||
void send_execute_card_trade(std::shared_ptr<Client> c,
|
||||
const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count) {
|
||||
void send_execute_card_trade(shared_ptr<Client> c,
|
||||
const vector<pair<uint32_t, uint32_t>>& card_to_count) {
|
||||
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
|
||||
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(
|
||||
std::shared_ptr<Client> c,
|
||||
shared_ptr<Client> c,
|
||||
uint32_t type,
|
||||
uint32_t which,
|
||||
const std::string& compressed_data) {
|
||||
const string& compressed_data) {
|
||||
StringWriter w;
|
||||
w.put<S_UpdateMediaHeader_GC_Ep3_B9>({type, which, compressed_data.size(), 0});
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (size < 8) {
|
||||
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) {
|
||||
uint64_t t = now();
|
||||
|
||||
|
||||
@@ -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_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_quest_info(std::shared_ptr<Client> c, const std::u16string& text,
|
||||
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());
|
||||
}
|
||||
|
||||
__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_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_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
|
||||
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,
|
||||
const std::string& basename, const std::string& contents,
|
||||
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);
|
||||
|
||||
|
||||
+18
-2
@@ -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
|
||||
// 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
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -64,7 +65,22 @@ void Server::dispatch_destroy_clients(evutil_socket_t, short, void* ctx) {
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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) {
|
||||
// find the entry in the command table and run the command
|
||||
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\
|
||||
announce <message>\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\
|
||||
Proxy commands (these will only work when exactly one client is connected):\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);
|
||||
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
|
||||
|
||||
+6
-2
@@ -26,6 +26,7 @@ ServerState::ServerState()
|
||||
catch_handler_exceptions(true),
|
||||
ep3_behavior_flags(0),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT),
|
||||
ep3_tournament_index(new Episode3::TournamentIndex()),
|
||||
ep3_card_auction_points(0),
|
||||
ep3_card_auction_min_size(0),
|
||||
ep3_card_auction_max_size(0),
|
||||
@@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
this->send_lobby_join_notifications(new_lobby, c);
|
||||
if (send_join_notification) {
|
||||
this->send_lobby_join_notifications(new_lobby, c);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -9,6 +9,8 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Episode3/DataIndex.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
#include "Client.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "GSLArchive.hh"
|
||||
@@ -66,6 +68,8 @@ struct ServerState {
|
||||
std::shared_ptr<const GSLArchive> bb_data_gsl;
|
||||
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_min_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 remove_client_from_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,
|
||||
std::shared_ptr<Client> joining_client);
|
||||
|
||||
@@ -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) {
|
||||
if (data0 == 4) {
|
||||
return 999999;
|
||||
|
||||
@@ -47,4 +47,6 @@ const char* abbreviation_for_char_class(uint8_t cls);
|
||||
const char* name_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);
|
||||
|
||||
@@ -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]],
|
||||
]
|
||||
Reference in New Issue
Block a user