From 6f63770f22dae5a1ae7e403d3d0cfc02e21a6874 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 11 Aug 2026 13:28:11 -0500 Subject: [PATCH] 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) --- MUNGA/SPOOLER.cpp | 3 ++ MUNGA/SPOOLER.h | 18 ++++++++++ MUNGA_L4/L4NET.CPP | 80 +++++++++++++++++++++++++++++++++++++++++++++ MUNGA_L4/L4SPLR.cpp | 56 +++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+) diff --git a/MUNGA/SPOOLER.cpp b/MUNGA/SPOOLER.cpp index 90a708c..888cc58 100644 --- a/MUNGA/SPOOLER.cpp +++ b/MUNGA/SPOOLER.cpp @@ -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; } diff --git a/MUNGA/SPOOLER.h b/MUNGA/SPOOLER.h index ca19126..6c3ade6 100644 --- a/MUNGA/SPOOLER.h +++ b/MUNGA/SPOOLER.h @@ -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; }; diff --git a/MUNGA_L4/L4NET.CPP b/MUNGA_L4/L4NET.CPP index d25637c..6716692 100644 --- a/MUNGA_L4/L4NET.CPP +++ b/MUNGA_L4/L4NET.CPP @@ -24,6 +24,7 @@ #include "..\munga\appmsg.h" #include "..\munga\mission.h" #include "..\munga\notation.h" +#include "..\munga\spooler.h" //#include @@ -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. // diff --git a/MUNGA_L4/L4SPLR.cpp b/MUNGA_L4/L4SPLR.cpp index 8bff86a..54ecc9e 100644 --- a/MUNGA_L4/L4SPLR.cpp +++ b/MUNGA_L4/L4SPLR.cpp @@ -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; + } } }