add Ep3 battle replay test
This commit is contained in:
@@ -18,6 +18,7 @@ build
|
||||
|
||||
# Files modified by the user and/or server that don't have defaults
|
||||
system/config.json
|
||||
system/ep3/battle-records/*.mzr
|
||||
system/ep3/battle-records/*.mzrd
|
||||
system/ep3/tournament-state.json
|
||||
system/licenses.nsi
|
||||
|
||||
+9
-6
@@ -1882,13 +1882,13 @@ ChatCommandDefinition cc_ping(
|
||||
co_return;
|
||||
});
|
||||
|
||||
static string file_path_for_recording(const std::string& args, uint32_t account_id) {
|
||||
static string file_path_for_recording(const std::string& args, uint32_t account_id, bool compressed) {
|
||||
for (char ch : args) {
|
||||
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
|
||||
throw runtime_error("invalid recording name");
|
||||
}
|
||||
}
|
||||
return std::format("system/ep3/battle-records/{:010}_{}.mzrd", account_id, args);
|
||||
return std::format("system/ep3/battle-records/{:010}_{}.mzr{}", account_id, args, compressed ? "" : "d");
|
||||
}
|
||||
|
||||
ChatCommandDefinition cc_playrec(
|
||||
@@ -1908,13 +1908,16 @@ ChatCommandDefinition cc_playrec(
|
||||
if (!start_battle_player_immediately) {
|
||||
filename = filename.substr(1);
|
||||
}
|
||||
string file_path = file_path_for_recording(filename, a.c->login->account->account_id);
|
||||
|
||||
string data;
|
||||
try {
|
||||
data = phosg::load_file(file_path);
|
||||
data = phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, false));
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
throw precondition_failed("$C4The recording does\nnot exist");
|
||||
try {
|
||||
data = prs_decompress(phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, true)));
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
throw precondition_failed("$C4The recording does\nnot exist");
|
||||
}
|
||||
}
|
||||
auto record = make_shared<Episode3::BattleRecord>(data);
|
||||
auto battle_player = make_shared<Episode3::BattleRecordPlayer>(s->io_context, record);
|
||||
@@ -2288,7 +2291,7 @@ ChatCommandDefinition cc_saverec(
|
||||
if (!a.c->ep3_prev_battle_record) {
|
||||
throw precondition_failed("$C4No finished\nrecording is\npresent");
|
||||
}
|
||||
string file_path = file_path_for_recording(a.text, a.c->login->account->account_id);
|
||||
string file_path = file_path_for_recording(a.text, a.c->login->account->account_id, false);
|
||||
string data = a.c->ep3_prev_battle_record->serialize();
|
||||
phosg::save_file(file_path, data);
|
||||
send_text_message(a.c, "$C7Recording saved");
|
||||
|
||||
@@ -174,21 +174,6 @@ string BattleRecord::serialize() const {
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
bool BattleRecord::writable() const {
|
||||
return this->is_writable;
|
||||
}
|
||||
|
||||
bool BattleRecord::battle_in_progress() const {
|
||||
return (this->battle_start_timestamp != 0);
|
||||
}
|
||||
|
||||
const BattleRecord::Event* BattleRecord::get_first_event() const {
|
||||
if (this->events.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &this->events.front();
|
||||
}
|
||||
|
||||
void BattleRecord::add_player(
|
||||
const PlayerLobbyDataDCGC& lobby_data,
|
||||
const PlayerInventory& inventory,
|
||||
@@ -256,16 +241,6 @@ void BattleRecord::add_random_data(const void* data, size_t size) {
|
||||
this->random_stream.append(reinterpret_cast<const char*>(data), size);
|
||||
}
|
||||
|
||||
vector<string> BattleRecord::get_all_server_data_commands() const {
|
||||
vector<string> ret;
|
||||
for (const auto& event : this->events) {
|
||||
if (event.type == Event::Type::SERVER_DATA_COMMAND) {
|
||||
ret.emplace_back(event.data);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const string& BattleRecord::get_random_stream() const {
|
||||
return this->random_stream;
|
||||
}
|
||||
|
||||
@@ -62,10 +62,25 @@ public:
|
||||
explicit BattleRecord(const std::string& data);
|
||||
std::string serialize() const;
|
||||
|
||||
bool writable() const;
|
||||
bool battle_in_progress() const;
|
||||
inline bool writable() const {
|
||||
return this->is_writable;
|
||||
}
|
||||
|
||||
const Event* get_first_event() const;
|
||||
inline uint32_t get_behavior_flags() const {
|
||||
return this->behavior_flags;
|
||||
}
|
||||
|
||||
inline bool battle_in_progress() const {
|
||||
return (this->battle_start_timestamp != 0);
|
||||
}
|
||||
|
||||
inline const Event* get_first_event() const {
|
||||
return this->events.empty() ? nullptr : &this->events.front();
|
||||
}
|
||||
|
||||
inline std::deque<Event> get_all_events() const {
|
||||
return this->events;
|
||||
}
|
||||
|
||||
void add_player(
|
||||
const PlayerLobbyDataDCGC& lobby_data,
|
||||
@@ -86,7 +101,6 @@ public:
|
||||
|
||||
void print(FILE* stream) const;
|
||||
|
||||
std::vector<std::string> get_all_server_data_commands() const;
|
||||
const std::string& get_random_stream() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -231,6 +231,11 @@ int8_t Server::get_winner_team_id() 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.
|
||||
|
||||
if (this->options.output_queue) {
|
||||
this->options.output_queue->emplace_back(reinterpret_cast<const char*>(data), size);
|
||||
}
|
||||
|
||||
if (this->has_lobby) {
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
@@ -2579,8 +2584,7 @@ void Server::send_6xB6x41_to_all_clients() const {
|
||||
// in the playback lobby
|
||||
for (string& data : map_commands_by_language) {
|
||||
if (!data.empty()) {
|
||||
this->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public:
|
||||
std::shared_ptr<RandomGenerator> rand_crypt;
|
||||
std::shared_ptr<const Tournament> tournament;
|
||||
std::array<std::vector<uint16_t>, 5> trap_card_ids;
|
||||
std::shared_ptr<std::deque<std::string>> output_queue; // For replay testing
|
||||
|
||||
inline bool is_nte() const {
|
||||
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
|
||||
|
||||
@@ -313,6 +313,7 @@ void Lobby::create_ep3_server() {
|
||||
.rand_crypt = this->rand_crypt,
|
||||
.tournament = tourn,
|
||||
.trap_card_ids = s->ep3_trap_card_ids,
|
||||
.output_queue = nullptr,
|
||||
};
|
||||
if (is_nte) {
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
|
||||
+111
-15
@@ -3340,6 +3340,7 @@ Action a_replay_ep3_battle_commands(
|
||||
.rand_crypt = make_shared<MT19937Generator>(seed),
|
||||
.tournament = nullptr,
|
||||
.trap_card_ids = {},
|
||||
.output_queue = nullptr,
|
||||
};
|
||||
if (is_trial) {
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
@@ -3365,36 +3366,131 @@ Action a_replay_ep3_battle_commands(
|
||||
|
||||
Action a_replay_ep3_battle_record(
|
||||
"replay-ep3-battle-record", nullptr, +[](phosg::Arguments& args) {
|
||||
auto rec = make_shared<Episode3::BattleRecord>(read_input_data(args));
|
||||
auto record_data = read_input_data(args);
|
||||
if (args.get<bool>("compressed")) {
|
||||
record_data = prs_decompress(record_data);
|
||||
}
|
||||
auto rec = make_shared<Episode3::BattleRecord>(record_data);
|
||||
|
||||
bool use_color = isatty(fileno(stdout));
|
||||
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_ep3_cards();
|
||||
s->load_ep3_maps();
|
||||
|
||||
bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE);
|
||||
|
||||
bool is_nte = rec->get_behavior_flags() & Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
auto output_queue = std::make_shared<std::deque<std::string>>();
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = s->ep3_card_index,
|
||||
.map_index = s->ep3_map_index,
|
||||
.behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS |
|
||||
Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES |
|
||||
Episode3::BehaviorFlag::DISABLE_MASKING |
|
||||
Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
|
||||
.behavior_flags = rec->get_behavior_flags() & ~(Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
|
||||
.opt_rand_stream = make_shared<phosg::StringReader>(rec->get_random_stream()),
|
||||
.rand_crypt = make_shared<DisabledRandomGenerator>(),
|
||||
.tournament = nullptr,
|
||||
.trap_card_ids = {},
|
||||
.output_queue = output_queue,
|
||||
};
|
||||
if (is_trial) {
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
}
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING;
|
||||
auto server = make_shared<Episode3::Server>(nullptr, std::move(options));
|
||||
server->init();
|
||||
for (const auto& command : rec->get_all_server_data_commands()) {
|
||||
phosg::log_info_f("Server data command");
|
||||
phosg::print_data(stderr, command, 0, nullptr, phosg::PrintDataFlags::PRINT_ASCII | phosg::PrintDataFlags::DISABLE_COLOR | phosg::PrintDataFlags::OFFSET_16_BITS);
|
||||
server->on_server_data_input(nullptr, command);
|
||||
|
||||
// Ignore commands generated by the server when it's constructed (these
|
||||
// are not included in the battle record)
|
||||
output_queue->clear();
|
||||
|
||||
std::array<bool, 4> players_present = {false, false, false, false};
|
||||
for (const auto& ev : rec->get_all_events()) {
|
||||
switch (ev.type) {
|
||||
case Episode3::BattleRecord::Event::Type::SET_INITIAL_PLAYERS:
|
||||
ev.print(stdout);
|
||||
for (const auto& player : ev.players) {
|
||||
players_present.at(player.lobby_data.client_id) = true;
|
||||
phosg::fwrite_fmt(stderr, "Player {} is present\n", player.lobby_data.client_id.load());
|
||||
}
|
||||
break;
|
||||
case Episode3::BattleRecord::Event::Type::PLAYER_JOIN:
|
||||
case Episode3::BattleRecord::Event::Type::PLAYER_LEAVE:
|
||||
case Episode3::BattleRecord::Event::Type::CHAT_MESSAGE:
|
||||
case Episode3::BattleRecord::Event::Type::GAME_COMMAND:
|
||||
case Episode3::BattleRecord::Event::Type::EP3_GAME_COMMAND:
|
||||
ev.print(stdout);
|
||||
break;
|
||||
case Episode3::BattleRecord::Event::Type::BATTLE_COMMAND:
|
||||
// Ignore the map command (this is handled separately) and 6xB4x4B
|
||||
// (which is only generated when a lobby is present)
|
||||
if (ev.data.empty() || (static_cast<uint8_t>(ev.data[0]) == 0xB6) || (ev.data.at(4) == 0x4B)) {
|
||||
ev.print(stdout);
|
||||
} else {
|
||||
if (use_color) {
|
||||
phosg::print_color_escape(stdout, phosg::TerminalFormat::FG_RED, phosg::TerminalFormat::BOLD, phosg::TerminalFormat::END);
|
||||
}
|
||||
ev.print(stdout);
|
||||
if (use_color) {
|
||||
phosg::print_color_escape(stdout, phosg::TerminalFormat::NORMAL, phosg::TerminalFormat::END);
|
||||
fflush(stdout);
|
||||
}
|
||||
if (output_queue->empty()) {
|
||||
phosg::fwrite_fmt(stderr, "Output queue is empty, but expected battle command:\n");
|
||||
phosg::print_data(stderr, ev.data, 0, nullptr, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||
throw std::runtime_error("Output did not match expectations");
|
||||
}
|
||||
// Hack: don't check the last field in 6xB4x46 since it contains
|
||||
// a timestamp on non-NTE
|
||||
bool matched = false;
|
||||
if ((ev.data.at(4) == 0x46) && !is_nte) {
|
||||
auto received_cmd = check_size_t<G_ServerVersionStrings_Ep3_6xB4x46>(output_queue->front());
|
||||
auto expected_cmd = check_size_t<G_ServerVersionStrings_Ep3_6xB4x46>(ev.data);
|
||||
received_cmd.date_str2.clear(0);
|
||||
expected_cmd.date_str2.clear(0);
|
||||
matched = !memcmp(&received_cmd, &expected_cmd, sizeof(received_cmd));
|
||||
} else {
|
||||
matched = (output_queue->front() == ev.data);
|
||||
}
|
||||
if (!matched) {
|
||||
const void* prev = (ev.data.size() == output_queue->front().size()) ? ev.data.data() : nullptr;
|
||||
phosg::fwrite_fmt(stderr, "Output queue front did not match expected command; expected:\n");
|
||||
phosg::print_data(stderr, ev.data, 0, nullptr, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||
phosg::fwrite_fmt(stderr, "Received:\n");
|
||||
phosg::print_data(stderr, output_queue->front(), 0, prev, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||
throw std::runtime_error("Output did not match expectations");
|
||||
}
|
||||
output_queue->pop_front();
|
||||
}
|
||||
break;
|
||||
case Episode3::BattleRecord::Event::Type::SERVER_DATA_COMMAND:
|
||||
if (use_color) {
|
||||
phosg::print_color_escape(stdout, phosg::TerminalFormat::FG_GREEN, phosg::TerminalFormat::BOLD, phosg::TerminalFormat::END);
|
||||
}
|
||||
ev.print(stdout);
|
||||
if (use_color) {
|
||||
phosg::print_color_escape(stdout, phosg::TerminalFormat::NORMAL, phosg::TerminalFormat::END);
|
||||
fflush(stdout);
|
||||
}
|
||||
if (!output_queue->empty()) {
|
||||
phosg::fwrite_fmt(stderr, "Received extra output after preceding SERVER_DATA event:\n");
|
||||
phosg::print_data(stderr, output_queue->front());
|
||||
throw std::runtime_error("Output did not match expectations");
|
||||
}
|
||||
// Hack: Set the CPU player flag if the player isn't present in the
|
||||
// recording (normally this is done by checking the Lobby, but
|
||||
// there's no Lobby during a replay)
|
||||
if (ev.data.at(4) == 0x1B) {
|
||||
string mutable_data = ev.data;
|
||||
auto& cmd = check_size_t<G_SetPlayerName_Ep3_CAx1B>(mutable_data);
|
||||
cmd.entry.is_cpu_player = !players_present.at(cmd.entry.client_id);
|
||||
phosg::fwrite_fmt(stderr, "Overriding is_cpu_player with {}\n", cmd.entry.is_cpu_player ? "true" : "false");
|
||||
server->on_server_data_input(nullptr, mutable_data);
|
||||
} else {
|
||||
server->on_server_data_input(nullptr, ev.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unknown event type: {}");
|
||||
}
|
||||
}
|
||||
if (!output_queue->empty()) {
|
||||
phosg::fwrite_fmt(stderr, "Received extra output after recording completed:\n");
|
||||
phosg::print_data(stderr, output_queue->front());
|
||||
throw std::runtime_error("Output did not match expectations");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
EXECUTABLE="$1"
|
||||
if [ -z "$EXECUTABLE" ]; then
|
||||
EXECUTABLE="./newserv"
|
||||
fi
|
||||
|
||||
$EXECUTABLE --config=tests/config.json replay-ep3-battle-record --compressed tests/replay-ep3-battle-input.mzr
|
||||
Reference in New Issue
Block a user