Playback plays
Two faults, and playback runs. The spooler task was reading before the header came off the stream. The header's length depends on the egg's host count, so it can only be taken off once the mission exists - which is L4PlaybackNetworkManager::StartConnecting - and until then the cursor sits on the header. The task parsed it as packet one: our application ID is 0, which is NetworkManagerClientID exactly, and the two host IDs behind it read as a message length of 1 and a message ID of 3. Message 3 on the network manager is ReceiveEggFile, so it built a Mission from the header and died inside it. The task now waits for SpoolHeaderConsumed, set only after the header has been read AND passed its sanity check, so a spool that does not add up is never played at all. The arcade's WaitingForEgg case, which replays an egg carried in the spool as network manager packets, cannot work with this format in any case: reading those packets means passing the header first, and passing the header means already knowing the egg. A recording made by this build keeps its egg beside it, so there is no such circle. And last.egg had three other writers, not one. Every network manager dumps the egg it loaded to that name as a debug aid - networkEggNotationFile->WriteFile - and it writes a notation image, 101,920 bytes of it, straight over the 9,470 byte text egg saved with the recording. Playback then read that as its egg, found no map entry, and Mission::Mission carried on past its own PostQuitMessage with an uninitialised map name to dereference a NULL resource. All three dumps are last-loaded.egg now, and last.egg belongs to the recording alone. Worth noting the copy was never wrong: SPOOLS\<stamp>.egg is byte-identical to the frontend.egg it came from. Only the last.egg convenience copy was being overwritten, which is the sort of thing that looks like a corrupt recording and is not. Where it stands: -pb loads the spool, takes the egg saved beside it, lays the cockpit out as the Live Cam it was recorded from, consumes the header, and dispatches the race's packets to the interest manager with sane lengths, without crashing. Whether the pods MOVE on screen is the next thing to look at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -287,7 +287,7 @@ void
|
||||
networkEggNotationFile = new NotationFile();
|
||||
Register_Object(networkEggNotationFile);
|
||||
networkEggNotationFile->ReadText(eggTempBuffer, eggTempNext);
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
|
||||
//
|
||||
// Now turn the notation file into a mission
|
||||
|
||||
+2
-2
@@ -387,7 +387,7 @@ void
|
||||
|
||||
networkEggNotationFile = new NotationFile(egg_path);
|
||||
Register_Object(networkEggNotationFile);
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
|
||||
// In network mode (the owner pod of a multiplayer race) the state
|
||||
// gate must open or CheckBuffers keeps dropping mesh packets - a
|
||||
@@ -907,7 +907,7 @@ void
|
||||
#if defined(LAB_ONLY)
|
||||
DEBUG_STREAM << "Created egg\n";
|
||||
#endif
|
||||
networkEggNotationFile->WriteFile("last.egg");
|
||||
networkEggNotationFile->WriteFile("last-loaded.egg");
|
||||
currentNetworkState = NormalState;
|
||||
|
||||
//
|
||||
|
||||
@@ -195,11 +195,30 @@ void
|
||||
//###################### L4PlaybackNetworkManager #########################
|
||||
//#############################################################################
|
||||
|
||||
//
|
||||
// Whether the spool's header has been taken off the front of the stream, so
|
||||
// SpoolerTask::Execute knows when the cursor is standing on a packet rather
|
||||
// than on the header. Cleared when a playback manager is built, since the
|
||||
// single-binary loop can play a second spool in the same process.
|
||||
//
|
||||
namespace
|
||||
{
|
||||
Logical gSpoolHeaderConsumed = False;
|
||||
}
|
||||
|
||||
Logical
|
||||
SpoolHeaderConsumed()
|
||||
{
|
||||
return gSpoolHeaderConsumed;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
L4PlaybackNetworkManager::L4PlaybackNetworkManager():
|
||||
NetworkManager(L4PlaybackNetworkManager::DefaultData)
|
||||
{
|
||||
gSpoolHeaderConsumed = False;
|
||||
|
||||
//
|
||||
// Give this application its mission.
|
||||
//
|
||||
@@ -400,6 +419,16 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The cursor is now standing on the first packet, so the spooler task
|
||||
// may start reading. Set only after the sanity check above, so a spool
|
||||
// whose header did not add up is never played at all.
|
||||
//
|
||||
gSpoolHeaderConsumed = True;
|
||||
DEBUG_STREAM << "Playback: header consumed, "
|
||||
<< (int) spool->GetBytesRemaining() << " bytes of packets to play\n"
|
||||
<< std::flush;
|
||||
|
||||
//
|
||||
// Now, just send the load message
|
||||
//
|
||||
@@ -817,6 +846,32 @@ void
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Nothing may be read until the header has been taken off the front.
|
||||
//
|
||||
// A spool opens with the application ID, the resource version and one
|
||||
// (remote, hostID) pair per host named in the egg, and the length of
|
||||
// that depends on the egg - so it can only be read once the mission
|
||||
// exists, which is what L4PlaybackNetworkManager::StartConnecting does.
|
||||
// Until then the cursor is sitting on the header, and this function
|
||||
// happily parsed it as packet one: the application ID read as a client
|
||||
// ID, and ours is 0, which is NetworkManagerClientID exactly. The pair
|
||||
// of host IDs after it read as a message length of 1 and a message ID
|
||||
// of 3 - and message 3 on the network manager is ReceiveEggFile, which
|
||||
// builds a Mission. From a packet. That was the heap corruption.
|
||||
//
|
||||
// The WaitingForEgg case below is for an arcade spool that CARRIES its
|
||||
// egg as network manager packets, which cannot work with this format
|
||||
// anyway: reading those packets means passing the header first, and
|
||||
// passing the header means already knowing the egg. A recording made
|
||||
// by this build keeps its egg beside it instead, which is why there is
|
||||
// no such circle to break.
|
||||
//
|
||||
if (!SpoolHeaderConsumed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// We have a spool file, so interpret it based upon the application state
|
||||
|
||||
@@ -54,6 +54,14 @@ public:
|
||||
//################### L4PlaybackNetworkManager #########################
|
||||
//##########################################################################
|
||||
|
||||
// True once a spool's header has been read off the front of the stream, so
|
||||
// the cursor is standing on a packet. SpoolerTask must not read before
|
||||
// this: the header's length depends on the egg's host count, so it can only
|
||||
// be taken off once the mission exists, and a task that reads early parses
|
||||
// the header as a packet. See SpoolerTask::Execute.
|
||||
Logical
|
||||
SpoolHeaderConsumed();
|
||||
|
||||
class L4PlaybackNetworkManager:
|
||||
public NetworkManager
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user