A spool needs a header, and a world to play into

Two more blockers down, found by watching it rather than reasoning about
it - Cyd reported a black screen with a full pod cockpit over it, and both
halves of that turned out to be real and separate.

The black screen: playback had no mission. L4NetworkManager reads the -egg
file and posts a ReceiveEggFileMessage in single user mode, which is what
ends in CreateMission and builds the world - but that is the POD's network
manager. L4PlaybackNetworkManager descends from NetworkManager and
inherited none of it, so the playback application came up with a cockpit,
nothing behind it, and not a word in the log. A spool records what MOVED,
never the track it moved through, so the egg is not optional.

The cockpit: Application::SetCameraStation is the front end's answer to a
question asked on the setup screen, and playback never sees the setup
screen, so a Live Cam recording replayed as a pod - five instrument panes
over the view, map back in the middle. The egg knows what the station was;
it is the same egg the race ran on. Read it from the host type instead.

Then playback got far enough to reject the spool outright:

    Error - Not a spool file for this application!
    Error - Spool file major data version should be 3, not 0!

Correct of it. A spool opens with the application ID, the resource major
version, and one (remote, hostID) pair per egg host, and SpoolRecorder was
writing packets and nothing else - so playback read a zero where the
application ID belonged. The header is written now, in
L4NetworkManager::StartConnecting, because every field in it is
network-layer knowledge and that is the first moment all of it exists.

