LocalConsole: the in-process marshal that ends missions
Domain correction from playtest: hand-fed eggs are a developer shortcut - a mission only ends on a console command, so the clock hits 00:00 and counts up forever. Even single-player games need a console marshal. RPL4CONSOLE is that console. Like the real one it lives on its own thread: it owns the mission clock and raises the stop request at the selected length; the app-manager per-frame hook (new gPerFrameHook seam in APPMGR, called while the application global is live - the loop condition NULLs it on exit, which ate the first attempt) executes the engine-safe part, dispatching the same StopMissionMessage TeslaConsole sent. Final scores flow in through a new RP-layer sink (gConsoleScoreSink in RPCNSL): RPPlayer feeds it the same score it sends a real console at mission end. It also inherits the launcher role: the application tears down after a stop (arcade pods were relaunched per mission by TeslaLauncher), so WinMain respawns the process when the console ended the mission, landing back on the race-setup screen. L4NetworkManager grows FeedLocalEgg (the single-user egg-inject path, callable mid-session) for the future in-process loop. Verified end to end: menu -> 3:00 race -> stop dispatched exactly on time -> final score collected (host 1 = 4113) -> process respawned with the front end up. -egg runs stay unmarshaled (the dev shortcut). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
#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 (in the
|
||||
// arcade the launcher restarted the pod for the next
|
||||
// mission). WinMain asks ShouldRelaunch() on the way out
|
||||
// and respawns the process, landing back on the setup
|
||||
// screen with the collected results in the log.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RPL4LocalConsole_Install(int mission_seconds)
|
||||
{
|
||||
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_ShouldRelaunch()
|
||||
{
|
||||
return gPhase == PhaseStopped;
|
||||
}
|
||||
Reference in New Issue
Block a user