set up test harness for ep3 server log replays
This commit is contained in:
+85
-82
@@ -3,6 +3,7 @@
|
|||||||
#include <phosg/Random.hh>
|
#include <phosg/Random.hh>
|
||||||
#include <phosg/Time.hh>
|
#include <phosg/Time.hh>
|
||||||
|
|
||||||
|
#include "../Loggers.hh"
|
||||||
#include "../SendCommands.hh"
|
#include "../SendCommands.hh"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -26,6 +27,7 @@ void Server::PresenceEntry::clear() {
|
|||||||
|
|
||||||
Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
||||||
: lobby(lobby),
|
: lobby(lobby),
|
||||||
|
has_lobby(lobby != nullptr),
|
||||||
options(std::move(options)),
|
options(std::move(options)),
|
||||||
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
|
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
|
||||||
tournament_match_result_sent(false),
|
tournament_match_result_sent(false),
|
||||||
@@ -64,7 +66,11 @@ Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
|||||||
has_done_pb(0),
|
has_done_pb(0),
|
||||||
num_6xB4x06_commands_sent(0),
|
num_6xB4x06_commands_sent(0),
|
||||||
prev_num_6xB4x06_commands_sent(0) {
|
prev_num_6xB4x06_commands_sent(0) {
|
||||||
new StackLogger(this, lobby->log.prefix + "[Ep3::Server] ", lobby->log.min_level);
|
if (this->has_lobby) {
|
||||||
|
new StackLogger(this, lobby->log.prefix + "[Ep3::Server] ", lobby->log.min_level);
|
||||||
|
} else {
|
||||||
|
new StackLogger(this, "[Ep3::Server] ", lobby_log.min_level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Server::~Server() noexcept(false) {
|
Server::~Server() noexcept(false) {
|
||||||
@@ -187,52 +193,50 @@ int8_t Server::get_winner_team_id() const {
|
|||||||
return -1; // No team has won (yet)
|
return -1; // No team has won (yet)
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::send(const void* data, size_t size) const {
|
void Server::send(const void* data, size_t size, uint8_t command, bool enable_masking) const {
|
||||||
// Note: This function is (obviously) not part of the original implementation.
|
// Note: This function is (obviously) not part of the original implementation.
|
||||||
auto l = this->lobby.lock();
|
if (this->has_lobby) {
|
||||||
if (!l) {
|
auto l = this->lobby.lock();
|
||||||
throw runtime_error("lobby is deleted");
|
if (!l) {
|
||||||
}
|
throw runtime_error("lobby is deleted");
|
||||||
|
}
|
||||||
|
|
||||||
string masked_data;
|
string masked_data;
|
||||||
if (!(this->options.behavior_flags & BehaviorFlag::DISABLE_MASKING)) {
|
if (enable_masking &&
|
||||||
if (size >= 8) {
|
!(this->options.behavior_flags & BehaviorFlag::DISABLE_MASKING) &&
|
||||||
|
(size >= 8)) {
|
||||||
masked_data.assign(reinterpret_cast<const char*>(data), size);
|
masked_data.assign(reinterpret_cast<const char*>(data), size);
|
||||||
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
|
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
|
||||||
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
|
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
|
||||||
data = masked_data.data();
|
data = masked_data.data();
|
||||||
size = masked_data.size();
|
size = masked_data.size();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Sega's servers sent battle commands with the 60 command. The handlers
|
// Note: Sega's servers sent battle commands with the 60 command. The handlers
|
||||||
// for 60, 62, and C9 on the client are identical, so we choose to use C9
|
// for 60, 62, and C9 on the client are identical, so we choose to use C9
|
||||||
// instead because it's unique to Episode 3, and therefore seems more
|
// instead because it's unique to Episode 3, and therefore seems more
|
||||||
// appropriate to convey battle commands.
|
// appropriate to convey battle commands.
|
||||||
send_command(l, 0xC9, 0x00, data, size);
|
send_command(l, command, 0x00, data, size);
|
||||||
for (auto watcher_l : l->watcher_lobbies) {
|
for (auto watcher_l : l->watcher_lobbies) {
|
||||||
send_command_if_not_loading(watcher_l, 0xC9, 0x00, data, size);
|
send_command_if_not_loading(watcher_l, command, 0x00, data, size);
|
||||||
}
|
}
|
||||||
if (l->battle_record && l->battle_record->writable()) {
|
if (l->battle_record && l->battle_record->writable()) {
|
||||||
l->battle_record->add_command(
|
l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||||
BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
}
|
||||||
|
|
||||||
|
} else if (this->log().info("Generated command")) {
|
||||||
|
print_data(stderr, data, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::send_6xB4x46() const {
|
void Server::send_6xB4x46() const {
|
||||||
// Note: This function is not part of the original implementation; it was
|
// Note: This function is not part of the original implementation; it was
|
||||||
// factored out from its callsites in this file and the strings were changed.
|
// factored out from its callsites in this file and the strings were changed.
|
||||||
auto l = this->lobby.lock();
|
|
||||||
if (!l) {
|
|
||||||
throw runtime_error("lobby is deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46;
|
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46;
|
||||||
cmd46.version_signature.encode(VERSION_SIGNATURE, 1);
|
cmd46.version_signature.encode(VERSION_SIGNATURE, 1);
|
||||||
cmd46.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1);
|
cmd46.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1);
|
||||||
string date_str2 = string_printf(
|
string date_str2 = string_printf(
|
||||||
"Lobby:%08" PRIX32 " Random:%08" PRIX32 "+%08" PRIX32,
|
"Random:%08" PRIX32 "+%08" PRIX32,
|
||||||
l->lobby_id,
|
|
||||||
this->options.random_crypt->seed(),
|
this->options.random_crypt->seed(),
|
||||||
this->options.random_crypt->absolute_offset());
|
this->options.random_crypt->absolute_offset());
|
||||||
if (this->last_chosen_map) {
|
if (this->last_chosen_map) {
|
||||||
@@ -2110,17 +2114,15 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
|||||||
this->battle_in_progress = false;
|
this->battle_in_progress = false;
|
||||||
} else {
|
} else {
|
||||||
auto l = this->lobby.lock();
|
auto l = this->lobby.lock();
|
||||||
if (!l) {
|
if (l) {
|
||||||
throw runtime_error("lobby is deleted");
|
if (l->battle_record) {
|
||||||
}
|
l->battle_record->set_battle_start_timestamp();
|
||||||
if (l->battle_record) {
|
}
|
||||||
l->battle_record->set_battle_start_timestamp();
|
// Note: Sega's implementation doesn't set EX results values here; they
|
||||||
}
|
// did it at game join time instead. We do it here for code simplicity.
|
||||||
|
if (l->ep3_ex_result_values) {
|
||||||
// Note: Sega's implementation doesn't set EX results values here; they
|
this->send(*l->ep3_ex_result_values);
|
||||||
// did it at game join time instead. We do it here for code simplicity.
|
}
|
||||||
if (l->ep3_ex_result_values) {
|
|
||||||
this->send(*l->ep3_ex_result_values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->setup_and_start_battle();
|
this->setup_and_start_battle();
|
||||||
@@ -2327,65 +2329,66 @@ void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const st
|
|||||||
throw runtime_error("lobby is deleted");
|
throw runtime_error("lobby is deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& list_data = this->options.map_index->get_compressed_list(l->count_clients(), sender_c->language());
|
size_t num_players = l ? l->count_clients() : 1;
|
||||||
|
uint8_t language = sender_c ? sender_c->language() : 1;
|
||||||
|
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language);
|
||||||
|
|
||||||
StringWriter w;
|
StringWriter w;
|
||||||
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
|
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
|
||||||
w.put<G_MapList_GC_Ep3_6xB6x40>(
|
w.put<G_MapList_GC_Ep3_6xB6x40>(
|
||||||
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
|
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
|
||||||
w.write(list_data);
|
w.write(list_data);
|
||||||
send_command(l, 0x6C, 0x00, w.str());
|
while (w.size() & 3) {
|
||||||
for (auto watcher_l : l->watcher_lobbies) {
|
w.put_u8(0);
|
||||||
send_command_if_not_loading(watcher_l, 0x6C, 0x00, w.str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l->battle_record && l->battle_record->writable()) {
|
const auto& out_data = w.str();
|
||||||
l->battle_record->add_command(
|
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
||||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(w.str()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::send_6xB6x41_to_all_clients() const {
|
void Server::send_6xB6x41_to_all_clients() const {
|
||||||
auto l = this->lobby.lock();
|
auto l = this->lobby.lock();
|
||||||
if (!l) {
|
if (l) {
|
||||||
throw runtime_error("lobby is deleted");
|
vector<string> map_commands_by_language;
|
||||||
}
|
auto send_to_client = [&](shared_ptr<Client> c) -> void {
|
||||||
|
if (!c) {
|
||||||
vector<string> map_commands_by_language;
|
return;
|
||||||
auto send_to_client = [&](shared_ptr<Client> c) -> void {
|
}
|
||||||
if (!c) {
|
if (map_commands_by_language.size() <= c->language()) {
|
||||||
return;
|
map_commands_by_language.resize(c->language() + 1);
|
||||||
}
|
}
|
||||||
if (map_commands_by_language.size() <= c->language()) {
|
if (map_commands_by_language[c->language()].empty()) {
|
||||||
map_commands_by_language.resize(c->language() + 1);
|
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
|
||||||
}
|
this->last_chosen_map, c->language(), (l->base_version == Version::GC_EP3_NTE));
|
||||||
if (map_commands_by_language[c->language()].empty()) {
|
}
|
||||||
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
|
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(c->language()), this->last_chosen_map->map_number);
|
||||||
this->last_chosen_map, c->language(), (l->base_version == Version::GC_EP3_NTE));
|
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
|
||||||
}
|
};
|
||||||
this->log().info("Sending %c version of map %08" PRIX32, char_for_language_code(c->language()), this->last_chosen_map->map_number);
|
for (const auto& c : l->clients) {
|
||||||
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
|
|
||||||
};
|
|
||||||
for (const auto& c : l->clients) {
|
|
||||||
send_to_client(c);
|
|
||||||
}
|
|
||||||
for (auto watcher_l : l->watcher_lobbies) {
|
|
||||||
for (const auto& c : watcher_l->clients) {
|
|
||||||
send_to_client(c);
|
send_to_client(c);
|
||||||
}
|
}
|
||||||
}
|
for (auto watcher_l : l->watcher_lobbies) {
|
||||||
|
for (const auto& c : watcher_l->clients) {
|
||||||
if (l->battle_record && l->battle_record->writable()) {
|
send_to_client(c);
|
||||||
// TODO: It's not great that we just pick the first one; ideally we'd put
|
|
||||||
// all of them in the recording and send the appropriate one to the client
|
|
||||||
// in the playback lobby
|
|
||||||
for (string& data : map_commands_by_language) {
|
|
||||||
if (!data.empty()) {
|
|
||||||
l->battle_record->add_command(
|
|
||||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (l->battle_record && l->battle_record->writable()) {
|
||||||
|
// TODO: It's not great that we just pick the first one; ideally we'd put
|
||||||
|
// all of them in the recording and send the appropriate one to the client
|
||||||
|
// in the playback lobby
|
||||||
|
for (string& data : map_commands_by_language) {
|
||||||
|
if (!data.empty()) {
|
||||||
|
l->battle_record->add_command(
|
||||||
|
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
auto out_data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, 1, false);
|
||||||
|
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public:
|
|||||||
int8_t get_winner_team_id() const;
|
int8_t get_winner_team_id() const;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void send(const T& cmd) const {
|
void send(const T& cmd, uint8_t command = 0xC9, bool enable_masking = true) const {
|
||||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||||
throw std::logic_error("outbound command size field is incorrect");
|
throw std::logic_error("outbound command size field is incorrect");
|
||||||
}
|
}
|
||||||
@@ -109,9 +109,9 @@ public:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->send(&cmd, cmd.header.size * 4);
|
this->send(&cmd, cmd.header.size * 4, command, enable_masking);
|
||||||
}
|
}
|
||||||
void send(const void* data, size_t size) const;
|
void send(const void* data, size_t size, uint8_t command = 0xC9, bool enable_masking = true) const;
|
||||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||||
|
|
||||||
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
||||||
@@ -242,6 +242,7 @@ private:
|
|||||||
public:
|
public:
|
||||||
// These fields are not part of the original implementation
|
// These fields are not part of the original implementation
|
||||||
std::weak_ptr<Lobby> lobby;
|
std::weak_ptr<Lobby> lobby;
|
||||||
|
bool has_lobby;
|
||||||
Options options;
|
Options options;
|
||||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||||
bool tournament_match_result_sent;
|
bool tournament_match_result_sent;
|
||||||
|
|||||||
+26
@@ -1827,6 +1827,32 @@ Action a_diff_dol_files(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Action a_replay_ep3_battle_commands(
|
||||||
|
"replay-ep3-battle-commands", nullptr, +[](Arguments& args) {
|
||||||
|
auto card_index = make_shared<Episode3::CardIndex>("system/ep3/card-definitions.mnr", "system/ep3/card-definitions.mnrd");
|
||||||
|
auto map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
|
||||||
|
auto random_crypt = make_shared<PSOV2Encryption>(args.get<uint32_t>("seed", 0, Arguments::IntFormat::HEX));
|
||||||
|
Episode3::Server::Options options = {
|
||||||
|
.card_index = card_index,
|
||||||
|
.map_index = map_index,
|
||||||
|
.behavior_flags = 0x0092,
|
||||||
|
.random_crypt = random_crypt,
|
||||||
|
.tournament = nullptr,
|
||||||
|
.trap_card_ids = {},
|
||||||
|
};
|
||||||
|
auto server = make_shared<Episode3::Server>(nullptr, std::move(options));
|
||||||
|
server->init();
|
||||||
|
|
||||||
|
auto input = read_input_data(args);
|
||||||
|
auto lines = split(input, '\n');
|
||||||
|
for (const auto& line : lines) {
|
||||||
|
string data = parse_data_string(line);
|
||||||
|
if (!data.empty()) {
|
||||||
|
server->on_server_data_input(nullptr, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Action a_run_server_replay_log(
|
Action a_run_server_replay_log(
|
||||||
"", nullptr, +[](Arguments& args) {
|
"", nullptr, +[](Arguments& args) {
|
||||||
if (!isdir("system/players")) {
|
if (!isdir("system/players")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user