NOTE the recordings made before this cannot be played back. They have no
header, and there is nothing in the file to reconstruct one from - the
host table describes machines that were on the wire at the time. A race
recorded from here on will have one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 13:28:11 -05:00
co-authored by Claude Opus 5
parent ae23c040ce
commit 6f63770f22
4 changed files with 157 additions and 0 deletions
+3
View File
@@ -197,6 +197,7 @@ SpoolRecorder::SpoolRecorder():
bufferSize(0),
armed(False),
full(False),
headerWritten(False),
packetsRecorded(0)
{
}
@@ -235,6 +236,7 @@ Logical
spool->spoolState = SpoolFile::Spooling;
armed = True;
full = False;
headerWritten = False;
packetsRecorded = 0;
DEBUG_STREAM << "Record: armed, " << (bufferSize / (1024 * 1024))
@@ -388,6 +390,7 @@ void
bufferSize = 0;
armed = False;
full = False;
headerWritten = False;
packetsRecorded = 0;
}
+18
View File
@@ -94,12 +94,30 @@ public:
IsArmed() const
{ return armed; }
//
// A spool opens with a header - the application ID, the resource
// major version, and one (remote, hostID) pair per host named in the
// egg - and playback refuses a spool without it. All of that is
// network-layer knowledge, so it is written by the network manager
// once the hosts exist, and these three are what let it.
//
SpoolFile*
GetSpool()
{ return spool; }
Logical
HeaderWritten() const
{ return headerWritten; }
void
MarkHeaderWritten()
{ headerWritten = True; }
protected:
char *buffer;
SpoolFile *spool;
size_t bufferSize;
Logical armed;
Logical full;
Logical headerWritten;
int packetsRecorded;
};
+80
View File
@@ -24,6 +24,7 @@
#include "..\munga\appmsg.h"
#include "..\munga\mission.h"
#include "..\munga\notation.h"
#include "..\munga\spooler.h"
//#include <netnub.hpp>
@@ -567,6 +568,85 @@ void
nextOpenHostID++;
}
//
// A recording starts with a header, and this is where it can be written.
//
// A spool opens with the application ID, the resource major version,
// and one (remote, hostID) pair per host named in the egg - playback
// reads them straight back in L4PlaybackNetworkManager::StartConnecting
// and refuses the file without them. The first recordings this build
// made were packets only, so playback met a zero where the application
// ID should be and said "Not a spool file for this application".
//
// It goes here rather than in the recorder because every part of it is
// network-layer knowledge, and here is the moment all of it first
// exists: the hosts have just been created and the mission is in hand.
//
if (Application::IsRecording())
{
SpoolRecorder *recorder = SpoolRecorder_Get();
if (!recorder->IsArmed())
{
recorder->Arm();
}
if (recorder->IsArmed() && !recorder->HeaderWritten())
{
SpoolFile *header_spool = recorder->GetSpool();
ResourceFile *res_file = application->GetResourceFile();
Check(res_file);
*(ApplicationID*)header_spool->GetPointer() =
application->GetApplicationID();
header_spool->AdvancePointer(sizeof(ApplicationID));
int major_version = res_file->versionArray[1];
*(int*)header_spool->GetPointer() = major_version;
header_spool->AdvancePointer(sizeof(major_version));
HostManager *header_host_mgr = application->GetHostManager();
Check(header_host_mgr);
Mission::HostIterator header_iterator(mission);
MissionHostData *header_host_data;
int header_host_count = 0;
while ((header_host_data = header_iterator.ReadAndNext()) != NULL)
{
CString header_name(header_host_data->GetAddressString());
SOCKADDR_IN header_address;
NetTransport_Get()->Resolve((LPSTR)header_name, &header_address);
if (header_address.sin_port == 0)
{
header_address.sin_port = htons(localGamePort);
}
Host *header_host = header_host_mgr->FindHost(header_address);
//
// Same guard as the arcade spooler needs: a host can be in
// the egg without being connected, and the pair still has to
// be written or every entry after it shifts.
//
*(Logical*)header_spool->GetPointer() = (header_host == NULL)
? True
: (header_host != header_host_mgr->GetLocalHost());
header_spool->AdvancePointer(sizeof(Logical));
*(HostID*)header_spool->GetPointer() = (header_host == NULL)
? (HostID) 0
: header_host->GetHostID();
header_spool->AdvancePointer(sizeof(HostID));
++header_host_count;
}
recorder->MarkHeaderWritten();
DEBUG_STREAM << "Record: spool header written, " << header_host_count
<< " host(s) from the egg\n" << std::flush;
}
}
//
// All the hosts are created, the connects/listens done.
//
+56
View File
@@ -200,6 +200,42 @@ void
L4PlaybackNetworkManager::L4PlaybackNetworkManager():
NetworkManager(L4PlaybackNetworkManager::DefaultData)
{
//
// Give this application its mission.
//
// Nothing else will. A spool is a record of what MOVED, not of the
// world it moved through - no track, no models, no drop zones - and
// playback has neither a wire to be sent an egg over nor a console to
// send one. L4NetworkManager does exactly this for single user mode,
// but that is the pod's network manager; this one descends from
// NetworkManager and inherited none of it, so a playback build came up
// with a cockpit, no world behind it, and nothing in the log to say
// why. A black screen is what that looks like.
//
const char *egg_name =
((L4Application *) application)->GetEggNotationFileName();
if (egg_name == NULL || strlen(egg_name) == 0)
{
DEBUG_STREAM << "Playback: no egg given. A spool records the race but"
<< " not the track it was run on, so -egg must name the one it"
<< " was recorded with.\n" << std::flush;
return;
}
DEBUG_STREAM << "Playback: loading world from egg '" << egg_name
<< "'\n" << std::flush;
networkEggNotationFile = new NotationFile(egg_name);
Register_Object(networkEggNotationFile);
//
// The handler is NetworkManager's, and it ends in CreateMission, which
// is what calls StartConnecting below to read the host table back out
// of the spool.
//
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
application->Post(DefaultEventPriority, this, &egg_message);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -271,6 +307,26 @@ void
else
{
host_mgr->AdoptLocalHost(my_host);
//
// Lay the cockpit out as whatever this station WAS.
//
// Application::SetCameraStation is normally the front end's
// answer to a question asked on the setup screen, and playback
// never sees the setup screen - so a recording made from a Live
// Cam replayed with five instrument panes hung over the view
// and the map back in the middle, which is a pod's cockpit, not
// a camera's. The egg knows: it is the same egg the race ran
// on, and it says what this host was.
//
Application::SetCameraStation(
mission_host_data->GetHostType() == CameraShipHostType
);
if (mission_host_data->GetHostType() == CameraShipHostType)
{
DEBUG_STREAM << "Playback: this station recorded as a Live Cam"
<< " - no instrument panes, map landscape\n" << std::flush;
}
}
}