keep tournament state consistent on clients

This commit is contained in:
Martin Michelsen
2023-09-15 22:09:31 -07:00
parent 5caa21bccb
commit 1d45c18ce8
11 changed files with 404 additions and 161 deletions
+158 -89
View File
@@ -10,8 +10,11 @@ using namespace std;
namespace Episode3 { namespace Episode3 {
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number) Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number)
: serial_number(serial_number), : serial_number(serial_number) {}
com_deck() {}
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
: serial_number(c->license->serial_number),
client(c) {}
Tournament::PlayerEntry::PlayerEntry( Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck) shared_ptr<const COMDeckDefinition> com_deck)
@@ -57,7 +60,7 @@ string Tournament::Team::str() const {
} }
void Tournament::Team::register_player( void Tournament::Team::register_player(
uint32_t serial_number, shared_ptr<Client> c,
const string& team_name, const string& team_name,
const string& password) { const string& password) {
if (this->players.size() >= this->max_players) { if (this->players.size() >= this->max_players) {
@@ -72,17 +75,17 @@ void Tournament::Team::register_player(
if (!tournament) { if (!tournament) {
throw runtime_error("tournament has been deleted"); throw runtime_error("tournament has been deleted");
} }
if (!tournament->all_player_serial_numbers.emplace(serial_number).second) { if (!tournament->all_player_serial_numbers.emplace(c->license->serial_number).second) {
throw runtime_error("player already registered in same tournament"); throw runtime_error("player already registered in same tournament");
} }
for (const auto& player : this->players) { for (const auto& player : this->players) {
if (player.is_human() && (player.serial_number == serial_number)) { if (player.is_human() && (player.serial_number == c->license->serial_number)) {
throw logic_error("player already registered in team but not in tournament"); throw logic_error("player already registered in team but not in tournament");
} }
} }
this->players.emplace_back(serial_number); this->players.emplace_back(c);
if (this->name.empty()) { if (this->name.empty()) {
this->name = team_name; this->name = team_name;
@@ -290,24 +293,23 @@ shared_ptr<Tournament::Team> Tournament::Match::opponent_team_for_team(
Tournament::Tournament( Tournament::Tournament(
shared_ptr<const MapIndex> map_index, shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index, shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const string& name, const string& name,
shared_ptr<const MapIndex::MapEntry> map, shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules, const Rules& rules,
size_t num_teams, size_t num_teams,
bool is_2v2, bool is_2v2,
bool has_com_teams) bool has_com_teams)
: log(string_printf("[Tournament/%02hhX] ", number)), : log(string_printf("[Tournament/%s] ", name.c_str())),
map_index(map_index), map_index(map_index),
com_deck_index(com_deck_index), com_deck_index(com_deck_index),
number(number),
name(name), name(name),
map(map), map(map),
rules(rules), rules(rules),
num_teams(num_teams), num_teams(num_teams),
is_2v2(is_2v2), is_2v2(is_2v2),
has_com_teams(has_com_teams), has_com_teams(has_com_teams),
current_state(State::REGISTRATION) { current_state(State::REGISTRATION),
menu_item_id(0xFFFFFFFF) {
if (this->num_teams < 4) { if (this->num_teams < 4) {
throw invalid_argument("team count must be 4 or more"); throw invalid_argument("team count must be 4 or more");
} }
@@ -322,13 +324,11 @@ Tournament::Tournament(
Tournament::Tournament( Tournament::Tournament(
shared_ptr<const MapIndex> map_index, shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index, shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const JSON& json) const JSON& json)
: log(string_printf("[Tournament/%02hhX] ", number)), : log(string_printf("[Tournament/%s] ", json.get_string("name").c_str())),
map_index(map_index), map_index(map_index),
com_deck_index(com_deck_index), com_deck_index(com_deck_index),
source_json(json), source_json(json),
number(number),
current_state(State::REGISTRATION) {} current_state(State::REGISTRATION) {}
void Tournament::init() { void Tournament::init() {
@@ -380,27 +380,27 @@ void Tournament::init() {
this->shared_from_this(), this->teams[this->zero_round_matches.size()])); this->shared_from_this(), this->teams[this->zero_round_matches.size()]));
} }
// 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(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
// Compute the match state from the teams' states // Compute the match state from the teams' states
if (is_registration_complete) { if (is_registration_complete) {
this->current_state = State::IN_PROGRESS; this->current_state = State::IN_PROGRESS;
// 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(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
// Start with all first-round matches in the match queue // Start with all first-round matches in the match queue
unordered_set<shared_ptr<Match>> match_queue; unordered_set<shared_ptr<Match>> match_queue;
for (auto match : this->zero_round_matches) { for (auto match : this->zero_round_matches) {
@@ -501,6 +501,9 @@ shared_ptr<Tournament::Team> Tournament::get_winner_team() const {
if (this->current_state != State::COMPLETE) { if (this->current_state != State::COMPLETE) {
return nullptr; return nullptr;
} }
if (!this->final_match) {
throw logic_error("tournament is complete but final match is missing");
}
if (!this->final_match->winner_team) { if (!this->final_match->winner_team) {
throw logic_error("tournament is complete but winner is not set"); throw logic_error("tournament is complete but winner is not set");
} }
@@ -604,6 +607,23 @@ void Tournament::start() {
} }
} }
void Tournament::send_all_state_updates(shared_ptr<ServerState> s) const {
for (const auto& team : this->teams) {
for (const auto& player : team->players) {
auto c = player.client.lock();
// Note: The last check here is to make sure the client is still linked
// with this instance of the tournament - an intervening shell command
// `reload ep3` could have changed the client's linkage
if (c &&
(c->flags & Client::Flag::IS_EPISODE_3) &&
!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) &&
(c->ep3_tournament_team.lock() == team)) {
send_ep3_confirm_tournament_entry(s, c, this->shared_from_this());
}
}
}
}
void Tournament::print_bracket(FILE* stream) const { 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 { 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++) { for (size_t z = 0; z < indent_level; z++) {
@@ -619,7 +639,7 @@ void Tournament::print_bracket(FILE* stream) const {
print_match(m->preceding_b, indent_level + 1); print_match(m->preceding_b, indent_level + 1);
} }
}; };
fprintf(stream, "Tournament %02hhX: %s\n", this->number, this->name.c_str()); fprintf(stream, "Tournament \"%s\"\n", this->name.c_str());
string map_name = this->map->map.name; string map_name = this->map->map.name;
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str()); fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str());
string rules_str = this->rules.str(); string rules_str = this->rules.str();
@@ -639,8 +659,10 @@ void Tournament::print_bracket(FILE* stream) const {
fprintf(stream, " State: UNKNOWN\n"); fprintf(stream, " State: UNKNOWN\n");
break; break;
} }
fprintf(stream, " Standings:\n"); if (this->final_match) {
print_match(this->final_match, 2); fprintf(stream, " Standings:\n");
print_match(this->final_match, 2);
}
fprintf(stream, " Pending matches:\n"); fprintf(stream, " Pending matches:\n");
for (const auto& match : this->pending_matches) { for (const auto& match : this->pending_matches) {
string match_str = match->str(); string match_str = match->str();
@@ -667,14 +689,38 @@ TournamentIndex::TournamentIndex(
json = JSON::list(); json = JSON::list();
} }
if (json.size() > 0x20) { if (json.is_list()) {
throw runtime_error("tournament JSON list length is incorrect"); if (json.size() > 0x20) {
} throw runtime_error("tournament JSON list length is incorrect");
for (size_t z = 0; z < min<size_t>(json.size(), 0x20); z++) {
if (!json.at(z).is_null()) {
this->tournaments[z].reset(new Tournament(this->map_index, this->com_deck_index, z, json.at(z)));
this->tournaments[z]->init();
} }
for (size_t z = 0; z < min<size_t>(json.size(), 0x20); z++) {
if (!json.at(z).is_null()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, json.at(z)));
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
throw runtime_error("multiple tournaments have the same name: " + tourn->get_name());
}
tourn->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(tourn);
}
}
} else if (json.is_dict()) {
if (json.size() > 0x20) {
throw runtime_error("tournament JSON dict length is incorrect");
}
for (const auto& it : json.as_dict()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, *it.second));
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
// This is logic_error instead of runtime_error because JSON dicts are
// supposed to already have unique keys
throw logic_error("multiple tournaments have the same name: " + tourn->get_name());
}
tourn->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(tourn);
}
} else {
throw runtime_error("tournament state root JSON is not a list or dict");
} }
} }
@@ -683,25 +729,11 @@ void TournamentIndex::save() const {
return; return;
} }
auto list = JSON::list(); auto json = JSON::dict();
for (size_t z = 0; z < 0x20; z++) { for (const auto& it : this->name_to_tournament) {
if (this->tournaments[z]) { json.emplace(it.second->get_name(), it.second->json());
list.emplace_back(this->tournaments[z]->json());
} else {
list.emplace_back(nullptr);
}
} }
save_file(this->state_filename, list.serialize(JSON::SerializeOption::FORMAT)); save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT));
}
vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
vector<shared_ptr<Tournament>> ret;
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z]) {
ret.emplace_back(this->tournaments[z]);
}
}
return ret;
} }
shared_ptr<Tournament> TournamentIndex::create_tournament( shared_ptr<Tournament> TournamentIndex::create_tournament(
@@ -711,48 +743,52 @@ shared_ptr<Tournament> TournamentIndex::create_tournament(
size_t num_teams, size_t num_teams,
bool is_2v2, bool is_2v2,
bool has_com_teams) { bool has_com_teams) {
// Find an unused tournament number if (this->name_to_tournament.size() >= 0x20) {
uint8_t number; throw runtime_error("there can be at most 32 tournaments at a time");
for (number = 0; number < 0x20; number++) {
if (!this->tournaments[number]) {
break;
}
}
if (number >= 0x20) {
throw runtime_error("all tournament slots are full");
} }
auto t = make_shared<Tournament>( auto t = make_shared<Tournament>(
this->map_index, this->com_deck_index, number, name, map, rules, num_teams, is_2v2, has_com_teams); this->map_index, this->com_deck_index, name, map, rules, num_teams, is_2v2, has_com_teams);
t->init(); t->init();
this->tournaments[number] = t; this->name_to_tournament.emplace(t->get_name(), t);
size_t z;
for (z = 0; z < this->menu_item_id_to_tournament.size(); z++) {
if (!this->menu_item_id_to_tournament[z]) {
t->set_menu_item_id(z);
this->menu_item_id_to_tournament[z] = t;
break;
}
}
if (z == this->menu_item_id_to_tournament.size()) {
t->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(t);
}
this->save();
return t; return t;
} }
void TournamentIndex::delete_tournament(uint8_t number) { bool TournamentIndex::delete_tournament(const string& name) {
this->tournaments[number].reset(); auto it = this->name_to_tournament.find(name);
} if (it == this->name_to_tournament.end()) {
return false;
shared_ptr<Tournament> TournamentIndex::get_tournament(uint8_t number) const { }
return this->tournaments[number]; for (size_t z = 0; z < this->menu_item_id_to_tournament.size(); z++) {
} if (this->menu_item_id_to_tournament[z] == it->second) {
this->menu_item_id_to_tournament[z] = nullptr;
shared_ptr<Tournament> TournamentIndex::get_tournament(const string& name) const { it->second->set_menu_item_id(0xFFFFFFFF);
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z] && (this->tournaments[z]->get_name() == name)) {
return this->tournaments[z];
} }
} }
return nullptr; this->name_to_tournament.erase(it);
this->save();
return true;
} }
shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number( shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(uint32_t serial_number) const {
uint32_t serial_number) const { for (const auto& it : this->name_to_tournament) {
for (size_t z = 0; z < 0x20; z++) { const auto& tourn = it.second;
if (!this->tournaments[z]) { auto team = tourn->team_for_serial_number(serial_number);
continue;
}
auto team = this->tournaments[z]->team_for_serial_number(serial_number);
if (team) { if (team) {
return team; return team;
} }
@@ -760,4 +796,37 @@ shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(
return nullptr; return nullptr;
} }
void TournamentIndex::link_client(shared_ptr<ServerState> s, shared_ptr<Client> c) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
return;
}
auto team = this->team_for_serial_number(c->license->serial_number);
auto tourn = team ? team->tournament.lock() : nullptr;
if (team && tourn) {
for (auto& player : team->players) {
if (player.serial_number == c->license->serial_number) {
c->ep3_tournament_team = team;
player.client = c;
if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
send_ep3_confirm_tournament_entry(s, c, tourn);
}
return;
}
}
throw logic_error("tournament team found for player, but player not found on team");
} else {
c->ep3_tournament_team.reset();
if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
send_ep3_confirm_tournament_entry(s, c, nullptr);
}
}
}
void TournamentIndex::link_all_clients(std::shared_ptr<ServerState> s) {
for (const auto& c_it : s->channel_to_client) {
this->link_client(s, c_it.second);
}
}
} // namespace Episode3 } // namespace Episode3
+41 -15
View File
@@ -13,6 +13,8 @@
#include "../Player.hh" #include "../Player.hh"
struct Lobby; struct Lobby;
struct Client;
struct ServerState;
namespace Episode3 { namespace Episode3 {
@@ -32,7 +34,11 @@ public:
uint32_t serial_number; uint32_t serial_number;
std::shared_ptr<const COMDeckDefinition> com_deck; std::shared_ptr<const COMDeckDefinition> com_deck;
// client is valid if serial_number is nonzero and the client is connected
std::weak_ptr<Client> client;
explicit PlayerEntry(uint32_t serial_number); explicit PlayerEntry(uint32_t serial_number);
explicit PlayerEntry(std::shared_ptr<Client> c);
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck); explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck);
bool is_com() const; bool is_com() const;
@@ -57,7 +63,7 @@ public:
std::string str() const; std::string str() const;
void register_player( void register_player(
uint32_t serial_number, std::shared_ptr<Client> c,
const std::string& team_name, const std::string& team_name,
const std::string& password); const std::string& password);
bool unregister_player(uint32_t serial_number); bool unregister_player(uint32_t serial_number);
@@ -94,7 +100,6 @@ public:
Tournament( Tournament(
std::shared_ptr<const MapIndex> map_index, std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index, std::shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const std::string& name, const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map, std::shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules, const Rules& rules,
@@ -104,16 +109,12 @@ public:
Tournament( Tournament(
std::shared_ptr<const MapIndex> map_index, std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index, std::shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const JSON& json); const JSON& json);
~Tournament() = default; ~Tournament() = default;
void init(); void init();
JSON json() const; JSON json() const;
inline uint8_t get_number() const {
return this->number;
}
inline const std::string& get_name() const { inline const std::string& get_name() const {
return this->name; return this->name;
} }
@@ -135,9 +136,15 @@ public:
inline const std::vector<std::shared_ptr<Team>>& all_teams() const { inline const std::vector<std::shared_ptr<Team>>& all_teams() const {
return this->teams; return this->teams;
} }
std::shared_ptr<Team> get_team(size_t index) const { inline std::shared_ptr<Team> get_team(size_t index) const {
return this->teams.at(index); return this->teams.at(index);
} }
inline uint32_t get_menu_item_id() const {
return this->menu_item_id;
}
inline void set_menu_item_id(uint32_t menu_item_id) {
this->menu_item_id = menu_item_id;
}
std::shared_ptr<Team> get_winner_team() const; std::shared_ptr<Team> get_winner_team() const;
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const; std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
@@ -147,6 +154,8 @@ public:
void start(); void start();
void send_all_state_updates(std::shared_ptr<ServerState> s) const;
void print_bracket(FILE* stream) const; void print_bracket(FILE* stream) const;
private: private:
@@ -155,7 +164,6 @@ private:
std::shared_ptr<const MapIndex> map_index; std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index; std::shared_ptr<const COMDeckIndex> com_deck_index;
JSON source_json; JSON source_json;
uint8_t number;
std::string name; std::string name;
std::shared_ptr<const MapIndex::MapEntry> map; std::shared_ptr<const MapIndex::MapEntry> map;
Rules rules; Rules rules;
@@ -163,6 +171,7 @@ private:
bool is_2v2; bool is_2v2;
bool has_com_teams; bool has_com_teams;
State current_state; State current_state;
uint32_t menu_item_id;
std::set<uint32_t> all_player_serial_numbers; std::set<uint32_t> all_player_serial_numbers;
std::unordered_set<std::shared_ptr<Match>> pending_matches; std::unordered_set<std::shared_ptr<Match>> pending_matches;
@@ -191,7 +200,23 @@ public:
void save() const; void save() const;
std::vector<std::shared_ptr<Tournament>> all_tournaments() const; inline const std::unordered_map<std::string, std::shared_ptr<Tournament>>& all_tournaments() const {
return this->name_to_tournament;
}
inline std::shared_ptr<Tournament> get_tournament(uint32_t menu_item_id) const {
try {
return this->menu_item_id_to_tournament.at(menu_item_id);
} catch (const std::out_of_range&) {
return nullptr;
}
}
inline std::shared_ptr<Tournament> get_tournament(const std::string& name) const {
try {
return this->name_to_tournament.at(name);
} catch (const std::out_of_range&) {
return nullptr;
}
}
std::shared_ptr<Tournament> create_tournament( std::shared_ptr<Tournament> create_tournament(
const std::string& name, const std::string& name,
@@ -200,18 +225,19 @@ public:
size_t num_teams, size_t num_teams,
bool is_2v2, bool is_2v2,
bool has_com_teams); bool has_com_teams);
void delete_tournament(uint8_t number); bool delete_tournament(const std::string& name);
std::shared_ptr<Tournament> get_tournament(uint8_t number) const;
std::shared_ptr<Tournament> get_tournament(const std::string& name) const;
std::shared_ptr<Tournament::Team> team_for_serial_number( std::shared_ptr<Tournament::Team> team_for_serial_number(uint32_t serial_number) const;
uint32_t serial_number) const;
void link_client(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void link_all_clients(std::shared_ptr<ServerState> s);
private: private:
std::shared_ptr<const MapIndex> map_index; std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index; std::shared_ptr<const COMDeckIndex> com_deck_index;
std::string state_filename; std::string state_filename;
std::shared_ptr<Tournament> tournaments[0x20]; std::unordered_map<std::string, std::shared_ptr<Tournament>> name_to_tournament;
std::vector<std::shared_ptr<Tournament>> menu_item_id_to_tournament;
}; };
} // namespace Episode3 } // namespace Episode3
+1
View File
@@ -1655,6 +1655,7 @@ int main(int argc, char** argv) {
shared_ptr<struct event_base> base(event_base_new(), event_base_free); shared_ptr<struct event_base> base(event_base_new(), event_base_free);
shared_ptr<ServerState> state(new ServerState(config_filename, is_replay)); shared_ptr<ServerState> state(new ServerState(config_filename, is_replay));
state->init();
shared_ptr<DNSServer> dns_server; shared_ptr<DNSServer> dns_server;
if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) { if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) {
+7 -30
View File
@@ -231,9 +231,7 @@ static void send_main_menu(shared_ptr<ServerState> s, shared_ptr<Client> c) {
void on_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) { void on_login_complete(shared_ptr<ServerState> s, shared_ptr<Client> c) {
if (c->flags & Client::Flag::IS_EPISODE_3) { if (c->flags & Client::Flag::IS_EPISODE_3) {
auto team = s->ep3_tournament_index->team_for_serial_number(c->license->serial_number); s->ep3_tournament_index->link_client(s, c);
auto tourn = team ? team->tournament.lock() : nullptr;
c->ep3_tournament_team = team;
} }
// On the BB data server, this function is called only on the last connection // On the BB data server, this function is called only on the last connection
@@ -1227,25 +1225,7 @@ static void on_DC_Ep3(shared_ptr<ServerState> s, shared_ptr<Client> c,
static void on_tournament_bracket_updated( static void on_tournament_bracket_updated(
shared_ptr<ServerState> s, shared_ptr<const Episode3::Tournament> tourn) { shared_ptr<ServerState> s, shared_ptr<const Episode3::Tournament> tourn) {
const auto& serial_numbers = tourn->get_all_player_serial_numbers(); tourn->send_all_state_updates(s);
for (const auto& l : s->all_lobbies()) {
for (const auto& c : l->clients) {
if (!c ||
!c->license ||
!serial_numbers.count(c->license->serial_number) ||
c->ep3_tournament_team.expired() ||
(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
continue;
}
send_ep3_confirm_tournament_entry(s, c, tourn);
}
}
if (tourn && (tourn->get_state() == Episode3::Tournament::State::COMPLETE)) {
s->ep3_tournament_index->delete_tournament(tourn->get_number());
}
if (tourn->get_state() == Episode3::Tournament::State::COMPLETE) { if (tourn->get_state() == Episode3::Tournament::State::COMPLETE) {
auto team = tourn->get_winner_team(); auto team = tourn->get_winner_team();
if (!team->has_any_human_players()) { if (!team->has_any_human_players()) {
@@ -1253,10 +1233,10 @@ static void on_tournament_bracket_updated(
} else { } 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()); send_ep3_text_message_printf(s, "$C6%s$C7\nwon the tournament\n$C6%s", team->name.c_str(), tourn->get_name().c_str());
} }
s->ep3_tournament_index->delete_tournament(tourn->get_number()); s->ep3_tournament_index->delete_tournament(tourn->get_name());
} else {
s->ep3_tournament_index->save();
} }
s->ep3_tournament_index->save();
} }
static void on_CA_Ep3(shared_ptr<ServerState> s, shared_ptr<Client> c, static void on_CA_Ep3(shared_ptr<ServerState> s, shared_ptr<Client> c,
@@ -2060,12 +2040,9 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
auto team = tourn->get_team(team_index); auto team = tourn->get_team(team_index);
if (team) { if (team) {
try { try {
team->register_player( team->register_player(c, encode_sjis(team_name), encode_sjis(password));
c->license->serial_number,
encode_sjis(team_name),
encode_sjis(password));
c->ep3_tournament_team = team; c->ep3_tournament_team = team;
send_ep3_confirm_tournament_entry(s, c, tourn); tourn->send_all_state_updates(s);
string message = string_printf("$C7You are registered in $C6%s$C7.\n\ string message = string_printf("$C7You are registered in $C6%s$C7.\n\
\n\ \n\
After registration ends, start your matches by\n\ After registration ends, start your matches by\n\
+6 -5
View File
@@ -2285,7 +2285,8 @@ void send_ep3_tournament_list(
bool is_for_spectator_team_create) { bool is_for_spectator_team_create) {
S_TournamentList_GC_Ep3_E0 cmd; S_TournamentList_GC_Ep3_E0 cmd;
size_t z = 0; size_t z = 0;
for (const auto& tourn : s->ep3_tournament_index->all_tournaments()) { for (const auto& it : s->ep3_tournament_index->all_tournaments()) {
const auto& tourn = it.second;
if (z >= 0x20) { if (z >= 0x20) {
throw logic_error("more than 32 tournaments exist"); throw logic_error("more than 32 tournaments exist");
} }
@@ -2293,7 +2294,7 @@ void send_ep3_tournament_list(
entry.menu_id = is_for_spectator_team_create entry.menu_id = is_for_spectator_team_create
? MenuID::TOURNAMENTS_FOR_SPEC ? MenuID::TOURNAMENTS_FOR_SPEC
: MenuID::TOURNAMENTS; : MenuID::TOURNAMENTS;
entry.item_id = tourn->get_number(); entry.item_id = tourn->get_menu_item_id();
// TODO: What does it mean for a tournament to be locked? Should we support // TODO: What does it mean for a tournament to be locked? Should we support
// that? // that?
// TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is // TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is
@@ -2333,7 +2334,7 @@ void send_ep3_tournament_entry_list(
} }
auto& entry = cmd.entries[z]; auto& entry = cmd.entries[z];
entry.menu_id = MenuID::TOURNAMENT_ENTRIES; entry.menu_id = MenuID::TOURNAMENT_ENTRIES;
entry.item_id = (tourn->get_number() << 16) | z; entry.item_id = (tourn->get_menu_item_id() << 16) | z;
entry.unknown_a2 = team->num_rounds_cleared; entry.unknown_a2 = team->num_rounds_cleared;
entry.locked = team->password.empty() ? 0 : 1; entry.locked = team->password.empty() ? 0 : 1;
if (tourn->get_state() != Episode3::Tournament::State::REGISTRATION) { if (tourn->get_state() != Episode3::Tournament::State::REGISTRATION) {
@@ -2597,8 +2598,8 @@ void send_ep3_tournament_match_result(
send_command_t(l, 0xC9, 0x00, cmd); send_command_t(l, 0xC9, 0x00, cmd);
if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) {
send_text_message_printf(l, "$C5TOURN/%02hhX/%zu WIN %c", send_text_message_printf(l, "$C5TOURN/%" PRIX32 "/%zu WIN %c",
tourn->get_number(), match->round_num, tourn->get_menu_item_id(), match->round_num,
match->winner_team == match->preceding_a->winner_team ? 'A' : 'B'); match->winner_team == match->preceding_a->winner_team ? 'A' : 'B');
} }
} }
+9 -9
View File
@@ -38,7 +38,7 @@ void Server::disconnect_client(shared_ptr<Client> c) {
c->id, bufferevent_getfd(c->channel.bev.get())); c->id, bufferevent_getfd(c->channel.bev.get()));
} }
this->channel_to_client.erase(&c->channel); this->state->channel_to_client.erase(&c->channel);
c->channel.disconnect(); c->channel.disconnect();
try { try {
@@ -118,7 +118,7 @@ void Server::on_listen_accept(struct evconnlistener* listener,
c->channel.on_command_received = Server::on_client_input; c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error; c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this; c->channel.context_obj = this;
this->channel_to_client.emplace(&c->channel, c); this->state->channel_to_client.emplace(&c->channel, c);
server_log.info("Client connected: C-%" PRIX64 " on fd %d via %d (%s)", server_log.info("Client connected: C-%" PRIX64 " on fd %d via %d (%s)",
c->id, fd, listen_fd, listening_socket->addr_str.c_str()); c->id, fd, listen_fd, listening_socket->addr_str.c_str());
@@ -148,7 +148,7 @@ void Server::connect_client(
name_for_version(version), name_for_version(version),
name_for_server_behavior(initial_state)); name_for_server_behavior(initial_state));
this->channel_to_client.emplace(&c->channel, c); this->state->channel_to_client.emplace(&c->channel, c);
// Manually set the remote address, since the bufferevent has no fd and the // Manually set the remote address, since the bufferevent has no fd and the
// Channel constructor can't figure out the virtual remote address // Channel constructor can't figure out the virtual remote address
@@ -174,7 +174,7 @@ void Server::on_listen_error(struct evconnlistener* listener) {
void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) { void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
Server* server = reinterpret_cast<Server*>(ch.context_obj); Server* server = reinterpret_cast<Server*>(ch.context_obj);
shared_ptr<Client> c = server->channel_to_client.at(&ch); shared_ptr<Client> c = server->state->channel_to_client.at(&ch);
if (c->should_disconnect) { if (c->should_disconnect) {
server->disconnect_client(c); server->disconnect_client(c);
@@ -197,7 +197,7 @@ void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::
void Server::on_client_error(Channel& ch, short events) { void Server::on_client_error(Channel& ch, short events) {
Server* server = reinterpret_cast<Server*>(ch.context_obj); Server* server = reinterpret_cast<Server*>(ch.context_obj);
shared_ptr<Client> c = server->channel_to_client.at(&ch); shared_ptr<Client> c = server->state->channel_to_client.at(&ch);
if (events & BEV_EVENT_ERROR) { if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR(); int err = EVUTIL_SOCKET_ERROR();
@@ -271,13 +271,13 @@ void Server::add_socket(
} }
shared_ptr<Client> Server::get_client() const { shared_ptr<Client> Server::get_client() const {
if (this->channel_to_client.empty()) { if (this->state->channel_to_client.empty()) {
throw runtime_error("no clients on game server"); throw runtime_error("no clients on game server");
} }
if (this->channel_to_client.size() > 1) { if (this->state->channel_to_client.size() > 1) {
throw runtime_error("multiple clients on game server"); throw runtime_error("multiple clients on game server");
} }
return this->channel_to_client.begin()->second; return this->state->channel_to_client.begin()->second;
} }
vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident) const { vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident) const {
@@ -296,7 +296,7 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
// TODO: It's kind of not great that we do a linear search here, but this is // TODO: It's kind of not great that we do a linear search here, but this is
// only used in the shell, so it should be pretty rare. // only used in the shell, so it should be pretty rare.
vector<shared_ptr<Client>> results; vector<shared_ptr<Client>> results;
for (const auto& it : this->channel_to_client) { for (const auto& it : this->state->channel_to_client) {
auto c = it.second; auto c = it.second;
if (c->license && c->license->serial_number == serial_number_dec) { if (c->license && c->license->serial_number == serial_number_dec) {
results.emplace_back(std::move(c)); results.emplace_back(std::move(c));
-1
View File
@@ -52,7 +52,6 @@ private:
ServerBehavior behavior); ServerBehavior behavior);
}; };
std::unordered_map<int, ListeningSocket> listening_sockets; std::unordered_map<int, ListeningSocket> listening_sockets;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::unordered_set<std::shared_ptr<Client>> clients_to_destroy; std::unordered_set<std::shared_ptr<Client>> clients_to_destroy;
std::shared_ptr<ServerState> state; std::shared_ptr<ServerState> state;
+7 -10
View File
@@ -182,7 +182,7 @@ Server commands:\n\
start-tournament TOURNAMENT-NAME\n\ start-tournament TOURNAMENT-NAME\n\
End registration for a tournament and allow matches to begin. Quotes are\n\ End registration for a tournament and allow matches to begin. Quotes are\n\
required around the tournament name unless the name contains no spaces.\n\ required around the tournament name unless the name contains no spaces.\n\
tournament-state TOURNAMENT-NAME\n\ describe-tournament TOURNAMENT-NAME\n\
Show the current state of a tournament. Quotes are required around the\n\ Show the current state of a tournament. Quotes are required around the\n\
tournament name unless the name contains no spaces.\n\ tournament name unless the name contains no spaces.\n\
\n\ \n\
@@ -540,23 +540,19 @@ Proxy session commands:\n\
} }
auto tourn = this->state->ep3_tournament_index->create_tournament( auto tourn = this->state->ep3_tournament_index->create_tournament(
name, map, rules, num_teams, is_2v2, has_com_teams); name, map, rules, num_teams, is_2v2, has_com_teams);
this->state->ep3_tournament_index->save(); fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str());
fprintf(stderr, "created tournament %02hhX\n", tourn->get_number());
} else if (command_name == "delete-tournament") { } else if (command_name == "delete-tournament") {
string name = get_quoted_string(command_args); string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name); if (this->state->ep3_tournament_index->delete_tournament(name)) {
if (tourn) {
this->state->ep3_tournament_index->delete_tournament(tourn->get_number());
this->state->ep3_tournament_index->save();
fprintf(stderr, "tournament deleted\n"); fprintf(stderr, "tournament deleted\n");
} else { } else {
fprintf(stderr, "no such tournament exists\n"); fprintf(stderr, "no such tournament exists\n");
} }
} else if (command_name == "list-tournaments") { } else if (command_name == "list-tournaments") {
for (const auto& tourn : this->state->ep3_tournament_index->all_tournaments()) { for (const auto& it : this->state->ep3_tournament_index->all_tournaments()) {
fprintf(stderr, " %s\n", tourn->get_name().c_str()); fprintf(stderr, " %s\n", it.second->get_name().c_str());
} }
} else if (command_name == "start-tournament") { } else if (command_name == "start-tournament") {
@@ -565,13 +561,14 @@ Proxy session commands:\n\
if (tourn) { if (tourn) {
tourn->start(); tourn->start();
this->state->ep3_tournament_index->save(); this->state->ep3_tournament_index->save();
tourn->send_all_state_updates(this->state);
send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str()); send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str());
fprintf(stderr, "tournament started\n"); fprintf(stderr, "tournament started\n");
} else { } else {
fprintf(stderr, "no such tournament exists\n"); fprintf(stderr, "no such tournament exists\n");
} }
} else if (command_name == "tournament-status") { } else if (command_name == "describe-tournament") {
string name = get_quoted_string(command_args); string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name); auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) { if (tourn) {
+4 -1
View File
@@ -41,7 +41,9 @@ ServerState::ServerState(const char* config_filename, bool is_replay)
local_address(0), local_address(0),
external_address(0), external_address(0),
proxy_allow_save_files(true), proxy_allow_save_files(true),
proxy_enable_login_options(false) { proxy_enable_login_options(false) {}
void ServerState::init() {
vector<shared_ptr<Lobby>> non_v1_only_lobbies; vector<shared_ptr<Lobby>> non_v1_only_lobbies;
vector<shared_ptr<Lobby>> ep3_only_lobbies; vector<shared_ptr<Lobby>> ep3_only_lobbies;
@@ -875,6 +877,7 @@ void ServerState::load_ep3_data() {
const string& tournament_state_filename = "system/ep3/tournament-state.json"; const string& tournament_state_filename = "system/ep3/tournament-state.json";
this->ep3_tournament_index.reset(new Episode3::TournamentIndex( this->ep3_tournament_index.reset(new Episode3::TournamentIndex(
this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename)); this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename));
this->ep3_tournament_index->link_all_clients(this->shared_from_this());
config_log.info("Loaded Episode 3 tournament state"); config_log.info("Loaded Episode 3 tournament state");
} }
+7 -1
View File
@@ -33,7 +33,7 @@ struct PortConfiguration {
ServerBehavior behavior; ServerBehavior behavior;
}; };
struct ServerState { struct ServerState : public std::enable_shared_from_this<ServerState> {
enum class RunShellBehavior { enum class RunShellBehavior {
DEFAULT = 0, DEFAULT = 0,
ALWAYS, ALWAYS,
@@ -129,6 +129,7 @@ struct ServerState {
std::u16string pc_patch_server_message; std::u16string pc_patch_server_message;
std::u16string bb_patch_server_message; std::u16string bb_patch_server_message;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby; std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_v1; std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_v1;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_non_v1; std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_non_v1;
@@ -148,6 +149,11 @@ struct ServerState {
std::shared_ptr<Server> game_server; std::shared_ptr<Server> game_server;
ServerState(const char* config_filename, bool is_replay); ServerState(const char* config_filename, bool is_replay);
ServerState(const ServerState&) = delete;
ServerState(ServerState&&) = delete;
ServerState& operator=(const ServerState&) = delete;
ServerState& operator=(ServerState&&) = delete;
void init();
void add_client_to_available_lobby(std::shared_ptr<Client> c); void add_client_to_available_lobby(std::shared_ptr<Client> c);
void remove_client_from_lobby(std::shared_ptr<Client> c); void remove_client_from_lobby(std::shared_ptr<Client> c);
+164
View File
@@ -61,6 +61,88 @@ I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=04 f
0010 | 0E 89 2A 49 0A 03 02 00 30 45 53 33 00 00 00 00 | *I 0ES3 0010 | 0E 89 2A 49 0A 03 02 00 30 45 53 33 00 00 00 00 | *I 0ES3
0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF |
I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=B7 flag=00) I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=B7 flag=00)
0000 | CC 00 0C 05 00 00 00 00 00 00 00 00 00 00 00 00 |
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0100 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0120 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0130 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0140 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0200 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0210 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0220 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0230 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0240 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0250 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0260 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0270 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0280 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0290 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0300 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0310 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0320 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0330 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0340 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0350 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0360 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0370 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0380 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0390 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0400 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0410 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0420 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0430 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0440 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0450 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0460 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0470 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0480 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0490 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0500 | 00 00 00 00 00 00 00 00 00 00 00 00 |
I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=B7 flag=00)
0000 | B7 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0000 | B7 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0010 | 00 00 00 00 40 42 0F 00 40 42 0F 00 FF FF FF FF | 0010 | 00 00 00 00 40 42 0F 00 40 42 0F 00 FF FF FF FF |
I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=95 flag=00) I 32209 2023-09-08 23:38:59 - [Commands] Sending to C-6 (version=GC command=95 flag=00)
@@ -2705,6 +2787,88 @@ I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=04 f
0000 | 04 00 2C 00 00 00 01 00 11 11 11 11 39 98 AC 82 | , 9 0000 | 04 00 2C 00 00 00 01 00 11 11 11 11 39 98 AC 82 | , 9
0010 | 0E 89 2A 49 0A 44 82 00 30 45 53 33 00 00 00 00 | *I D 0ES3 0010 | 0E 89 2A 49 0A 44 82 00 30 45 53 33 00 00 00 00 | *I D 0ES3
0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF |
I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=04 flag=00)
0000 | CC 00 0C 05 00 00 00 00 00 00 00 00 00 00 00 00 |
0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0100 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0120 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0130 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0140 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0150 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0160 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0170 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0190 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
01F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0200 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0210 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0220 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0230 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0240 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0250 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0260 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0270 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0280 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0290 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
02F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0300 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0310 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0320 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0330 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0340 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0350 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0360 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0370 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0380 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0390 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
03F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0400 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0410 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0420 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0430 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0440 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0450 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0460 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0470 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0480 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0490 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04A0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
04F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
0500 | 00 00 00 00 00 00 00 00 00 00 00 00 |
I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=83 flag=14) I 32209 2023-09-08 23:39:02 - [Commands] Sending to C-7 (version=GC command=83 flag=14)
0000 | 83 14 F4 00 33 00 00 33 01 00 00 00 00 00 00 00 | 3 3 0000 | 83 14 F4 00 33 00 00 33 01 00 00 00 00 00 00 00 | 3 3
0010 | 33 00 00 33 02 00 00 00 00 00 00 00 33 00 00 33 | 3 3 3 3 0010 | 33 00 00 33 02 00 00 00 00 00 00 00 33 00 00 33 | 3 3 3 3