add Ep3 battle replay test

This commit is contained in:
Martin Michelsen
2025-11-05 09:02:22 -08:00
parent 8cb7d2b2fe
commit 540a41a583
10 changed files with 157 additions and 52 deletions
+111 -15
View File
@@ -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");
}
});