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
+26
View File
@@ -20,6 +20,9 @@
#include "btl4fe.hpp"
#include "l4app.hpp" // L4Application::SetEggNotationFileName
// Mission length (seconds) chosen on the last LAUNCH; WinMain arms the marshal.
static int gLastMissionSeconds = 300;
//---------------------------------------------------------------------------//
// Catalogs -- the BattleTech content the console's RPConfig.xml named.
// Maps: the 8 in BTL4.RES. Mechs: the base 8 + the known variants.
@@ -536,6 +539,17 @@ LRESULT CALLBACK MenuWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
//---------------------------------------------------------------------------//
int BTFrontEnd_Run()
{
// BT_FE_AUTOLAUNCH: skip the interactive menu and launch the default
// loadout straight away (quick-launch / deterministic marshal testing).
if (getenv("BT_FE_AUTOLAUNCH") != NULL) {
BTFeMission mission; BTFeMission_Default(&mission);
const char *egg = "frontend.egg";
if (!BTFeMission_WriteEgg(&mission, egg)) return 0;
L4Application::SetEggNotationFileName(egg);
gLastMissionSeconds = mission.lengthSeconds;
return 1;
}
HINSTANCE instance = GetModuleHandleA(NULL);
MenuState m; memset(&m, 0, sizeof(m));
@@ -583,6 +597,12 @@ int BTFrontEnd_Run()
while (!m.launched && !m.closed) {
BOOL r = GetMessageA(&msg, NULL, 0, 0);
if (r <= 0) { m.closed = 1; break; }
// Enter = LAUNCH, Escape = quit -- intercepted at the loop level so
// they fire whichever child (menu or name box) has focus.
if (msg.message == WM_KEYDOWN) {
if (msg.wParam == VK_RETURN) { m.launched = 1; continue; }
if (msg.wParam == VK_ESCAPE) { m.closed = 1; continue; }
}
if (msg.message == WM_QUIT) m.closed = 1;
TranslateMessage(&msg);
DispatchMessageA(&msg);
@@ -620,5 +640,11 @@ int BTFrontEnd_Run()
if (!BTFeMission_WriteEgg(&mission, egg_path))
return 0;
L4Application::SetEggNotationFileName(egg_path);
gLastMissionSeconds = mission.lengthSeconds;
return 1;
}
int BTFrontEnd_LastMissionSeconds()
{
return gLastMissionSeconds;
}