Mission review gets far enough to load the race

Starting the MR port, since the pods want it too. Four changes, and one
correction of my own making.

A release Fail now says what failed. It expanded to a bare abort() that
threw the message away, and because the compiler merges identical cold
paths, every Fail in a function became one anonymous stub - a crash named
the function and nothing else. It now prints message, file and line.

It also writes that line to rpl4-fail.log and closes the file. rpl4.log is
std::cout with an ofstream's streambuf swapped in, and the text written
there did NOT survive the exit however it was flushed; rather than guess
where it stopped, the one line that matters goes to its own file, and
fclose is a promise it reached the disk. Its own file because rpl4.log is
already open for writing and Windows will not share it.

-pb plays a spool back without recording a new one. Mode 2 has been in
RPL4.CPP all along - one spool file instead of two, no spooling
application - with no way to ask for it. -mr starts a recorder alongside
the playback and that recorder is the half that cannot cope away from a
pod bay.

Which is the third fix: L4SpoolingNetworkManager::StartConnecting walks
the hosts named in the egg, and called host->GetHostID() on whatever
FindHost returned. FindHost answers NULL for a host that is in the egg but
not connected - impossible in an arcade, ordinary everywhere else - and it
crashed before the mission could start. The table is read back one pair
per egg host in egg order, so a missing host cannot be skipped without
shifting every entry after it; it writes the pair, says so, and carries on.

And RPL4.CPP chose between the playback application and an idle one in
silence. For the first few seconds those look identical from outside, so a
spool that failed to load was indistinguishable from one that had not
started yet. It now says which, and why not.

The correction: I reported a failure in L4AudioRenderer::Initialize on
audiomr.ini's clipping_radius. That was mine. I was running cdb from the
source tree, so the game looked for audio\audiomr.ini beside the debugger
rather than beside itself and found nothing. With the working directory
right, audio initialises fine and always did.

