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()
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()
+30 -10
View File
@@ -4041,7 +4041,7 @@ Action a_run_server_replay_log(
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
signal(SIGPIPE, SIG_IGN);
@@ -4050,7 +4050,7 @@ Action a_run_server_replay_log(
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")) {
state->is_debug = true;
}
@@ -4069,19 +4069,39 @@ Action a_run_server_replay_log(
}
shared_ptr<ServerShell> shell;
shared_ptr<ReplaySession> replay_session;
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");
state->game_server = make_shared<GameServer>(state);
// TODO: Do this properly via a config option, you lazy bum
state->dol_file_index = make_shared<DOLFileIndex>();
auto log_f = phosg::fopen_shared(replay_log_filename, "rt");
replay_session = make_shared<ReplaySession>(state, log_f.get(), false);
asio::co_spawn(*state->io_context, replay_session->run(), asio::detached);
auto run_replays = [&]() -> asio::awaitable<void> {
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<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 {
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");
}
});
+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),
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<void> 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() {
+1 -2
View File
@@ -13,7 +13,7 @@
class ReplaySession {
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(ReplaySession&&) = delete;
ReplaySession& operator=(const ReplaySession&) = delete;
@@ -62,7 +62,6 @@ private:
};
std::shared_ptr<ServerState> state;
bool is_interactive;
bool prev_psov2_crypt_enabled;
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();
}
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() {
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 reset_between_replays();
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);
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>{};
});