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:
Cyd
2026-07-16 22:41:20 -05:00
co-authored by Claude Fable 5
parent 4556187b5c
commit 9d660fa50b
8 changed files with 239 additions and 20 deletions
+65 -20
View File
@@ -392,29 +392,16 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
//
// FRONT-END MODE (BT412 Phase 5): no egg and no network address on the
// command line means there is no operator console to hand us a mission.
// Run the in-game miniconsole menu -- the player configures the race
// (map / mech / color / time / weather / length / pilot name) and clicks
// LAUNCH, which builds the mission egg locally and points the standard
// -egg load path at it. Closing the menu quits. `-egg`/`-net` runs skip
// the menu. (docs/BT412-ROADMAP.md Phase 5.)
// command line means there is no operator console to hand us a mission --
// the in-game miniconsole menu builds one, and the in-process marshal
// runs the single-binary loop (menu -> mission -> menu). `-egg`/`-net`
// runs are a single pass (the developer shortcut / real console).
//
int front_end_mode;
{
const char *cmdline_egg = L4Application::GetEggNotationFileName();
const int no_egg = (!cmdline_egg || !strlen(cmdline_egg));
const int no_net = (L4Application::GetNetworkCommonFlatAddress() == 0);
if (no_egg && no_net)
{
extern int BTFrontEnd_Run();
if (BTFrontEnd_Run())
std::cout << "[frontend] menu LAUNCH -> frontend.egg built"
<< std::endl << std::flush;
else
{
std::cout << "[frontend] menu closed -- exiting" << std::endl << std::flush;
return 0; // player quit from the setup menu
}
}
front_end_mode = (!cmdline_egg || !strlen(cmdline_egg))
&& (L4Application::GetNetworkCommonFlatAddress() == 0);
}
Start_Registering();
@@ -531,6 +518,40 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
ShowWindow(hWnd, nShowCmd);
//=======================================================================//
// SINGLE-BINARY LOOP (BT412 Phase 5), arcade-launcher model: ONE mission
// per process. Front-end mode shows the miniconsole menu, arms the
// marshal, and runs the mission; when the marshal stops it at the chosen
// length the process RELAUNCHES itself (below) -- landing back on the
// setup menu. This is exactly how the arcade launcher restarted the pod
// per mission, and it sidesteps BT's in-process re-init fragility (the
// reconstructed gBT* globals hold per-mission entity pointers that a
// second in-process mission would trip over). `-egg`/`-net` = single pass.
//=======================================================================//
extern int BTFrontEnd_Run();
extern int BTFrontEnd_LastMissionSeconds();
extern void BTLocalConsole_Install(int mission_seconds);
extern int BTLocalConsole_MissionCompleted();
if (front_end_mode)
{
ShowWindow(hWnd, SW_HIDE); // the menu is its own window
if (!BTFrontEnd_Run())
{
std::cout << "[frontend] menu closed -- exiting" << std::endl << std::flush;
Stop_Registering();
return 0; // player quit from the setup menu
}
ShowWindow(hWnd, SW_SHOW);
int secs = BTFrontEnd_LastMissionSeconds();
const char *ov = getenv("BT_MISSION_SECONDS");
if (ov != 0) secs = atoi(ov);
BTLocalConsole_Install(secs);
std::cout << "[frontend] menu LAUNCH -> marshal armed (" << secs << "s)"
<< std::endl << std::flush;
}
{
std::cout << "[boot] opening btl4.res..." << std::endl << std::flush;
StreamableResourceFile resources("btl4.res", version);
@@ -576,5 +597,29 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
}
Stop_Registering();
//-------------------------------------------------------------------//
// LAUNCHER ROLE: if the marshal ended this front-end mission (the
// selected time expired), relaunch a fresh instance -- it front-end-modes
// again and lands back on the setup menu, exactly as the arcade launcher
// restarted the pod per mission. A `-egg`/`-net` pass, or a mission that
// ended some other way (window closed), just exits.
//-------------------------------------------------------------------//
if (front_end_mode && BTLocalConsole_MissionCompleted() && IsWindow(hWnd))
{
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(NULL, exePath, MAX_PATH);
STARTUPINFOW si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);
PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi));
// no args -> the new instance front-end-modes and shows the menu
if (CreateProcessW(exePath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
std::cout << "[launcher] mission ended -- relaunched for the next race"
<< std::endl << std::flush;
}
}
return Exit_Code;
}