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:
@@ -271,6 +271,7 @@ add_library(bt410_l4 STATIC
|
||||
"game/reconstructed/btdirect.cpp"
|
||||
"game/reconstructed/btl4app.cpp"
|
||||
"game/reconstructed/btl4fe.cpp"
|
||||
"game/reconstructed/btl4console.cpp"
|
||||
"game/reconstructed/btl4galm.cpp"
|
||||
"game/reconstructed/btl4gau2.cpp"
|
||||
"game/reconstructed/btl4gau3.cpp"
|
||||
|
||||
@@ -12,6 +12,11 @@ HWND ghWnd = 0;
|
||||
// front end / lobby member path (Phase 5/6).
|
||||
Logical gConsoleLossEndsMission = False;
|
||||
|
||||
// BT412: a per-frame observer called each frame in RunMissions while an
|
||||
// application is live (the in-process LocalConsole marshal hangs off this to
|
||||
// own the mission clock). NULL when unregistered.
|
||||
void (*gPerFrameHook)() = NULL;
|
||||
|
||||
ApplicationManager* ApplicationManager::CurrentAppManager = NULL;
|
||||
|
||||
ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this)
|
||||
@@ -81,6 +86,15 @@ Start_Of_Frame:
|
||||
{
|
||||
Check(application);
|
||||
foregroundTasksRun++;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// BT412: give the per-frame observer a slice while 'application' is
|
||||
// live (the loop condition NULLs the global on exit). The
|
||||
// single-player LocalConsole marshal hangs off this; NULL otherwise.
|
||||
//------------------------------------------------------------------
|
||||
if (gPerFrameHook != NULL)
|
||||
(*gPerFrameHook)();
|
||||
|
||||
if (!application->ExecuteForeground(end_of_frame, frameDuration))
|
||||
{
|
||||
if (!application->Shutdown(current_application.GetSize()))
|
||||
|
||||
@@ -9,6 +9,9 @@ extern HWND ghWnd;
|
||||
// False = the arcade -net pod re-listens for its console. See APPMGR.cpp.
|
||||
extern Logical gConsoleLossEndsMission;
|
||||
|
||||
// BT412: per-frame observer hook (the LocalConsole marshal's game-thread tick).
|
||||
extern void (*gPerFrameHook)();
|
||||
|
||||
class ApplicationManager : public Node
|
||||
{
|
||||
public:
|
||||
|
||||
+65
-20
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//===========================================================================//
|
||||
// btl4console.hpp -- BT412 in-process LocalConsole marshal.
|
||||
//
|
||||
// A front-end-launched solo mission has no operator console, so the mission
|
||||
// clock would count up forever. This marshal plays the console's role in
|
||||
// process: it watches the mission via the engine's per-frame hook
|
||||
// (gPerFrameHook, called on the game thread in RunMissions) and dispatches
|
||||
// StopMissionMessage when the selected length expires, exactly as the arcade
|
||||
// console did. The single-binary WinMain loop then collects the result and
|
||||
// cycles back to the setup menu.
|
||||
//===========================================================================//
|
||||
#ifndef BTL4CONSOLE_HPP
|
||||
#define BTL4CONSOLE_HPP
|
||||
|
||||
// Arm the marshal for the next mission: it stops the mission after
|
||||
// `mission_seconds` (0 = endless -- never auto-stops). Registers the
|
||||
// per-frame hook.
|
||||
void BTLocalConsole_Install(int mission_seconds);
|
||||
|
||||
// True once the marshaled mission has stopped (time expired, or ended some
|
||||
// other way while armed) -- the WinMain loop cycles back to the menu.
|
||||
int BTLocalConsole_MissionCompleted();
|
||||
|
||||
// Drop the marshal + clear the per-frame hook.
|
||||
void BTLocalConsole_Uninstall();
|
||||
|
||||
#endif // BTL4CONSOLE_HPP
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -69,4 +69,8 @@ int BTFeMission_WriteEgg(const BTFeMission *mission, const char *path);
|
||||
//---------------------------------------------------------------------------//
|
||||
int BTFrontEnd_Run();
|
||||
|
||||
// The mission length (seconds; 0 = endless) the player chose on the last
|
||||
// LAUNCH -- WinMain arms the LocalConsole marshal with it.
|
||||
int BTFrontEnd_LastMissionSeconds();
|
||||
|
||||
#endif // BTL4FE_HPP
|
||||
|
||||
Reference in New Issue
Block a user