support multiple replays in the same session

This commit is contained in:
Martin Michelsen
2026-05-13 21:25:55 -07:00
parent 2e667bbe50
commit a6c25568ba
7 changed files with 68 additions and 40 deletions
+13 -11
View File
@@ -154,23 +154,25 @@ add_dependencies(newserv newserv-Revision-cc)
enable_testing() enable_testing()
file(GLOB LogTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.txt) file(GLOB LOG_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.txt)
file(GLOB LogRDTestCases ${CMAKE_SOURCE_DIR}/tests/*.rdtest.txt) foreach(LOG_TEST_CASE IN ITEMS ${LOG_TEST_CASES})
foreach(LogTestCase IN ITEMS ${LogTestCases})
add_test( add_test(
NAME ${LogTestCase} NAME ${LOG_TEST_CASE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${LogTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json) COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${LOG_TEST_CASE} --config=${CMAKE_SOURCE_DIR}/tests/config.json)
endforeach() endforeach()
# list(TRANSFORM LOG_TEST_CASES PREPEND "--replay-log=" OUTPUT_VARIABLE LOG_REPLAY_ARGS)
# add_test(
# NAME "log-replays"
# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
# COMMAND ${CMAKE_BINARY_DIR}/newserv --config=${CMAKE_SOURCE_DIR}/tests/config.json ${LOG_REPLAY_ARGS})
file(GLOB ScriptTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.sh) file(GLOB SCRIPT_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.sh)
foreach(SCRIPT_TEST_CASE IN ITEMS ${SCRIPT_TEST_CASES})
foreach(ScriptTestCase IN ITEMS ${ScriptTestCases})
add_test( add_test(
NAME ${ScriptTestCase} NAME ${SCRIPT_TEST_CASE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${ScriptTestCase} ${CMAKE_BINARY_DIR}/newserv) COMMAND ${SCRIPT_TEST_CASE} ${CMAKE_BINARY_DIR}/newserv)
endforeach() endforeach()
+30 -10
View File
@@ -4041,7 +4041,7 @@ Action a_run_server_replay_log(
std::filesystem::create_directories("system/players"); std::filesystem::create_directories("system/players");
} }
const string& replay_log_filename = args.get<string>("replay-log"); const auto& replay_log_filenames = args.get_multi<string>("replay-log");
#ifndef PHOSG_WINDOWS #ifndef PHOSG_WINDOWS
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
@@ -4050,7 +4050,7 @@ Action a_run_server_replay_log(
use_terminal_colors = true; use_terminal_colors = true;
} }
auto state = make_shared<ServerState>(get_config_filename(args), !replay_log_filename.empty()); auto state = make_shared<ServerState>(get_config_filename(args), !replay_log_filenames.empty());
if (args.get<bool>("debug")) { if (args.get<bool>("debug")) {
state->is_debug = true; state->is_debug = true;
} }
@@ -4069,19 +4069,39 @@ Action a_run_server_replay_log(
} }
shared_ptr<ServerShell> shell; shared_ptr<ServerShell> shell;
shared_ptr<ReplaySession> replay_session;
shared_ptr<SignalWatcher> signal_watcher; shared_ptr<SignalWatcher> signal_watcher;
if (!replay_log_filename.empty()) { shared_ptr<ReplaySession> last_running_replay;
if (!replay_log_filenames.empty()) {
config_log.info_f("Starting game server"); config_log.info_f("Starting game server");
state->game_server = make_shared<GameServer>(state); state->game_server = make_shared<GameServer>(state);
// TODO: Do this properly via a config option, you lazy bum // TODO: Do this properly via a config option, you lazy bum
state->dol_file_index = make_shared<DOLFileIndex>(); state->dol_file_index = make_shared<DOLFileIndex>();
auto log_f = phosg::fopen_shared(replay_log_filename, "rt"); auto run_replays = [&]() -> asio::awaitable<void> {
try {
replay_session = make_shared<ReplaySession>(state, log_f.get(), false); for (const auto& log_filename : replay_log_filenames) {
asio::co_spawn(*state->io_context, replay_session->run(), asio::detached); phosg::log_info_f("[Replay] {} ...", log_filename);
auto log_f = phosg::fopen_shared(log_filename, "rt");
last_running_replay = make_shared<ReplaySession>(state, log_f.get());
co_await last_running_replay->run();
if (last_running_replay->failed()) {
phosg::log_error_f("[Replay] {} failed", log_filename);
break;
}
phosg::log_info_f("[Replay] {} OK", log_filename);
state->reset_between_replays();
}
phosg::log_info_f("[Replay] All replays complete");
} catch (const std::exception& e) {
phosg::log_info_f("[Replay] Replays failed: {}", e.what());
}
if (!last_running_replay->failed()) {
last_running_replay.reset();
}
state->io_context->stop();
};
asio::co_spawn(*state->io_context, run_replays, asio::detached);
} else { } else {
config_log.info_f("Opening sockets"); config_log.info_f("Opening sockets");
@@ -4174,7 +4194,7 @@ Action a_run_server_replay_log(
should_run_shell = false; should_run_shell = false;
} }
if (should_run_shell) { if (should_run_shell) {
should_run_shell = !replay_session.get(); should_run_shell = replay_log_filenames.empty();
} }
config_log.info_f("Ready"); config_log.info_f("Ready");
@@ -4185,7 +4205,7 @@ Action a_run_server_replay_log(
state->io_context->run(); state->io_context->run();
config_log.info_f("Normal shutdown"); config_log.info_f("Normal shutdown");
if (replay_session && replay_session->failed()) { if (last_running_replay) {
throw runtime_error("Replay failed"); throw runtime_error("Replay failed");
} }
}); });
+1 -5
View File
@@ -321,9 +321,8 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
} }
} }
ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log, bool is_interactive) ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log)
: state(state), : state(state),
is_interactive(is_interactive),
prev_psov2_crypt_enabled(this->state->use_psov2_rand_crypt), prev_psov2_crypt_enabled(this->state->use_psov2_rand_crypt),
commands_sent(0), commands_sent(0),
bytes_sent(0), bytes_sent(0),
@@ -669,9 +668,6 @@ asio::awaitable<void> ReplaySession::run() {
replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)", replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)",
this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received); this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received);
} }
if (!this->is_interactive) {
this->state->io_context->stop();
}
} }
void ReplaySession::reschedule_idle_timeout() { void ReplaySession::reschedule_idle_timeout() {
+1 -2
View File
@@ -13,7 +13,7 @@
class ReplaySession { class ReplaySession {
public: public:
ReplaySession(std::shared_ptr<ServerState> state, FILE* input_log, bool is_interactive); ReplaySession(std::shared_ptr<ServerState> state, FILE* input_log);
ReplaySession(const ReplaySession&) = delete; ReplaySession(const ReplaySession&) = delete;
ReplaySession(ReplaySession&&) = delete; ReplaySession(ReplaySession&&) = delete;
ReplaySession& operator=(const ReplaySession&) = delete; ReplaySession& operator=(const ReplaySession&) = delete;
@@ -62,7 +62,6 @@ private:
}; };
std::shared_ptr<ServerState> state; std::shared_ptr<ServerState> state;
bool is_interactive;
bool prev_psov2_crypt_enabled; bool prev_psov2_crypt_enabled;
std::unordered_map<uint64_t, std::shared_ptr<Client>> clients; std::unordered_map<uint64_t, std::shared_ptr<Client>> clients;
+21
View File
@@ -2361,6 +2361,27 @@ void ServerState::load_all(bool enable_thread_pool) {
this->generate_bb_stream_file(); this->generate_bb_stream_file();
} }
void ServerState::reset_between_replays() {
if (this->allow_saving_accounts) {
throw std::logic_error("Account saving is enabled during replay");
}
this->account_index = make_shared<AccountIndex>(true);
this->next_lobby_id = 0;
std::vector<std::shared_ptr<Lobby>> lobbies_to_delete;
for (const auto& l : this->all_lobbies()) {
if (l->is_game()) {
lobbies_to_delete.emplace_back(l);
} else {
this->next_lobby_id = std::max<uint32_t>(this->next_lobby_id, l->lobby_id + 1);
}
}
for (const auto& l : lobbies_to_delete) {
phosg::log_warning_f("Previous replay left a game open ({:08X}); destroying it", l->lobby_id);
this->remove_lobby(l);
}
}
void ServerState::disconnect_all_banned_clients() { void ServerState::disconnect_all_banned_clients() {
uint64_t now_usecs = phosg::now(); uint64_t now_usecs = phosg::now();
+2
View File
@@ -458,5 +458,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void load_all(bool enable_thread_pool); void load_all(bool enable_thread_pool);
void reset_between_replays();
void disconnect_all_banned_clients(); void disconnect_all_banned_clients();
}; };
-12
View File
@@ -1095,15 +1095,3 @@ ShellCommand c_create_item(
send_text_message(c->channel, "$C7Item created:\n" + name); send_text_message(c->channel, "$C7Item created:\n" + name);
co_return deque<string>{}; co_return deque<string>{};
}); });
ShellCommand c_replay_log(
"replay-log", nullptr,
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
if (args.s->allow_saving_accounts) {
throw runtime_error("Replays cannot be run when account saving is enabled");
}
auto log_f = phosg::fopen_shared(args.args, "rt");
auto replay_session = make_shared<ReplaySession>(args.s, log_f.get(), true);
co_await replay_session->run();
co_return deque<string>{};
});