Files
RP412/RP_L4/RPL4CONSOLE.cpp
T
CydandClaude Fable 5 570eb3aceb Single-binary race loop: menu -> race -> menu in one process
WinMain now wraps the engine block in a loop: when a front-end-launched
mission ends under the local console, the setup screen comes back in the
same process instead of exiting (the arcade relaunch-per-mission model).
Replaces the CreateProcess self-respawn - required for Steam, where the
lobby and sockets must survive across races.

Second-cycle re-init crash fixed: d3d_OBJECT kept a static texture cache
keyed by filename, so race 2 got IDirect3DTexture9 pointers created on
race 1 destroyed device and died at first draw (DrawMesh AV). The cache
is now flushed in ~DPLRenderer before the device is released, and
ParticleEngine::Initialize drops particles left over from the previous
mission. Verified: three consecutive 30s races in one PID, each stopped
on time by the console with final scores collected.

Also: L4CONSOLELEN env override for test-length races, and the console
exposes MissionCompleted() for the loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:48:56 -05:00

199 lines
5.7 KiB
C++

#include "..\munga_l4\mungal4.h"
#pragma hdrstop
#include "rpl4console.h"
#include "rpl4fe.h"
#include "..\munga\appmgr.h"
#include "..\munga\appmsg.h"
#include "..\rp\rpcnsl.h"
#include "..\munga_l4\l4app.h"
#include "..\munga_l4\l4net.h"
//########################################################################
// The local console runs on ITS OWN THREAD, like the real console: it
// stays alive across the whole session, owns the mission clock, and
// raises the stop request when the selected length expires. The game
// thread's per-frame tick is the only place engine calls happen - it
// reports state transitions to the console thread and executes the
// requested StopMissionMessage dispatch (the engine is single
// threaded; cross-thread dispatch is not safe).
//
// Results flow in through gConsoleScoreSink (RP layer): the same final
// scores RPPlayer sent the arcade console at mission end.
//########################################################################
namespace
{
enum ConsolePhase
{
PhaseWaiting = 0, // waiting for the mission to start running
PhaseRunning, // mission running, console thread watching the clock
PhaseStopped // stop dispatched, waiting for teardown
};
ConsolePhase gPhase = PhaseWaiting;
int gMissionSeconds = 0;
Application *gWatchedApp = NULL;
HANDLE gConsoleThread = NULL;
// shared with the console thread
volatile LONG gMissionRunning = 0;
volatile LONG gStopRequested = 0;
volatile LONG gShuttingDown = 0;
volatile LONG gRunStartTick = 0;
volatile LONG gLengthMs = 0;
// collected mission results (this session's last race)
enum { maxResults = 16 };
struct FinalScore
{
int hostID;
int score;
};
FinalScore gResults[maxResults];
int gResultCount = 0;
//---------------------------------------------------------------
// The console thread: the mission clock lives here
//---------------------------------------------------------------
DWORD WINAPI ConsoleThreadProc(LPVOID)
{
while (!gShuttingDown)
{
Sleep(250);
if (gMissionRunning && !gStopRequested)
{
LONG length_ms = gLengthMs;
if (length_ms > 0 &&
(LONG)(GetTickCount() - (DWORD) gRunStartTick) >= length_ms)
{
InterlockedExchange(&gStopRequested, 1);
}
}
}
return 0;
}
//---------------------------------------------------------------
// Final-score intake (called on the game thread from RPPlayer's
// mission-ending path, via the RP-layer sink)
//---------------------------------------------------------------
void CollectFinalScore(int host_ID, int score)
{
if (gResultCount < maxResults)
{
gResults[gResultCount].hostID = host_ID;
gResults[gResultCount].score = score;
++gResultCount;
}
DEBUG_STREAM << "LocalConsole: final score, host " << host_ID
<< " = " << score << "\n" << std::flush;
}
//---------------------------------------------------------------
// The game-thread tick: state reporting + engine-safe execution
//---------------------------------------------------------------
void ConsoleTick()
{
if (application == NULL)
{
return;
}
if (gPhase != PhaseWaiting && application != gWatchedApp)
{
return;
}
int state = application->GetApplicationState();
switch (gPhase)
{
case PhaseWaiting:
if (state == Application::RunningMission)
{
gPhase = PhaseRunning;
gWatchedApp = application;
gResultCount = 0;
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
InterlockedExchange(&gStopRequested, 0);
InterlockedExchange(&gMissionRunning, 1);
DEBUG_STREAM << "LocalConsole: mission running, length "
<< gMissionSeconds << "s\n" << std::flush;
}
break;
case PhaseRunning:
if (state != Application::RunningMission)
{
// mission ended some other way (pilot exit etc.)
InterlockedExchange(&gMissionRunning, 0);
gPhase = PhaseStopped;
}
else if (gStopRequested)
{
//-----------------------------------------------------
// The console clock expired: end the race exactly the
// way the arcade console did, otherwise the mission
// clock rolls past 00:00 and counts up forever.
//-----------------------------------------------------
DEBUG_STREAM << "LocalConsole: time expired - stopping mission\n" << std::flush;
InterlockedExchange(&gMissionRunning, 0);
Application::StopMissionMessage message(0);
application->Dispatch(&message);
gPhase = PhaseStopped;
}
break;
case PhaseStopped:
// The application tears itself down after a stop (arcade
// pods were relaunched per mission). WinMain's race loop
// asks MissionCompleted() and cycles back to the setup
// screen in the same process.
break;
}
}
}
void
RPL4LocalConsole_Install(int mission_seconds)
{
// debug: L4CONSOLELEN overrides the mission length (test races)
const char *override_string = getenv("L4CONSOLELEN");
if (override_string != NULL && atoi(override_string) > 0)
{
mission_seconds = atoi(override_string);
DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, "
<< mission_seconds << "s\n" << std::flush;
}
gMissionSeconds = mission_seconds;
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
gPhase = PhaseWaiting;
gWatchedApp = NULL;
// game-thread execution point
gPerFrameHook = &ConsoleTick;
// results intake from the RP layer
gConsoleScoreSink = &CollectFinalScore;
// the console itself lives on its own thread, like the real one
if (gConsoleThread == NULL)
{
gConsoleThread = CreateThread(
NULL, 0, ConsoleThreadProc, NULL, 0, NULL);
}
DEBUG_STREAM << "LocalConsole: installed (length "
<< mission_seconds << "s, console thread "
<< (gConsoleThread != NULL ? "up" : "FAILED") << ")\n" << std::flush;
}
Logical
RPL4LocalConsole_MissionCompleted()
{
return gPhase == PhaseStopped;
}