From d00e03ae204fd22c38a7e8adccb639fabdb6e49b Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 11 Aug 2026 14:58:30 -0500 Subject: [PATCH] Playback reads its own header as the first packet The heap corruption is explained, and it was never a memory bug - it was a mis-parse that corrupted the heap downstream. One line of instrumentation in DispatchPacket did it: Playback: packet client=0 message=3 length=1 -> the NETWORK MANAGER Against the spool's header - appID 0, major 3, (remote 0, id 2), (remote 1, id 3) - every field lines up: clientID 0 <- appID gameID 3 <- major version fromHost 0 <- host 1 remote timeStamp 2 <- host 1 id messageLength 1 <- host 2 remote messageID 3 <- host 2 id So the spooler task is parsing the 24 byte header as packet one. Client 0 message 3 is ReceiveEggFile, which builds a Mission - a SECOND one, from a packet - and that is the crash in Mission::Mission. The cursor is at byte zero because L4PlaybackNetworkManager::StartConnecting, which is what consumes the header, had not run yet. Its own log line for a camera station never appeared, and Application::CreateMission shows up in the stack UNDER SpoolerTask::Execute rather than under the egg message posted from the constructor. The task begins dispatching before the mission exists, and nothing stops it. That also explains why the wrong-egg guard stayed silent: it inspects the first packet in StartConnecting, which is correct when it runs, and it simply never got the chance. Left in: DispatchPacket names the client ID, message ID, length and the client each packet resolved to, for the first two dozen packets. Client IDs and message IDs are both small integers counted from the same base - NetworkClient::NextMessageID is 3, so the interest manager's NewDynamicEntity and the network manager's ReceiveEggFile are BOTH message 3 - which is exactly why a mis-routed packet looked valid to whoever received it. A NULL client is now returned from rather than called through, since Check() is a no-op in release. Next: the spooler task must not dispatch until the mission is loaded and the header consumed. Co-Authored-By: Claude Opus 5 (1M context) --- RP_L4/RPL4PB.cpp | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/RP_L4/RPL4PB.cpp b/RP_L4/RPL4PB.cpp index 86ae448..f275dc1 100644 --- a/RP_L4/RPL4PB.cpp +++ b/RP_L4/RPL4PB.cpp @@ -419,6 +419,47 @@ void NetworkManager *net_mgr = GetNetworkManager(); Check(net_mgr); NetworkClient *client = net_mgr->GetNetworkClientPointer(packet->clientID); + + // + // Say what is being routed where, for the first few. + // + // Playback dies inside Mission::Mission, reached through the egg file + // handler, while dispatching spooled packets - so a packet is arriving + // at a client that is not the one it was recorded for. Client IDs and + // message IDs are both small integers counted from the same base + // (NetworkClient::NextMessageID is 3, so the interest manager's + // NewDynamicEntity and the network manager's ReceiveEggFile are BOTH + // message 3), which makes a mis-routed packet look perfectly valid to + // whoever receives it. Naming the pair and the client it resolved to + // ends the guessing. + // + // A NULL client is the other candidate: Check() is a no-op in a + // release build, so a missing interest manager would be a call through + // nothing rather than a complaint. + // + { + static int said = 0; + + if (said < 24) + { + ++said; + DEBUG_STREAM << "Playback: packet client=" << (int) packet->clientID + << " message=" << (int) packet->messageData.messageID + << " length=" << (int) packet->messageData.messageLength + << " -> client " << (void *) client + << (client == NULL ? " (NULL!)" : "") + << (client == (NetworkClient *) net_mgr ? " = the NETWORK MANAGER" : "") + << (client == (NetworkClient *) this ? " = the application" : "") + << (client == (NetworkClient *) GetInterestManager() + ? " = the interest manager" : "") + << "\n" << std::flush; + } + } + + if (client == NULL) + { + return; + } Check(client); client->ReceiveNetworkPacket(packet, &packet->messageData); Check_Fpu();