From 31f1bcf2d31760da4473a34a812a6dfa308f8e22 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 09:45:29 -0500 Subject: [PATCH] fix(capture): replay keeps its records, skips RakNet's own messages and waits for transfers - the expected answers pointed into a copy of the records that was gone by the diff - the client's recorded connect and disconnect messages are RakNet's, not sent again - a recorded move to another world is waited for as long as a zone takes to start - the handshake sends the client net version; reports are written after every bundle - a crash or Ctrl-C of the tool stops the sandbox stack too Co-Authored-By: Claude Opus 5.5 --- dCaptureTool/CaptureTool.cpp | 3 +++ dCaptureTool/Replayer.cpp | 15 +++++++++++---- dCaptureTool/Sandbox.cpp | 12 ++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/dCaptureTool/CaptureTool.cpp b/dCaptureTool/CaptureTool.cpp index fc0458527..3ccacf9f8 100644 --- a/dCaptureTool/CaptureTool.cpp +++ b/dCaptureTool/CaptureTool.cpp @@ -254,6 +254,9 @@ namespace { entry["sandbox"] = stack->Dir().string(); report.push_back(entry); PrintResult(fs::path(path).filename().string(), result); + // Written after every bundle, so a long run's results survive it being stopped + if (!Arg(args, "--report").empty()) std::ofstream(Arg(args, "--report")) << report.dump(1); + std::cout.flush(); const bool failed = !result.stoppedAt.empty(); if (failed) failures++; if (options.keep || (failed && Flag(args, "--keep-on-failure"))) { diff --git a/dCaptureTool/Replayer.cpp b/dCaptureTool/Replayer.cpp index 4ff903414..4d36ffd99 100644 --- a/dCaptureTool/Replayer.cpp +++ b/dCaptureTool/Replayer.cpp @@ -65,6 +65,8 @@ namespace { for (const auto& record : records) { const auto source = static_cast(record.header.source); if ((source != eCaptureSource::AUTH && source != eCaptureSource::WORLD) || (record.header.flags & (PacketRecordFlags::MASTER_LINK | PacketRecordFlags::GAP))) continue; + // RakNet's own connection messages (connected, disconnected) aren't sent: the fake client's RakNet makes its own + if (CaptureTools::FromClient(record.header) && (record.bytes.empty() || static_cast(record.bytes[0]) != ID_USER_PACKET_ENUM)) continue; const bool handshake = CaptureTools::FromClient(record.header) && NameOf(record) == "VERSION_CONFIRM"; if (segments.empty() || handshake || segments.back().source != source) segments.push_back({ source, {}, false }); segments.back().records.push_back(&record); @@ -79,9 +81,10 @@ namespace { Run(const CaptureBundle::Bundle& bundle, const Replayer::Options& options, Replayer::Result& result) : m_Bundle(bundle), m_Options(options), m_Result(result) {} void Go() { - auto records = m_Bundle.records; - CaptureTools::SortTimeline(records); - const auto segments = Split(records); + // Kept for the whole run: the segments and the expected answers point into it + m_Records = m_Bundle.records; + CaptureTools::SortTimeline(m_Records); + const auto segments = Split(m_Records); m_Result.connections = segments.size(); if (segments.empty()) return Stop("The bundle has no client connections"); @@ -112,6 +115,7 @@ namespace { private: const CaptureBundle::Bundle& m_Bundle; + std::vector m_Records; const Replayer::Options& m_Options; Replayer::Result& m_Result; std::vector m_Expected; @@ -132,6 +136,7 @@ namespace { void Handshake(FakeClient& client) { CommonPackets::ClientVersionConfirm version; + version.netVersion = CommonPackets::ServerVersionConfirm::DEFAULT_NET_VERSION; version.serviceType = ServiceType::CLIENT; version.processID = 1; version.port = client.GetLocalPort(); @@ -345,7 +350,9 @@ namespace { if (client.GetReceived().size() == count && rounds >= 3) break; count = client.GetReceived().size(); } - const auto at = client.WaitFor([](const auto& r) { return NameOf(r.bytes) == "TRANSFER_TO_WORLD"; }, 0, 1s); + // A recorded move to another world is waited for as long as a zone takes to start + const auto at = client.WaitFor([](const auto& r) { return NameOf(r.bytes) == "TRANSFER_TO_WORLD"; }, 0, + recordedCounts.contains("TRANSFER_TO_WORLD") ? std::chrono::seconds(180) : std::chrono::seconds(1)); if (at >= 0) { ClientPackets::TransferToWorld transfer; if (Read(client.GetReceived()[at].bytes, transfer)) { diff --git a/dCaptureTool/Sandbox.cpp b/dCaptureTool/Sandbox.cpp index 3ef675182..c0e9e1511 100644 --- a/dCaptureTool/Sandbox.cpp +++ b/dCaptureTool/Sandbox.cpp @@ -28,6 +28,15 @@ namespace fs = std::filesystem; using json = nlohmann::json; namespace { + // The running stack's process group: stopped too if the tool is interrupted or crashes + volatile sig_atomic_t g_Running = 0; + + void StopOnSignal(int signal) { + if (g_Running > 0) kill(-g_Running, SIGKILL); + std::signal(signal, SIG_DFL); + std::raise(signal); + } + /** * Settings every sandbox gets over the server's own files. The first value of a key counts, so each key's line * is replaced where it is, and the ones the file doesn't have are added. @@ -181,7 +190,9 @@ namespace Sandbox { } bool Stack::Start(std::string& error) { + for (const int signal : { SIGINT, SIGTERM, SIGSEGV, SIGABRT }) std::signal(signal, StopOnSignal); m_Master = Launch(m_Dir, m_Dir / "MasterServer", m_Dir / "logs" / "master.out"); + g_Running = m_Master; // Auth answering is the sign the stack is up (master starts it after chat and the character select world) const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(120); while (std::chrono::steady_clock::now() < until) { @@ -213,6 +224,7 @@ namespace Sandbox { kill(-m_Master, SIGKILL); waitpid(m_Master, &status, 0); m_Master = 0; + g_Running = 0; } int SetupCommand(const fs::path& bundlePath, const std::string& username, const std::string& password, bool characters, const fs::path& out) {