make tournaments work with multiple human players
This commit is contained in:
@@ -29,7 +29,6 @@ newserv is many things - a server, a proxy, an encryption and decryption tool, a
|
||||
With that said, I offer no guarantees on how or when this project will advance. Feel free to submit GitHub issues if you find bugs or have feature requests; I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner. If you feel like contributing to newserv yourself, pull requests are welcome as well.
|
||||
|
||||
Current known issues / missing features / things to do:
|
||||
- Support disconnect hooks to clean up state, like if a client disconnects during quest loading or during a trade window execution.
|
||||
- Episode 3 battles are implemented but are not well-tested.
|
||||
- Fix behavior when joining a spectator team after the beginning of a battle.
|
||||
- PSOBB is not well-tested and likely will disconnect or misbehave when clients try to use unimplemented features.
|
||||
@@ -41,8 +40,13 @@ Current known issues / missing features / things to do:
|
||||
- Implement private and overflow lobbies.
|
||||
- Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.)
|
||||
- Encapsulate BB server-side random state and make replays deterministic.
|
||||
- The internal menu abstraction is ugly and hard to work with. Rewrite it.
|
||||
- Add default values for all commands (like we use for Episode 3 battle commands).
|
||||
- Code style
|
||||
- The internal menu abstraction is ugly and hard to work with. Rewrite it.
|
||||
- Add default values for all commands (like we use for Episode 3 battle commands).
|
||||
- Episode 3 bugs
|
||||
- Disconnecting during a match turns you into a COM if there are other humans in the match, even if the match is part of a tournament. This may be incorrect behavior for tournaments.
|
||||
- Disconnecting during a tournament when there are no other humans in the match simply cancels the match (so it can be replayed) instead of forfeiting, which is almost certainly incorrect behavior. (Then again, no one likes losing tournaments to COMs...)
|
||||
- There is a rare failure mode during battles that causes one of the clients to be disconnected.
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
||||
+109
-39
@@ -11,6 +11,23 @@ namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number)
|
||||
: serial_number(serial_number), com_deck() { }
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(
|
||||
shared_ptr<const COMDeckDefinition> com_deck)
|
||||
: serial_number(0), com_deck(com_deck) { }
|
||||
|
||||
bool Tournament::PlayerEntry::is_com() const {
|
||||
return (this->com_deck != nullptr);
|
||||
}
|
||||
|
||||
bool Tournament::PlayerEntry::is_human() const {
|
||||
return (this->serial_number != 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Tournament::Team::Team(
|
||||
shared_ptr<Tournament> tournament, size_t index, size_t max_players)
|
||||
: tournament(tournament),
|
||||
@@ -22,12 +39,21 @@ Tournament::Team::Team(
|
||||
is_active(true) { }
|
||||
|
||||
string Tournament::Team::str() const {
|
||||
string ret = string_printf("[Team/%zu %s %zu/%zuP name=%s pass=%s rounds=%zu",
|
||||
size_t num_human_players = 0;
|
||||
size_t num_com_players = 0;
|
||||
for (const auto& player : this->players) {
|
||||
num_human_players += player.is_human();
|
||||
num_com_players += player.is_com();
|
||||
}
|
||||
|
||||
string ret = string_printf("[Team/%zu %s %zuH/%zuC/%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(),
|
||||
num_human_players, num_com_players, 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);
|
||||
for (const auto& player : this->players) {
|
||||
if (player.is_human()) {
|
||||
ret += string_printf(" %08" PRIX32, player.serial_number);
|
||||
}
|
||||
}
|
||||
return ret + "]";
|
||||
}
|
||||
@@ -36,7 +62,7 @@ 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) {
|
||||
if (this->players.size() >= this->max_players) {
|
||||
throw runtime_error("team is full");
|
||||
}
|
||||
|
||||
@@ -52,10 +78,14 @@ void Tournament::Team::register_player(
|
||||
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");
|
||||
for (const auto& player : this->players) {
|
||||
if (player.is_human() && (player.serial_number == serial_number)) {
|
||||
throw logic_error("player already registered in team but not in tournament");
|
||||
}
|
||||
}
|
||||
|
||||
this->players.emplace_back(serial_number);
|
||||
|
||||
if (this->name.empty()) {
|
||||
this->name = team_name;
|
||||
this->password = password;
|
||||
@@ -63,8 +93,18 @@ void Tournament::Team::register_player(
|
||||
}
|
||||
|
||||
bool Tournament::Team::unregister_player(uint32_t serial_number) {
|
||||
if (this->player_serial_numbers.erase(serial_number)) {
|
||||
if (this->player_serial_numbers.empty()) {
|
||||
size_t index;
|
||||
for (index = 0; index < this->players.size(); index++) {
|
||||
if (this->players[index].is_human() &&
|
||||
(this->players[index].serial_number == serial_number)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < this->players.size()) {
|
||||
this->players.erase(this->players.begin() + index);
|
||||
|
||||
if (this->players.empty()) {
|
||||
this->name.clear();
|
||||
this->password.clear();
|
||||
}
|
||||
@@ -108,6 +148,31 @@ bool Tournament::Team::unregister_player(uint32_t serial_number) {
|
||||
}
|
||||
}
|
||||
|
||||
bool Tournament::Team::has_any_human_players() const {
|
||||
for (const auto& player : this->players) {
|
||||
if (player.is_human()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t Tournament::Team::num_human_players() const {
|
||||
size_t ret = 0;
|
||||
for (const auto& player : this->players) {
|
||||
ret += player.is_human();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t Tournament::Team::num_com_players() const {
|
||||
size_t ret = 0;
|
||||
for (const auto& player : this->players) {
|
||||
ret += player.is_com();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Tournament::Match::Match(
|
||||
@@ -139,7 +204,7 @@ string Tournament::Match::str() const {
|
||||
return string_printf("[Match round=%zu winner=%s]", this->round_num, winner_str.c_str());
|
||||
}
|
||||
|
||||
bool Tournament::Match::resolve_if_no_players() {
|
||||
bool Tournament::Match::resolve_if_no_human_players() {
|
||||
if (this->winner_team) {
|
||||
return true;
|
||||
}
|
||||
@@ -148,8 +213,8 @@ bool Tournament::Match::resolve_if_no_players() {
|
||||
// arbitrarily
|
||||
if (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->preceding_a->winner_team->has_any_human_players() &&
|
||||
!this->preceding_b->winner_team->has_any_human_players()) {
|
||||
this->set_winner_team((random_object<uint8_t>() & 1)
|
||||
? this->preceding_b->winner_team : this->preceding_a->winner_team);
|
||||
return true;
|
||||
@@ -169,7 +234,7 @@ void Tournament::Match::on_winner_team_set() {
|
||||
// Resolve the following match if possible (this skips CPU-only matches). If
|
||||
// the following match can't be resolved, mark it pending.
|
||||
auto following = this->following.lock();
|
||||
if (following && !following->resolve_if_no_players()) {
|
||||
if (following && !following->resolve_if_no_human_players()) {
|
||||
tournament->pending_matches.emplace(following);
|
||||
}
|
||||
|
||||
@@ -278,14 +343,15 @@ void Tournament::init() {
|
||||
team->name = team_dict.at("name")->as_string();
|
||||
team->password = team_dict.at("password")->as_string();
|
||||
team_index_to_rounds_cleared.emplace_back(team_dict.at("num_rounds_cleared")->as_int());
|
||||
for (const auto& serial_number_json : team_dict.at("player_serial_numbers")->as_list()) {
|
||||
uint32_t serial_number = serial_number_json->as_int();
|
||||
team->player_serial_numbers.emplace(serial_number);
|
||||
this->all_player_serial_numbers.emplace(serial_number);
|
||||
}
|
||||
for (const auto& com_deck_name_json : team_dict.at("com_deck_names")->as_list()) {
|
||||
team->com_decks.emplace_back(this->data_index->com_deck(
|
||||
com_deck_name_json->as_string()));
|
||||
for (const auto& player_json : team_dict.at("player_specs")->as_list()) {
|
||||
if (player_json->is_int()) {
|
||||
uint32_t serial_number = player_json->as_int();
|
||||
team->players.emplace_back(serial_number);
|
||||
this->all_player_serial_numbers.emplace(serial_number);
|
||||
} else {
|
||||
team->players.emplace_back(this->data_index->com_deck(
|
||||
player_json->as_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
this->num_teams = this->teams.size();
|
||||
@@ -410,16 +476,15 @@ std::shared_ptr<JSONObject> Tournament::json() const {
|
||||
for (auto team : this->teams) {
|
||||
unordered_map<string, shared_ptr<JSONObject>> team_dict;
|
||||
team_dict.emplace("max_players", make_json_int(team->max_players));
|
||||
vector<shared_ptr<JSONObject>> player_serial_numbers_list;
|
||||
for (uint32_t player_serial_number : team->player_serial_numbers) {
|
||||
player_serial_numbers_list.emplace_back(make_json_int(player_serial_number));
|
||||
vector<shared_ptr<JSONObject>> player_jsons_list;
|
||||
for (const auto& player : team->players) {
|
||||
if (player.is_human()) {
|
||||
player_jsons_list.emplace_back(make_json_int(player.serial_number));
|
||||
} else {
|
||||
player_jsons_list.emplace_back(make_json_str(player.com_deck->deck_name));
|
||||
}
|
||||
}
|
||||
team_dict.emplace("player_serial_numbers", make_json_list(move(player_serial_numbers_list)));
|
||||
vector<shared_ptr<JSONObject>> com_deck_names_list;
|
||||
for (auto com_deck : team->com_decks) {
|
||||
com_deck_names_list.emplace_back(make_json_str(com_deck->deck_name));
|
||||
}
|
||||
team_dict.emplace("com_deck_names", make_json_list(move(com_deck_names_list)));
|
||||
team_dict.emplace("player_specs", make_json_list(move(player_jsons_list)));
|
||||
team_dict.emplace("name", make_json_str(team->name));
|
||||
team_dict.emplace("password", make_json_str(team->password));
|
||||
team_dict.emplace("num_rounds_cleared", make_json_int(team->num_rounds_cleared));
|
||||
@@ -503,13 +568,11 @@ shared_ptr<Tournament::Team> Tournament::team_for_serial_number(
|
||||
}
|
||||
|
||||
for (auto team : this->teams) {
|
||||
if (!team->player_serial_numbers.count(serial_number)) {
|
||||
continue;
|
||||
for (const auto& player : team->players) {
|
||||
if (player.serial_number == serial_number) {
|
||||
return team->is_active ? team : nullptr;
|
||||
}
|
||||
}
|
||||
if (!team->is_active) {
|
||||
return nullptr;
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
throw logic_error("serial number registered in tournament but not in any team");
|
||||
@@ -533,11 +596,18 @@ void Tournament::start() {
|
||||
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()) {
|
||||
for (const auto& player : t->players) {
|
||||
if (player.is_com()) {
|
||||
throw logic_error("non-human player on team before tournament start");
|
||||
}
|
||||
}
|
||||
if (this->data_index->num_com_decks() < t->max_players - t->players.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_back(this->data_index->random_com_deck());
|
||||
// TODO: Don't allow duplicate COM decks, nor duplicate COM SCs on the same
|
||||
// team
|
||||
while (t->players.size() < t->max_players) {
|
||||
t->players.emplace_back(this->data_index->random_com_deck());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,25 @@ public:
|
||||
COMPLETE,
|
||||
};
|
||||
|
||||
struct PlayerEntry {
|
||||
// Invariant: (serial_number == 0) != (com_deck == nullptr)
|
||||
// (that is, exactly one of the following must be valid)
|
||||
uint32_t serial_number;
|
||||
std::shared_ptr<const COMDeckDefinition> com_deck;
|
||||
|
||||
explicit PlayerEntry(uint32_t serial_number);
|
||||
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck);
|
||||
|
||||
bool is_com() const;
|
||||
bool is_human() const;
|
||||
};
|
||||
|
||||
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::vector<std::shared_ptr<const COMDeckDefinition>> com_decks;
|
||||
|
||||
std::vector<PlayerEntry> players;
|
||||
std::string name;
|
||||
std::string password;
|
||||
size_t num_rounds_cleared;
|
||||
@@ -50,6 +63,10 @@ public:
|
||||
const std::string& team_name,
|
||||
const std::string& password);
|
||||
bool unregister_player(uint32_t serial_number);
|
||||
|
||||
bool has_any_human_players() const;
|
||||
size_t num_human_players() const;
|
||||
size_t num_com_players() const;
|
||||
};
|
||||
|
||||
struct Match : public std::enable_shared_from_this<Match> {
|
||||
@@ -69,7 +86,7 @@ public:
|
||||
std::shared_ptr<Team> winner_team);
|
||||
std::string str() const;
|
||||
|
||||
bool resolve_if_no_players();
|
||||
bool resolve_if_no_human_players();
|
||||
void on_winner_team_set();
|
||||
void set_winner_team(std::shared_ptr<Team> team);
|
||||
void set_winner_team_without_triggers(std::shared_ptr<Team> team);
|
||||
|
||||
+32
-15
@@ -71,10 +71,18 @@ size_t Lobby::count_clients() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Lobby::add_client(shared_ptr<Client> c) {
|
||||
void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
ssize_t index;
|
||||
ssize_t min_client_id = (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
|
||||
if (c->prefer_high_lobby_client_id) {
|
||||
|
||||
if (required_client_id >= 0) {
|
||||
if (this->clients[required_client_id].get()) {
|
||||
throw out_of_range("required slot is in use");
|
||||
}
|
||||
this->clients[required_client_id] = c;
|
||||
index = required_client_id;
|
||||
|
||||
} else if (c->prefer_high_lobby_client_id) {
|
||||
for (index = max_clients - 1; index >= min_client_id; index--) {
|
||||
if (!this->clients[index].get()) {
|
||||
this->clients[index] = c;
|
||||
@@ -100,16 +108,15 @@ void Lobby::add_client(shared_ptr<Client> c) {
|
||||
c->lobby_id = this->lobby_id;
|
||||
|
||||
// If there's no one else in the lobby, set the leader id as well
|
||||
if (index == (max_clients - 1) * c->prefer_high_lobby_client_id) {
|
||||
for (index = 0; index < max_clients; index++) {
|
||||
if (this->clients[index].get() && this->clients[index] != c) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= max_clients) {
|
||||
this->leader_id = c->lobby_client_id;
|
||||
size_t leader_index;
|
||||
for (leader_index = 0; leader_index < max_clients; leader_index++) {
|
||||
if (this->clients[leader_index] && (this->clients[leader_index] != c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (leader_index >= max_clients) {
|
||||
this->leader_id = c->lobby_client_id;
|
||||
}
|
||||
|
||||
// If the lobby is a game and item tracking is enabled, assign the inventory's
|
||||
// item IDs
|
||||
@@ -184,18 +191,28 @@ void Lobby::remove_client(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
|
||||
shared_ptr<Client> c) {
|
||||
void Lobby::move_client_to_lobby(
|
||||
shared_ptr<Lobby> dest_lobby,
|
||||
shared_ptr<Client> c,
|
||||
ssize_t required_client_id) {
|
||||
if (dest_lobby.get() == this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dest_lobby->count_clients() >= dest_lobby->max_clients) {
|
||||
throw out_of_range("no space left in lobby");
|
||||
if (required_client_id >= 0) {
|
||||
if (dest_lobby->clients[required_client_id]) {
|
||||
throw out_of_range("required slot is in use");
|
||||
}
|
||||
} else {
|
||||
ssize_t min_client_id = (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
|
||||
size_t available_slots = dest_lobby->max_clients - min_client_id;
|
||||
if (dest_lobby->count_clients() >= available_slots) {
|
||||
throw out_of_range("no space left in lobby");
|
||||
}
|
||||
}
|
||||
|
||||
this->remove_client(c);
|
||||
dest_lobby->add_client(c);
|
||||
dest_lobby->add_client(c, required_client_id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-3
@@ -106,6 +106,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint32_t flags;
|
||||
std::shared_ptr<const Quest> loading_quest;
|
||||
std::array<std::shared_ptr<Client>, 12> clients;
|
||||
// Keys in this map are client_id
|
||||
std::unordered_map<size_t, std::weak_ptr<Client>> tournament_clients_to_add;
|
||||
|
||||
explicit Lobby(uint32_t id);
|
||||
|
||||
@@ -117,11 +119,13 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
size_t count_clients() const;
|
||||
bool any_client_loading() const;
|
||||
|
||||
void add_client(std::shared_ptr<Client> c);
|
||||
void add_client(std::shared_ptr<Client> c, ssize_t required_client_id = -1);
|
||||
void remove_client(std::shared_ptr<Client> c);
|
||||
|
||||
void move_client_to_lobby(std::shared_ptr<Lobby> dest_lobby,
|
||||
std::shared_ptr<Client> c);
|
||||
void move_client_to_lobby(
|
||||
std::shared_ptr<Lobby> dest_lobby,
|
||||
std::shared_ptr<Client> c,
|
||||
ssize_t required_client_id = -1);
|
||||
|
||||
std::shared_ptr<Client> find_client(
|
||||
const std::u16string* identifier = nullptr,
|
||||
|
||||
+130
-64
@@ -875,54 +875,33 @@ static void on_ep3_meseta_transaction(shared_ptr<ServerState>,
|
||||
send_command(c, command, 0x03, &out_cmd, sizeof(out_cmd));
|
||||
}
|
||||
|
||||
static bool start_ep3_tournament_match_if_pending(
|
||||
shared_ptr<ServerState> s,
|
||||
shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c,
|
||||
int16_t table_number) {
|
||||
auto team = c->ep3_tournament_team.lock();
|
||||
if (!team) {
|
||||
return false; // Client is not registered in a tournament
|
||||
static bool add_next_tournament_match_client(
|
||||
shared_ptr<ServerState> s, shared_ptr<Lobby> l) {
|
||||
if (!l->tournament_match) {
|
||||
return false;
|
||||
}
|
||||
auto tourn = team->tournament.lock();
|
||||
auto tourn = l->tournament_match->tournament.lock();
|
||||
if (!tourn) {
|
||||
return false; // The tournament has been canceled
|
||||
}
|
||||
auto match = tourn->next_match_for_team(team);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
auto it = l->tournament_clients_to_add.begin();
|
||||
if (it == l->tournament_clients_to_add.end()) {
|
||||
return false;
|
||||
}
|
||||
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 == table_number) &&
|
||||
required_serial_numbers.erase(other_c->license->serial_number)) {
|
||||
game_clients.emplace(other_c);
|
||||
}
|
||||
}
|
||||
if (!required_serial_numbers.empty()) {
|
||||
size_t target_client_id = it->first;
|
||||
shared_ptr<Client> c = it->second.lock();
|
||||
l->tournament_clients_to_add.erase(it);
|
||||
|
||||
// If the client has disconnected before they could join the match, disband
|
||||
// the entire game
|
||||
if (!c) {
|
||||
send_command(l, 0xED, 0x00);
|
||||
return false;
|
||||
}
|
||||
|
||||
// At this point, we've checked all the necessary conditions for a tournament
|
||||
// match to begin.
|
||||
|
||||
for (const auto& other_c : l->clients) {
|
||||
if (other_c && (other_c->card_battle_table_number == table_number)) {
|
||||
other_c->card_battle_table_number = -1;
|
||||
other_c->card_battle_table_seat_number = 0;
|
||||
}
|
||||
if (l->clients[target_client_id] != nullptr) {
|
||||
throw logic_error("client id is already in use");
|
||||
}
|
||||
|
||||
G_SetStateFlags_GC_Ep3_6xB4x03 state_cmd;
|
||||
@@ -955,8 +934,8 @@ static bool start_ep3_tournament_match_if_pending(
|
||||
static const std::pair<uint16_t, uint16_t> final_lose_entries[10] = {
|
||||
{1, -5}, {-1, -10}, {-3, -15}, {-7, -20}, {-15, -20}, {-20, -25}, {-30, -30}, {-40, -30}, {-50, -34}, {0, -40}};
|
||||
G_SetEXResultValues_GC_Ep3_6xB4x4B ex_cmd;
|
||||
const auto& win_entries = (match == tourn->get_final_match()) ? final_win_entries : non_final_win_entries;
|
||||
const auto& lose_entries = (match == tourn->get_final_match()) ? final_lose_entries : non_final_lose_entries;
|
||||
const auto& win_entries = (l->tournament_match == tourn->get_final_match()) ? final_win_entries : non_final_win_entries;
|
||||
const auto& lose_entries = (l->tournament_match == tourn->get_final_match()) ? final_lose_entries : non_final_lose_entries;
|
||||
for (size_t z = 0; z < 10; z++) {
|
||||
ex_cmd.win_entries[z].threshold = win_entries[z].first;
|
||||
ex_cmd.win_entries[z].value = win_entries[z].second;
|
||||
@@ -968,17 +947,106 @@ static bool start_ep3_tournament_match_if_pending(
|
||||
set_mask_for_ep3_game_command(&ex_cmd, sizeof(ex_cmd), mask_key);
|
||||
}
|
||||
|
||||
// TODO: We don't know if this works with multiple players. Test it.
|
||||
send_command_t(c, 0xC9, 0x00, state_cmd);
|
||||
send_command_t(c, 0xC9, 0x00, ex_cmd);
|
||||
s->change_client_lobby(c, l, true, target_client_id);
|
||||
c->flags |= Client::Flag::LOADING;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool start_ep3_tournament_match_if_pending(
|
||||
shared_ptr<ServerState> s,
|
||||
shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c,
|
||||
int16_t table_number) {
|
||||
auto team = c->ep3_tournament_team.lock();
|
||||
if (!team) {
|
||||
return false; // Client is not registered in a tournament
|
||||
}
|
||||
auto tourn = team->tournament.lock();
|
||||
if (!tourn) {
|
||||
return false; // The tournament has been canceled
|
||||
}
|
||||
auto match = tourn->next_match_for_team(team);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto other_team = match->opponent_team_for_team(team);
|
||||
|
||||
vector<uint32_t> required_serial_numbers;
|
||||
required_serial_numbers.resize(4, 0);
|
||||
auto add_team_players = [&](shared_ptr<const Episode3::Tournament::Team> team, size_t base_index) -> void {
|
||||
size_t z = 0;
|
||||
for (const auto& player : team->players) {
|
||||
if (z >= 2) {
|
||||
throw logic_error("more than 2 players on team");
|
||||
}
|
||||
if (player.is_human()) {
|
||||
required_serial_numbers.at(base_index + z) = player.serial_number;
|
||||
}
|
||||
z++;
|
||||
}
|
||||
};
|
||||
add_team_players(team, 0);
|
||||
add_team_players(other_team, 2);
|
||||
|
||||
auto clients_map = l->clients_by_serial_number();
|
||||
vector<shared_ptr<Client>> game_clients;
|
||||
game_clients.resize(4);
|
||||
for (size_t z = 0; z < required_serial_numbers.size(); z++) {
|
||||
uint32_t serial_number = required_serial_numbers[z];
|
||||
if (!serial_number) {
|
||||
continue;
|
||||
}
|
||||
shared_ptr<Client> player_c;
|
||||
try {
|
||||
player_c = clients_map.at(serial_number);
|
||||
} catch (const out_of_range&) {
|
||||
return false;
|
||||
}
|
||||
if (player_c->card_battle_table_number != table_number) {
|
||||
return false;
|
||||
}
|
||||
game_clients.at(z) = player_c;
|
||||
}
|
||||
|
||||
// If there is already a game for this match, do nothing (the player is
|
||||
// probably about to be pulled into it, when another player is done loading)
|
||||
for (auto l : s->all_lobbies()) {
|
||||
if (l->tournament_match == match) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we've checked all the necessary conditions for a tournament
|
||||
// match to begin.
|
||||
|
||||
for (const auto& other_c : game_clients) {
|
||||
if (!other_c) {
|
||||
continue;
|
||||
}
|
||||
|
||||
other_c->card_battle_table_number = -1;
|
||||
other_c->card_battle_table_seat_number = 0;
|
||||
|
||||
send_self_leave_notification(other_c);
|
||||
string message = string_printf(
|
||||
"$C7Waiting to begin match in tournament\n$C6%s$C7...\n\n(Hold B+X+START to abort)",
|
||||
tourn->get_name().c_str());
|
||||
send_message_box(other_c, decode_sjis(message));
|
||||
}
|
||||
|
||||
uint32_t flags = Lobby::Flag::NON_V1_ONLY | Lobby::Flag::EPISODE_3_ONLY;
|
||||
auto game = create_game_generic(s, c, decode_sjis(tourn->get_name()), u"", 0xFF, 0, flags);
|
||||
game->tournament_match = match;
|
||||
for (auto game_c : game_clients) {
|
||||
send_command_t(game_c, 0xC9, 0x00, state_cmd);
|
||||
send_command_t(game_c, 0xC9, 0x00, ex_cmd);
|
||||
s->change_client_lobby(game_c, game, false);
|
||||
send_join_lobby(game_c, game);
|
||||
game_c->flags |= Client::Flag::LOADING;
|
||||
game->tournament_clients_to_add.clear();
|
||||
for (size_t z = 0; z < game_clients.size(); z++) {
|
||||
if (game_clients[z]) {
|
||||
game->tournament_clients_to_add.emplace(z, game_clients[z]);
|
||||
}
|
||||
}
|
||||
add_next_tournament_match_client(s, game);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1074,7 +1142,7 @@ static void on_tournament_bracket_updated(
|
||||
|
||||
if (tourn->get_state() == Episode3::Tournament::State::COMPLETE) {
|
||||
auto team = tourn->get_winner_team();
|
||||
if (team->player_serial_numbers.empty()) {
|
||||
if (!team->has_any_human_players()) {
|
||||
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());
|
||||
@@ -1426,21 +1494,14 @@ static void on_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Clie
|
||||
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");
|
||||
}
|
||||
string team_name = team->name.empty() ? "(Unnamed team)" : team->name;
|
||||
string message = string_printf("$C7(Unnamed team)\n%zuH/%zuC/%zu players\n%zu %s\n%s",
|
||||
team->num_human_players(),
|
||||
team->num_com_players(),
|
||||
team->max_players,
|
||||
team->num_rounds_cleared,
|
||||
team->num_rounds_cleared == 1 ? "win" : "wins",
|
||||
team->password.empty() ? "" : "Locked");
|
||||
send_ship_info(c, decode_sjis(message));
|
||||
} else {
|
||||
send_ship_info(c, u"$C7No such team");
|
||||
@@ -3083,12 +3144,17 @@ static void on_client_ready(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
send_get_player_info(c);
|
||||
}
|
||||
|
||||
// Handle initial commands for spectator teams
|
||||
auto watched_lobby = l->watched_lobby.lock();
|
||||
if (l->battle_player && (l->flags & Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY)) {
|
||||
l->battle_player->start();
|
||||
} else if (watched_lobby && watched_lobby->ep3_server_base) {
|
||||
watched_lobby->ep3_server_base->server->send_commands_for_joining_spectator(c->channel);
|
||||
}
|
||||
|
||||
// If this is a tournament match and not all players are present, try to bring
|
||||
// in the next player
|
||||
add_next_tournament_match_client(s, l);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+38
-55
@@ -2021,7 +2021,7 @@ void send_ep3_tournament_entry_list(
|
||||
entry.state = 2;
|
||||
} else if (team->name.empty()) {
|
||||
entry.state = 0;
|
||||
} else if (team->player_serial_numbers.size() < team->max_players) {
|
||||
} else if (team->players.size() < team->max_players) {
|
||||
entry.state = 1;
|
||||
} else {
|
||||
entry.state = 2;
|
||||
@@ -2093,19 +2093,19 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
|
||||
if (primary_lobby) {
|
||||
auto serial_number_to_client = primary_lobby->clients_by_serial_number();
|
||||
auto describe_team = [&](S_TournamentGameDetails_GC_Ep3_E3::TeamEntry& entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
|
||||
entry.team_name = team->name;
|
||||
size_t z = 0;
|
||||
for (uint32_t serial_number : team->player_serial_numbers) {
|
||||
auto c = serial_number_to_client.at(serial_number);
|
||||
auto& player_entry = entry.players[z++];
|
||||
player_entry.name = c->game_data.player()->disp.name;
|
||||
player_entry.description = ep3_description_for_client(c);
|
||||
}
|
||||
for (auto com_deck : team->com_decks) {
|
||||
auto& player_entry = entry.players[z++];
|
||||
player_entry.name = com_deck->player_name;
|
||||
player_entry.description = "Deck: " + com_deck->deck_name;
|
||||
auto describe_team = [&](S_TournamentGameDetails_GC_Ep3_E3::TeamEntry& team_entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
|
||||
team_entry.team_name = team->name;
|
||||
for (size_t z = 0; z < team->players.size(); z++) {
|
||||
auto& entry = team_entry.players[z];
|
||||
const auto& player = team->players[z];
|
||||
if (player.is_human()) {
|
||||
auto c = serial_number_to_client.at(player.serial_number);
|
||||
entry.name = c->game_data.player()->disp.name;
|
||||
entry.description = ep3_description_for_client(c);
|
||||
} else {
|
||||
entry.name = player.com_deck->player_name;
|
||||
entry.description = "Deck: " + player.com_deck->deck_name;
|
||||
}
|
||||
}
|
||||
};
|
||||
describe_team(cmd.team_entries[0], tourn_match->preceding_a->winner_team);
|
||||
@@ -2195,43 +2195,27 @@ void send_ep3_set_tournament_player_decks(
|
||||
|
||||
auto serial_number_to_client = l->clients_by_serial_number();
|
||||
|
||||
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;
|
||||
auto add_entries_for_team = [&](shared_ptr<const Episode3::Tournament::Team> team, size_t base_index) -> void {
|
||||
for (size_t z = 0; z < team->players.size(); z++) {
|
||||
auto& entry = cmd.entries[base_index + z];
|
||||
const auto& player = team->players[z];
|
||||
if (player.is_human()) {
|
||||
entry.type = 1; // Human
|
||||
entry.player_name = serial_number_to_client.at(player.serial_number)->game_data.player()->disp.name;
|
||||
if (player.serial_number == c->license->serial_number) {
|
||||
cmd.player_slot = base_index + z;
|
||||
}
|
||||
} else {
|
||||
entry.type = 2; // COM
|
||||
entry.player_name = player.com_deck->player_name;
|
||||
entry.deck_name = player.com_deck->deck_name;
|
||||
entry.card_ids = player.com_deck->card_ids;
|
||||
}
|
||||
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");
|
||||
}
|
||||
add_entries_for_team(match->preceding_a->winner_team, 0);
|
||||
add_entries_for_team(match->preceding_b->winner_team, 2);
|
||||
|
||||
if (!(tourn->get_data_index()->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
||||
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
@@ -2258,14 +2242,13 @@ void send_ep3_tournament_match_result(
|
||||
auto serial_number_to_client = l->clients_by_serial_number();
|
||||
|
||||
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++;
|
||||
for (size_t z = 0; z < team->players.size(); z++) {
|
||||
const auto& player = team->players[z];
|
||||
if (player.is_human()) {
|
||||
entry.player_names[z] = serial_number_to_client.at(player.serial_number)->game_data.player()->disp.name;
|
||||
} else {
|
||||
entry.player_names[z] = player.com_deck->player_name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+6
-3
@@ -121,15 +121,18 @@ 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 send_join_notification) {
|
||||
shared_ptr<Client> c,
|
||||
shared_ptr<Lobby> new_lobby,
|
||||
bool send_join_notification,
|
||||
ssize_t required_client_id) {
|
||||
uint8_t old_lobby_client_id = c->lobby_client_id;
|
||||
|
||||
shared_ptr<Lobby> current_lobby = this->find_lobby(c->lobby_id);
|
||||
try {
|
||||
if (current_lobby) {
|
||||
current_lobby->move_client_to_lobby(new_lobby, c);
|
||||
current_lobby->move_client_to_lobby(new_lobby, c, required_client_id);
|
||||
} else {
|
||||
new_lobby->add_client(c);
|
||||
new_lobby->add_client(c, required_client_id);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
return false;
|
||||
|
||||
+5
-2
@@ -118,8 +118,11 @@ 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, bool send_join_notification = true);
|
||||
bool change_client_lobby(
|
||||
std::shared_ptr<Client> c,
|
||||
std::shared_ptr<Lobby> new_lobby,
|
||||
bool send_join_notification = true,
|
||||
ssize_t required_client_id = -1);
|
||||
|
||||
void send_lobby_join_notifications(std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> joining_client);
|
||||
|
||||
Reference in New Issue
Block a user