Where it stands: -pb loads last.spl - "2973012 bytes to play" - creates
RPL4PlaybackApplication and runs without crashing. Whether it puts the
race on the screen is the next thing to look at, and that wants eyes
rather than a log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 13:15:20 -05:00
co-authored by Claude Opus 5
parent 46418ebb5c
commit ae23c040ce
6 changed files with 158 additions and 4 deletions
+11
View File
@@ -91,3 +91,14 @@ RedPlanet-*.zip
# in the history: a minidump carries process memory and the sender's own
# file paths.
/Crashdmp/
# Runtime files the game writes beside itself when run from the tree.
# environ.ini in particular is GENERATED from the template in
# RP_L4/RPL4ENVIRON.cpp - a copy committed here would be a second source
# of truth that nothing updates.
bindings.txt
environ.ini
rpl4.log
rpl4-fail.log
last.spl
SPOOLS/
+19 -2
View File
@@ -58,6 +58,23 @@
#define Check(p)
#define Check_Signature(p)
//#define Fail(m) Fail_To_Debugger(m,__FILE__,__LINE__)
#define Fail(m) abort();
//
// A release build used to answer Fail("clipping_radius not defined") with a
// bare abort(), throwing the message away - and because the compiler merges
// identical cold paths, every Fail in a function became the same anonymous
// stub. A crash then told you the function and nothing else, which cost an
// afternoon of disassembly to learn that a mission review build stops in
// L4AudioRenderer::Initialize without ever saying which of its nine checks
// was the one that tripped.
//
// The debug build has always routed this to a function that prints the
// message with its file and line. Release now does too. The termination is
// unchanged - still abort(), still exit code 0xC0000409 - so nothing
// downstream sees anything different; it just says what happened on the way
// out.
//
extern void Fail_With_Message(const char *message, const char *file, int line);
#define Fail(m) Fail_With_Message(m, __FILE__, __LINE__)
#define Cast_Object(type, ptr) ((type)(ptr))
+64
View File
@@ -133,6 +133,70 @@ void Verify_Failed(char *Message, char *File, int Line)
}
}
//
//#############################################################################
//#############################################################################
//
//
// The release build's Fail. Says what went wrong before it goes.
//
// Terminates exactly as the old bare abort() did - same call, same
// 0xC0000409 - so no caller, script or crash handler sees a change. The
// only difference is the line in the log naming the check that failed and
// where it lives.
//
// The emergency shutdowns are worth doing before the process ends even
// though it is ending: the gauge renderer and the controls manager both
// hold hardware, and a display left mid-mode is a nuisance to whoever has
// to pick the machine up afterwards - which in a pod bay is not the
// person who was playing.
//
void
Fail_With_Message(const char *Message, const char *File, int Line)
{
if (!AlreadyFailed)
{
AlreadyFailed = True;
DEBUG_STREAM << "FAIL " << File << "(" << Line << "): " << Message
<< "\n" << std::flush;
//
// And again, somewhere that cannot be lost.
//
// rpl4.log is std::cout with its streambuf swapped for an ofstream's
// (see WinMain), and the text written above did NOT survive to disk
// across the exit - flushed as far as the stream and no further.
// Rather than guess at where it stops, write the one line that
// matters to its own file and close it: fclose is a promise the
// bytes are on the disk, and a file opened and closed inside this
// function cannot be left dangling by whatever happens next.
//
// Its own file rather than an append to rpl4.log because that one is
// already open for writing and Windows will not share it.
//
FILE *record = fopen("rpl4-fail.log", "w");
if (record != NULL)
{
fprintf(record, "FAIL %s(%d): %s\n", File, Line, Message);
fclose(record);
}
//
// Hardware last, and after the record is safely written: the gauge
// renderer and the controls manager both hold devices, and a display
// left mid-mode is a nuisance for whoever picks the machine up - in
// a pod bay, not the person who was playing. If one of these faults
// on the way down, the failure has already been recorded.
//
SystemClock::timer.Shutdown();
ControlsManager::Shutdown();
GaugeRenderer::EmergencyShutdown();
}
abort();
}
//
//#############################################################################
//#############################################################################
+20
View File
@@ -240,6 +240,9 @@ Logical
{
DEBUG_STREAM << "\n" << argv[0] <<
" -egg <filename> -net <memory_address> -solids -h -help\n"
" -mr mission review: play a spool back and\n"
" record a new one alongside it\n"
" -pb play a spool back only, no recording\n"
" -windowed windowed, title bar and all\n"
" -fit borderless over the whole monitor,\n"
" render size chosen to match\n"
@@ -403,6 +406,23 @@ Logical
{
suppressGauges = TRUE;
missionReviewMode = 1;
}
//
// Play a spool back without recording a new one.
//
// Mode 2 was always in RPL4.CPP - it takes one spool file
// instead of two and skips the spooling application - there was
// simply no way to ask for it. -mr starts a recorder alongside
// the playback, and that recorder is the half that falls over
// on a machine which is not a pod bay: it walks the hosts named
// in the egg expecting to find every one of them connected.
//
// Watching a race back does not need a recorder at all.
//
else if (!stricmp(W2A(argv[i]), "-pb"))
{
suppressGauges = TRUE;
missionReviewMode = 2;
} else if (!(*parser)(&i, argc, argv))
{
return False;
+30 -2
View File
@@ -124,10 +124,38 @@ void
ResolveAddress(host_name, &net_address);
Host *host = host_mgr->FindHost(net_address);
//
// FindHost answers NULL for a host named in the egg that is not
// actually connected, and this went straight on to call
// host->GetHostID(). In an arcade every station in the egg is on
// the wire, so the case could not arise; anywhere else it is the
// ordinary state of affairs, and it crashed the spooling
// application before the mission could even start.
//
// The table below is read back one pair per egg host, in egg
// order, so a missing host cannot simply be skipped - that would
// shift every entry after it. Write the pair, say what happened,
// and carry on.
//
if (host == NULL)
{
DEBUG_STREAM << "Spool: host '" << host_name
<< "' is in the egg but not connected - recording it as"
<< " remote with no ID. Packets from it will not map back.\n"
<< std::flush;
*(Logical*)spool->GetPointer() = True;
spool->AdvancePointer(sizeof(Logical));
*(HostID*)spool->GetPointer() = (HostID) 0;
spool->AdvancePointer(sizeof(HostID));
continue;
}
*(Logical*)spool->GetPointer() = (host != host_mgr->GetLocalHost());
spool->AdvancePointer(sizeof(Logical));
*(HostID*)spool->GetPointer() = host->GetHostID();
spool->AdvancePointer(sizeof(HostID));
}
+14
View File
@@ -638,12 +638,26 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
if (!spoolFileName)
spoolFileName = "last.spl";
//
// Say which of the two this is. The choice was made in silence,
// and the two look identical from outside for the first few
// seconds - one plays a race back, the other sits there - so a
// spool that failed to load was indistinguishable from a spool
// that loaded and had not started yet.
//
SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName);
if (spool)
{
DEBUG_STREAM << "Playback: " << spoolFileName
<< " loaded, " << (int) spool->GetBytesRemaining()
<< " bytes to play\n" << std::flush;
new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool);
} else
{
DEBUG_STREAM << "Playback: could not load '" << spoolFileName
<< "' - starting idle with nothing to show. It must exist, be"
<< " no larger than " << (int) SPOOL_SIZE
<< " bytes, and have been written by this build.\n" << std::flush;
new_app = new RPL4IdleApplication(hInstance, hWnd, &resources);
}