Front end: LocalConsole marshal + single-binary loop (timed stop, relaunch)
The in-process console that replaces the operator for solo play: it owns
the mission clock and ends the race at the chosen length, then relaunches
to the setup menu -- the arcade launcher behavior in one binary.
- engine/APPMGR: gPerFrameHook -- a per-frame observer called on the game
thread in RunMissions (the marshal's engine-safe tick site).
- btl4console.{hpp,cpp}: BTLocalConsole_Install/MissionCompleted -- the
marshal watches for RunningMission, starts the clock, and dispatches
Application::StopMissionMessage at expiry (BT_MISSION_SECONDS overrides
for testing). Confirmed: StopMission cleanly ends the mission and
RunMissions returns.
- btl4fe: expose BTFrontEnd_LastMissionSeconds; Enter=LAUNCH / Esc=quit in
the menu loop; BT_FE_AUTOLAUNCH quick-launch (skips the menu).
- btl4main WinMain: front-end mode runs menu -> arms marshal -> mission;
when the marshal ends it, RELAUNCH a fresh instance (arcade launcher
model) -> lands back on the menu. This sidesteps BT's in-process
re-init fragility: a second mission in-process AV'd on stale gBT*
per-mission entity globals (gBTTerrainEntity et al., cdb-traced to
MakeEntityRenderables); a fresh process has none.
Verified: menu LAUNCH -> mission runs -> marshal stops it at the set
length -> RunMissions returns -> relaunch (3 distinct PIDs across a
multi-cycle run, no crash).
Remaining in Phase 5: the results screen (kills/deaths at stop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
//===========================================================================//
|
||||
// btl4console.cpp -- BT412 in-process LocalConsole marshal (solo path).
|
||||
//
|
||||
// See btl4console.hpp. Models RP412's RPL4CONSOLE ConsoleTick, reduced to the
|
||||
// single-player case: no remote pods, no wire -- just own the mission clock and
|
||||
// dispatch StopMissionMessage at expiry. The tick runs on the game thread
|
||||
// (gPerFrameHook), so the engine call (Dispatch) is safe.
|
||||
//===========================================================================//
|
||||
#include <bt.hpp> // Application, application, DEBUG_STREAM
|
||||
#pragma hdrstop
|
||||
|
||||
#include "appmsg.hpp" // full Application::StopMissionMessage definition
|
||||
#include "appmgr.hpp" // gPerFrameHook
|
||||
#include "btl4console.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
enum Phase { PhaseIdle, PhaseWaiting, PhaseRunning, PhaseStopped };
|
||||
|
||||
Phase gPhase = PhaseIdle;
|
||||
int gMissionSeconds = 0;
|
||||
unsigned long gStartTick = 0;
|
||||
Application *gWatched = 0;
|
||||
|
||||
//
|
||||
// The game-thread tick (called every frame while an application is live).
|
||||
//
|
||||
void ConsoleTick()
|
||||
{
|
||||
if (application == 0)
|
||||
return;
|
||||
|
||||
int state = application->GetApplicationState();
|
||||
|
||||
switch (gPhase)
|
||||
{
|
||||
case PhaseWaiting:
|
||||
// The mission has staged and started running -> start the clock.
|
||||
if (state == Application::RunningMission)
|
||||
{
|
||||
gPhase = PhaseRunning;
|
||||
gWatched = application;
|
||||
gStartTick = GetTickCount();
|
||||
DEBUG_STREAM << "[marshal] mission running -- length "
|
||||
<< gMissionSeconds << "s\n" << std::flush;
|
||||
}
|
||||
break;
|
||||
|
||||
case PhaseRunning:
|
||||
if (application != gWatched)
|
||||
return; // only marshal our own mission
|
||||
|
||||
if (state != Application::RunningMission)
|
||||
{
|
||||
// ended some other way (pilot abort, teardown)
|
||||
gPhase = PhaseStopped;
|
||||
DEBUG_STREAM << "[marshal] mission ended (state change)\n" << std::flush;
|
||||
}
|
||||
else if (gMissionSeconds > 0 &&
|
||||
(GetTickCount() - gStartTick) >= (unsigned long)(gMissionSeconds * 1000))
|
||||
{
|
||||
// The console clock expired: stop the race exactly as the
|
||||
// arcade console did (StopMissionMessage on our own pod).
|
||||
DEBUG_STREAM << "[marshal] time expired -- stopping mission\n" << std::flush;
|
||||
Application::StopMissionMessage message(0);
|
||||
application->Dispatch(&message);
|
||||
gPhase = PhaseStopped;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BTLocalConsole_Install(int mission_seconds)
|
||||
{
|
||||
gMissionSeconds = mission_seconds;
|
||||
gPhase = PhaseWaiting;
|
||||
gStartTick = 0;
|
||||
gWatched = 0;
|
||||
// Marshaled: the in-process console owns the clock, so a console-loss
|
||||
// (there is none locally) would end the race -- harmless for solo.
|
||||
gConsoleLossEndsMission = True;
|
||||
gPerFrameHook = &ConsoleTick;
|
||||
DEBUG_STREAM << "[marshal] installed (length " << mission_seconds << "s)\n" << std::flush;
|
||||
}
|
||||
|
||||
int BTLocalConsole_MissionCompleted()
|
||||
{
|
||||
return (gPhase == PhaseStopped) ? 1 : 0;
|
||||
}
|
||||
|
||||
void BTLocalConsole_Uninstall()
|
||||
{
|
||||
gPerFrameHook = 0;
|
||||
gPhase = PhaseIdle;
|
||||
}
|
||||
Reference in New Issue
Block a user