From a6c25568ba02291aa70453d34e0526a087db2f38 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 13 May 2026 21:25:55 -0700 Subject: [PATCH] support multiple replays in the same session --- CMakeLists.txt | 24 +++++++++++++----------- src/Main.cc | 40 ++++++++++++++++++++++++++++++---------- src/ReplaySession.cc | 6 +----- src/ReplaySession.hh | 3 +-- src/ServerState.cc | 21 +++++++++++++++++++++ src/ServerState.hh | 2 ++ src/ShellCommands.cc | 12 ------------ 7 files changed, 68 insertions(+), 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe3eea94..c4fb2771 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,23 +154,25 @@ add_dependencies(newserv newserv-Revision-cc) enable_testing() -file(GLOB LogTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.txt) -file(GLOB LogRDTestCases ${CMAKE_SOURCE_DIR}/tests/*.rdtest.txt) - -foreach(LogTestCase IN ITEMS ${LogTestCases}) +file(GLOB LOG_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.txt) +foreach(LOG_TEST_CASE IN ITEMS ${LOG_TEST_CASES}) add_test( - NAME ${LogTestCase} + NAME ${LOG_TEST_CASE} 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() +# 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) - -foreach(ScriptTestCase IN ITEMS ${ScriptTestCases}) +file(GLOB SCRIPT_TEST_CASES ${CMAKE_SOURCE_DIR}/tests/*.test.sh) +foreach(SCRIPT_TEST_CASE IN ITEMS ${SCRIPT_TEST_CASES}) add_test( - NAME ${ScriptTestCase} + NAME ${SCRIPT_TEST_CASE} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMAND ${ScriptTestCase} ${CMAKE_BINARY_DIR}/newserv) + COMMAND ${SCRIPT_TEST_CASE} ${CMAKE_BINARY_DIR}/newserv) endforeach() diff --git a/src/Main.cc b/src/Main.cc index ccd1827b..0de0342d 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -4041,7 +4041,7 @@ Action a_run_server_replay_log( std::filesystem::create_directories("system/players"); } - const string& replay_log_filename = args.get("replay-log"); + const auto& replay_log_filenames = args.get_multi("replay-log"); #ifndef PHOSG_WINDOWS signal(SIGPIPE, SIG_IGN); @@ -4050,7 +4050,7 @@ Action a_run_server_replay_log( use_terminal_colors = true; } - auto state = make_shared(get_config_filename(args), !replay_log_filename.empty()); + auto state = make_shared(get_config_filename(args), !replay_log_filenames.empty()); if (args.get("debug")) { state->is_debug = true; } @@ -4069,19 +4069,39 @@ Action a_run_server_replay_log( } shared_ptr shell; - shared_ptr replay_session; shared_ptr signal_watcher; - if (!replay_log_filename.empty()) { + shared_ptr last_running_replay; + if (!replay_log_filenames.empty()) { config_log.info_f("Starting game server"); state->game_server = make_shared(state); // TODO: Do this properly via a config option, you lazy bum state->dol_file_index = make_shared(); - auto log_f = phosg::fopen_shared(replay_log_filename, "rt"); - - replay_session = make_shared(state, log_f.get(), false); - asio::co_spawn(*state->io_context, replay_session->run(), asio::detached); + auto run_replays = [&]() -> asio::awaitable { + try { + for (const auto& log_filename : replay_log_filenames) { + phosg::log_info_f("[Replay] {} ...", log_filename); + auto log_f = phosg::fopen_shared(log_filename, "rt"); + last_running_replay = make_shared(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 { config_log.info_f("Opening sockets"); @@ -4174,7 +4194,7 @@ Action a_run_server_replay_log( should_run_shell = false; } if (should_run_shell) { - should_run_shell = !replay_session.get(); + should_run_shell = replay_log_filenames.empty(); } config_log.info_f("Ready"); @@ -4185,7 +4205,7 @@ Action a_run_server_replay_log( state->io_context->run(); config_log.info_f("Normal shutdown"); - if (replay_session && replay_session->failed()) { + if (last_running_replay) { throw runtime_error("Replay failed"); } }); diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 359747ef..1117ec31 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -321,9 +321,8 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { } } -ReplaySession::ReplaySession(shared_ptr state, FILE* input_log, bool is_interactive) +ReplaySession::ReplaySession(shared_ptr state, FILE* input_log) : state(state), - is_interactive(is_interactive), prev_psov2_crypt_enabled(this->state->use_psov2_rand_crypt), commands_sent(0), bytes_sent(0), @@ -669,9 +668,6 @@ asio::awaitable ReplaySession::run() { replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)", 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() { diff --git a/src/ReplaySession.hh b/src/ReplaySession.hh index efa8dd10..917fe2bc 100644 --- a/src/ReplaySession.hh +++ b/src/ReplaySession.hh @@ -13,7 +13,7 @@ class ReplaySession { public: - ReplaySession(std::shared_ptr state, FILE* input_log, bool is_interactive); + ReplaySession(std::shared_ptr state, FILE* input_log); ReplaySession(const ReplaySession&) = delete; ReplaySession(ReplaySession&&) = delete; ReplaySession& operator=(const ReplaySession&) = delete; @@ -62,7 +62,6 @@ private: }; std::shared_ptr state; - bool is_interactive; bool prev_psov2_crypt_enabled; std::unordered_map> clients; diff --git a/src/ServerState.cc b/src/ServerState.cc index a4ef9b8d..401b7a51 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -2361,6 +2361,27 @@ void ServerState::load_all(bool enable_thread_pool) { 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(true); + + this->next_lobby_id = 0; + std::vector> 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(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() { uint64_t now_usecs = phosg::now(); diff --git a/src/ServerState.hh b/src/ServerState.hh index 084576af..2e73bb32 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -458,5 +458,7 @@ struct ServerState : public std::enable_shared_from_this { void load_all(bool enable_thread_pool); + void reset_between_replays(); + void disconnect_all_banned_clients(); }; diff --git a/src/ShellCommands.cc b/src/ShellCommands.cc index eb2e886a..367ff18f 100644 --- a/src/ShellCommands.cc +++ b/src/ShellCommands.cc @@ -1095,15 +1095,3 @@ ShellCommand c_create_item( send_text_message(c->channel, "$C7Item created:\n" + name); co_return deque{}; }); - -ShellCommand c_replay_log( - "replay-log", nullptr, - +[](ShellCommand::Args& args) -> asio::awaitable> { - 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(args.s, log_f.get(), true); - co_await replay_session->run(); - co_return deque{}; - });