Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1352 lines
57 KiB
C++
1352 lines
57 KiB
C++
//===========================================================================//
|
|
// File: btl4main.cpp //
|
|
// Project: BattleTech Brick: BattleTech LBE Application launcher //
|
|
//---------------------------------------------------------------------------//
|
|
// Win32 entry point for the reconstructed BattleTech port. //
|
|
// //
|
|
// Reconstructed/adapted from the Red Planet launcher RP_L4\RPL4.CPP -- the //
|
|
// only game-specific differences are the application class (BTL4Application //
|
|
// vs RPL4Application), the resource file (BTL4.RES), and the program name. //
|
|
// The RP-only mission-review / spool playback branch is intentionally //
|
|
// dropped for the first single-player bring-up. //
|
|
//===========================================================================//
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define _WIN32_WINNT 0x0500
|
|
#define WINVER 0x0500
|
|
|
|
// #define BT_HEAPCHECK // enable to validate the heap on every alloc (heap-corruption hunt)
|
|
#include <windows.h>
|
|
#include <shellapi.h> // CommandLineToArgvW
|
|
#include <btversion.h> // generated: 4.11.<commit count> (<hash>)
|
|
#include <crtdbg.h> // _CrtSetDbgFlag (heap validation, gated by BT_HEAPCHECK)
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime> // BT_EXPIRE tripwire (time)
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <bgfload.h> // LoadBgfFile (BGF probe) -- via game/fwd shim
|
|
#include <bt.hpp> // BT umbrella -- engine Entity/Application chain (matches btl4app.cpp)
|
|
#include "btl4app.hpp" // BTL4Application (pulls L4Application / Application chain)
|
|
#include "appmgr.hpp" // ApplicationManager
|
|
#include "resource.hpp" // StreamableResourceFile
|
|
#include "memreg.hpp" // Start_Registering / Register_Object / Stop_Registering
|
|
|
|
// Data version. The check (RESOURCE.cpp StreamableResourceFile ctor) compares our
|
|
// version[i] against the file's versionArray[i+1] (it skips the format byte), and the
|
|
// file is v1.1.0.6 → versionArray[1..3] = {1,0,6}. So our 3 bytes must be {1,0,6}.
|
|
Byte version[3] = { 1, 0, 6 };
|
|
|
|
extern const char* const ProgName;
|
|
const char* const ProgName = "btl4";
|
|
|
|
Application *btl4App = NULL;
|
|
HWND hWnd = NULL;
|
|
|
|
// Set by Application::StopMissionMessageHandler (APP.cpp): the round ended by
|
|
// operator/clock (NOT a window close) -- drives the between-rounds auto-rejoin.
|
|
int gBTMissionStoppedByConsole = 0;
|
|
|
|
// GLASS: resolvers for the per-display windows (declared in l4vb16.h), defined
|
|
// HERE off the launcher's own app + window pointers. They deliberately do NOT
|
|
// touch the `application`/`ghWnd` globals: those are duplicate-defined and bind
|
|
// non-deterministically per link under /FORCE (CMakeLists.txt:186), so a fresh
|
|
// engine TU (L4GLASSWIN) can read the NULL copy. btl4App/hWnd live in THIS obj
|
|
// and are the real, assigned pointers -- always correct.
|
|
class GaugeRenderer;
|
|
GaugeRenderer *BTResolveGaugeRenderer()
|
|
{
|
|
return btl4App ? btl4App->GetGaugeRenderer() : 0;
|
|
}
|
|
void *BTResolveMainWindow()
|
|
{
|
|
return (void *)hWnd;
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Bring-up player DRIVE input (Tier 2 locomotion).
|
|
// Populated by the keyboard handler in WndProc below (WASD / arrow keys)
|
|
// and consumed each frame by Mech::PerformAndWatch
|
|
// (decomp/reconstructed/mech4.cpp), which walks the player mech and advances
|
|
// its localToWorld -- the matrix the render tree AND the chase camera are
|
|
// bound to, so the mech moves and the camera follows.
|
|
//
|
|
// throttle: +1 full ahead .. -1 full reverse turn: +1 right .. -1 left
|
|
// Both are the OUTPUT of the virtual-control integrator in mech4.cpp
|
|
// (dt-scaled, fps-independent) -- the pod's real inputs were an ABSOLUTE
|
|
// analog throttle LEVER + turn stick, which momentary 0/1 keys can't
|
|
// express directly (and at 60fps every key tap = full demand = the "too
|
|
// sensitive controls" report). The WndProc only records KEY STATE
|
|
// (keyFwd/keyBack/keyLeft/keyRight); mech4 integrates:
|
|
// W/S = move the persistent throttle lever (tap ~= +/-0.07 fine step,
|
|
// hold sweeps full range in ~1.4s, DETENT at zero: braking stops
|
|
// AT 0 -- release and press S again to engage reverse)
|
|
// A/D = ramped momentary stick (deflects over ~0.4s, auto-centers)
|
|
// X = all stop (lever -> 0)
|
|
// forced : DEBUG toggle (env BT_FORCE_THROTTLE), default OFF. When set the
|
|
// mech walks full-ahead with NO keypress -- used only for headless
|
|
// autonomous verification (you can't press keys in a headless run).
|
|
//===========================================================================//
|
|
// fire: weapon trigger held (1 while the fire key is down).
|
|
// fireForced: DEBUG auto-fire (env BT_FORCE_FIRE) -- fires on cooldown with no
|
|
// keypress, for headless verification.
|
|
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
|
|
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
|
|
BTDriveInput gBTDrive = { 0.0f, 0.0f, 0, 0, 0, 1.0f, 0, 0, 0, 0, 0 };
|
|
|
|
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (uMsg)
|
|
{
|
|
case WM_KEYDOWN:
|
|
case WM_KEYUP:
|
|
// Drive keys are NOT read here: the engine's keyboard reader
|
|
// (L4CTRL.cpp:1506) GetMessage()s every WM_KEYUP / WM_CHAR out of the
|
|
// queue for its key-command channel, so this WndProc only ever saw the
|
|
// KEYDOWNs -- key state latched on and never released (the "controls
|
|
// incoherent" bug). The virtual-control integrator in mech4.cpp polls
|
|
// GetAsyncKeyState per frame instead (with a foreground guard), which
|
|
// is immune to message stealing.
|
|
return 0;
|
|
case WM_LBUTTONDOWN:
|
|
case WM_RBUTTONDOWN:
|
|
// COCKPIT SURROUND: route clicks on the composited panel buttons to the
|
|
// RIO (glass build) via the layout hit-test. No-op in other modes.
|
|
{
|
|
extern int gBTGaugeCockpit;
|
|
extern int BTCockpitMouseDown(int, int, int, int, int);
|
|
if (gBTGaugeCockpit)
|
|
{
|
|
RECT rc; GetClientRect(hWnd, &rc);
|
|
int consumed = BTCockpitMouseDown(
|
|
(int)(short)LOWORD(lParam), (int)(short)HIWORD(lParam),
|
|
(int)(rc.right - rc.left), (int)(rc.bottom - rc.top),
|
|
uMsg == WM_RBUTTONDOWN);
|
|
if (consumed && uMsg == WM_LBUTTONDOWN)
|
|
SetCapture(hWnd); // a release outside the button still clears it
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_LBUTTONUP:
|
|
{
|
|
extern int gBTGaugeCockpit;
|
|
extern void BTCockpitMouseUp(void);
|
|
if (gBTGaugeCockpit) { BTCockpitMouseUp(); ReleaseCapture(); }
|
|
}
|
|
return 0;
|
|
case WM_SIZE:
|
|
// Window-resize aspect fix (task #20): rebuild the projection for the
|
|
// new client aspect so the scene doesn't stretch fat/skinny (the D3D9
|
|
// backbuffer stays at the configured size and is stretched into the
|
|
// client area; rendering with the client aspect cancels the stretch).
|
|
// Under the cockpit LETTERBOX the canvas is scaled uniformly instead,
|
|
// so the aspect comes from the view rect -- BTWorldAspectOf handles it.
|
|
if (wParam != SIZE_MINIMIZED && LOWORD(lParam) > 0 && HIWORD(lParam) > 0)
|
|
{
|
|
extern void L4NotifyWindowResized(int client_w, int client_h);
|
|
L4NotifyWindowResized((int)LOWORD(lParam), (int)HIWORD(lParam));
|
|
// Repaint the letterbox bars: a COPY present only writes its dest
|
|
// rect, so whatever the old bars held would otherwise persist. The
|
|
// class brush is black, so an erase is all it takes.
|
|
extern int gBTCockpitLetterbox;
|
|
if (gBTCockpitLetterbox)
|
|
InvalidateRect(hWnd, NULL, TRUE);
|
|
}
|
|
return 0;
|
|
case WM_CLOSE:
|
|
DestroyWindow(hWnd);
|
|
return 0;
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
#ifdef BT_GLASS
|
|
#include "btl4fe.hpp"
|
|
#include "btl4console.hpp"
|
|
#endif
|
|
|
|
//
|
|
// CRASH SELF-REPORT (2026-07-24, the #35/#41 heisenbug hunt): every unhandled
|
|
// exception logs its code, address, and an EBP-chain stack walk as
|
|
// MODULE-RELATIVE offsets ("btl4+0x1234") into BT_LOG before the process
|
|
// dies -- so any field crash on any machine hands us a resolvable stack (we
|
|
// hold the PDB). The walker is guarded per-dereference; a corrupt frame
|
|
// chain just truncates the walk.
|
|
//
|
|
static LONG WINAPI
|
|
BTCrashFilter(EXCEPTION_POINTERS *info)
|
|
{
|
|
unsigned long base = (unsigned long)GetModuleHandleA(NULL);
|
|
unsigned long code = info->ExceptionRecord->ExceptionCode;
|
|
unsigned long addr = (unsigned long)info->ExceptionRecord->ExceptionAddress;
|
|
|
|
std::cout << "\n[crash] UNHANDLED EXCEPTION code=0x" << std::hex << code
|
|
<< " addr=0x" << addr;
|
|
if (addr >= base && addr < base + 0x2000000UL)
|
|
std::cout << " (btl4+0x" << (addr - base) << ")";
|
|
if (code == 0xC0000005 && info->ExceptionRecord->NumberParameters >= 2)
|
|
std::cout << " access=" << std::dec
|
|
<< (int)info->ExceptionRecord->ExceptionInformation[0]
|
|
<< std::hex << " target=0x"
|
|
<< (unsigned long)info->ExceptionRecord->ExceptionInformation[1];
|
|
std::cout << "\n[crash] stack:";
|
|
|
|
unsigned long eip = info->ContextRecord->Eip;
|
|
unsigned long ebp = info->ContextRecord->Ebp;
|
|
for (int depth = 0; depth < 24; ++depth)
|
|
{
|
|
std::cout << " ";
|
|
if (eip >= base && eip < base + 0x2000000UL)
|
|
std::cout << "btl4+0x" << std::hex << (eip - base);
|
|
else
|
|
std::cout << "0x" << std::hex << eip;
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
if (ebp == 0 || (ebp & 3) != 0
|
|
|| VirtualQuery((void *)ebp, &mbi, sizeof(mbi)) == 0
|
|
|| mbi.State != MEM_COMMIT
|
|
|| (mbi.Protect & (PAGE_READWRITE | PAGE_READONLY)) == 0)
|
|
break;
|
|
unsigned long next_eip = ((unsigned long *)ebp)[1];
|
|
unsigned long next_ebp = ((unsigned long *)ebp)[0];
|
|
if (next_eip == 0 || next_ebp <= ebp)
|
|
break;
|
|
eip = next_eip;
|
|
ebp = next_ebp;
|
|
}
|
|
std::cout << std::dec << "\n" << std::flush;
|
|
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
|
|
}
|
|
|
|
//===========================================================================//
|
|
// environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE
|
|
// per line; the real environment always WINS, so a launcher .bat or a shell
|
|
// export overrides the file rather than fighting it.
|
|
//
|
|
// ⚠ Loaded EARLY (2026-07-26). It used to be read ~300 lines into WinMain,
|
|
// after the platform-profile block had already run its getenv()s -- so every
|
|
// setting the profile reads (BT_PLATFORM, BT_COCKPIT, BT_GLASS_PANELS,
|
|
// BT_DEV_GAUGES, L4CONTROLS...) was silently ignored from the file and only
|
|
// worked as a real env var. It also putenv()'d comment lines verbatim, which
|
|
// turned a commented-out option into an environment variable literally named
|
|
// "#BT_MFD_SCALE" -- harmless, but it meant shipping a self-documenting file
|
|
// would have littered the environment with junk.
|
|
//===========================================================================//
|
|
//
|
|
// The shipped default, written on first run when the file is absent -- the
|
|
// bindings.txt convention ([[glass-cockpit]] §bindings.txt is a COMPATIBILITY
|
|
// SURFACE): the player customizes it, it stays UNTRACKED, and an
|
|
// extract-over-top upgrade therefore never clobbers their settings. Every
|
|
// option ships COMMENTED OUT, so a fresh install applies nothing and behaves
|
|
// exactly as it did before the file existed.
|
|
//
|
|
static const char *kEnvironIniDefault =
|
|
"# ============================================================================\n"
|
|
"# environ.ini -- BattleTech 4.11 settings\n"
|
|
"# ============================================================================\n"
|
|
"# One KEY=VALUE per line, read at game start. Lines starting with # or ;\n"
|
|
"# are comments. A real environment variable always WINS over this file, so\n"
|
|
"# a launcher .bat can override anything here. Delete a line to fall back to\n"
|
|
"# the built-in default.\n"
|
|
"#\n"
|
|
"# This file is for PERSISTENT settings only. One-shot switches (BT_JOYCONFIG\n"
|
|
"# -- use joyconfig.bat instead) are ignored here on purpose: they are designed\n"
|
|
"# to clear themselves after running once, and a file line would re-arm them at\n"
|
|
"# every mission start. Test/debug hooks (BT_MP_FORCE_DMG and friends) do not\n"
|
|
"# belong here either -- they would silently persist across every session.\n"
|
|
"#\n"
|
|
"# Input bindings live in bindings.txt beside this file (written with the\n"
|
|
"# full documented layout on first run; delete it to restore defaults).\n"
|
|
"\n"
|
|
"# ---- The cockpit ------------------------------------------------------------\n"
|
|
"\n"
|
|
"# Where the five MFDs and the map go:\n"
|
|
"# (unset) the COCKPIT SURROUND -- instruments composited around\n"
|
|
"# the viewscreen in one window (the default)\n"
|
|
"# BT_GLASS_PANELS=1 one desktop window PER display, arranged around the\n"
|
|
"# game window (the 'exploded' view -- handy for reading\n"
|
|
"# or screenshotting a display at full size)\n"
|
|
"# BT_COCKPIT=0 the plain docked gauge strip along the bottom\n"
|
|
"#BT_GLASS_PANELS=1\n"
|
|
"#BT_COCKPIT=0\n"
|
|
"\n"
|
|
"# Size of the secondary displays in the cockpit surround, as a percentage of\n"
|
|
"# their pod size. The pod bolted them down at one size; on a big panel there\n"
|
|
"# is room to trade viewscreen for instrument, so turn these up if you want to\n"
|
|
"# actually read the other displays while you fly. 100 = as the pod had them.\n"
|
|
"# Range 25-200 (out-of-range or unreadable values fall back to the group\n"
|
|
"# setting, then to 100). The surround grows to fit whatever you ask for.\n"
|
|
"#\n"
|
|
"# BT_MFD_SCALE sets all five MFDs at once...\n"
|
|
"#BT_MFD_SCALE=100\n"
|
|
"# ...and any single display can override it:\n"
|
|
"# UL upper left (heat) UC upper center (engineering)\n"
|
|
"# UR upper right (comm) LL lower left (left weapons)\n"
|
|
"# LR lower right (right weapons)\n"
|
|
"#BT_MFD_SCALE_UL=100\n"
|
|
"#BT_MFD_SCALE_UC=100\n"
|
|
"#BT_MFD_SCALE_UR=100\n"
|
|
"#BT_MFD_SCALE_LL=100\n"
|
|
"#BT_MFD_SCALE_LR=100\n"
|
|
"\n"
|
|
"# The portrait map/radar, sized on its own.\n"
|
|
"#BT_RADAR_SCALE=100\n"
|
|
"\n"
|
|
"# Where the map sits:\n"
|
|
"# CENTER bottom centre, under the viewscreen, as the pod had it\n"
|
|
"# (default; BOTTOM and CENTRE mean the same)\n"
|
|
"# LEFT bottom left corner (or BOTTOMLEFT)\n"
|
|
"# RIGHT bottom right corner (or BOTTOMRIGHT)\n"
|
|
"# MIDLEFT left edge, halfway up (or LEFTCENTER)\n"
|
|
"# MIDRIGHT right edge, halfway up (or RIGHTCENTER)\n"
|
|
"# Anywhere but CENTER stops it sitting where the road is. In a bottom corner\n"
|
|
"# the lower MFD whose corner it takes slides inboard beside it; halfway up a\n"
|
|
"# side it leaves the bottom row entirely.\n"
|
|
"#BT_RADAR_POS=CENTER\n"
|
|
"\n"
|
|
"# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n"
|
|
"#BT_COCKPIT_TINT=21FF42\n"
|
|
"\n"
|
|
"# ---- Display ----------------------------------------------------------------\n"
|
|
"\n"
|
|
"# The cockpit is fitted into the window at ONE uniform scale, centred, with\n"
|
|
"# the leftover black -- drag the window to any shape and nothing distorts.\n"
|
|
"# Launch with -fit (or -windowed-fullscreen) for a borderless window over the\n"
|
|
"# whole monitor.\n"
|
|
"\n"
|
|
"# Simulation/render frame rate, integer frames per second. The desktop\n"
|
|
"# default is 60; the arcade pods shipped at 25.\n"
|
|
"#TARGETFPS=60\n"
|
|
"\n"
|
|
"# Anti-aliasing sample count, passed straight to Direct3D 9: 0 = off, else\n"
|
|
"# 2..16 as the GPU supports. NOTE: turning this on disables the letterbox\n"
|
|
"# fit above (the two use incompatible swap modes) and the cockpit stretches\n"
|
|
"# to the window instead.\n"
|
|
"#MULTISAMPLE=0\n"
|
|
"\n"
|
|
"# Particle budget, integer. Default 8192.\n"
|
|
"#MAXPARTICLES=8192\n"
|
|
"\n"
|
|
"# ---- Optional ---------------------------------------------------------------\n"
|
|
"\n"
|
|
"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to a lamp\n"
|
|
"# button in bindings.txt glow with the panel -- yellow for the map's side\n"
|
|
"# columns, red for the rest -- and flash in step with the on-screen buttons.\n"
|
|
"# Per-key keyboards light each bound key; zone-lit boards mirror the\n"
|
|
"# strongest lamp board-wide. Unset or nonzero = on; 0 = off. A machine\n"
|
|
"# without Dynamic Lighting logs once and stays dormant.\n"
|
|
"#BT_KEYLIGHT=0\n"
|
|
"\n"
|
|
"# ---- Multiplayer ------------------------------------------------------------\n"
|
|
"\n"
|
|
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
|
|
"# running; without it the game logs the reason and falls back to Winsock.\n"
|
|
"#BT_STEAM_NET=1\n";
|
|
|
|
//===========================================================================//
|
|
// CWD GUARD (2026-07-26, field report).
|
|
//
|
|
// The engine resolves BTL4.RES, VIDEO\, BTDPL.INI, the eggs AND both player
|
|
// config files (bindings.txt, environ.ini) RELATIVE TO CWD -- every launcher
|
|
// .bat does `cd content` first, so this was invisible. A BARE launch of the
|
|
// exe (double-click, or a shell sitting in build\Release) then:
|
|
// - found no resources ("Resource file btl4.res v1.0.0.0 is obsolete!"),
|
|
// - wrote a stray bindings.txt / environ.ini / btl4.log NEXT TO THE EXE
|
|
// (so the player's real ones in content\ looked like they never appeared),
|
|
// - and killed the mission generation the menu launched.
|
|
// Reported live: `.\btl4.exe -fit` from build\Release -> menu opens -> crash
|
|
// on Launch.
|
|
//
|
|
// So find content\ ourselves rather than trusting whoever started us. Runs
|
|
// BEFORE the log file opens, so the log lands in content\ too -- same place
|
|
// the launchers put it. Returns what happened for the boot line.
|
|
//===========================================================================//
|
|
static const char *gBTCwdFixNote = NULL;
|
|
|
|
//
|
|
// -fit / -windowed-fullscreen, parsed EARLY: the miniconsole front end exits
|
|
// this process long before the window-creation code runs, and it rebuilds the
|
|
// mission child's command line from scratch -- so without this the flag was
|
|
// silently dropped the moment you launched from the menu (field report:
|
|
// `btl4.exe -fit` gave a borderless menu and then a windowed mission).
|
|
// btl4console.cpp reads it when composing the relaunch.
|
|
//
|
|
int gBTFitDisplay = 0;
|
|
|
|
static void BTEnsureContentDirectory(void)
|
|
{
|
|
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
|
|
{
|
|
return; // already in the right place
|
|
}
|
|
|
|
char exeDir[MAX_PATH];
|
|
if (GetModuleFileNameA(NULL, exeDir, MAX_PATH) == 0)
|
|
{
|
|
return;
|
|
}
|
|
char *slash = strrchr(exeDir, '\\');
|
|
if (slash == NULL)
|
|
{
|
|
return;
|
|
}
|
|
*slash = 0;
|
|
|
|
// build\Release\btl4.exe -> ..\..\content is the shipped layout; the
|
|
// others cover a flattened install and an exe dropped into content\.
|
|
static const char *kProbe[] = {
|
|
"\\..\\..\\content", "\\..\\content", "\\content",
|
|
"", "\\..", "\\..\\.."
|
|
};
|
|
for (int i = 0; i < (int)(sizeof(kProbe) / sizeof(kProbe[0])); ++i)
|
|
{
|
|
char candidate[MAX_PATH * 2];
|
|
char resource[MAX_PATH * 2];
|
|
sprintf_s(candidate, sizeof(candidate), "%s%s", exeDir, kProbe[i]);
|
|
sprintf_s(resource, sizeof(resource), "%s\\BTL4.RES", candidate);
|
|
if (GetFileAttributesA(resource) != INVALID_FILE_ATTRIBUTES)
|
|
{
|
|
if (SetCurrentDirectoryA(candidate))
|
|
{
|
|
gBTCwdFixNote = "found the content directory from the exe path";
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
gBTCwdFixNote = "NO content directory found (BTL4.RES is nowhere near the "
|
|
"exe) -- resources and settings will not load";
|
|
}
|
|
|
|
static void BTWriteDefaultEnvironIni(void)
|
|
{
|
|
FILE *probe;
|
|
if (fopen_s(&probe, "environ.ini", "r") == 0) // already there -- leave it alone
|
|
{
|
|
fclose(probe);
|
|
return;
|
|
}
|
|
// Only ever write it BESIDE THE RESOURCES. If the cwd guard could not
|
|
// find content\, writing here would scatter a stray settings file next to
|
|
// whatever the player happened to launch from -- which is exactly the
|
|
// confusion the guard exists to end.
|
|
if (GetFileAttributesA("BTL4.RES") == INVALID_FILE_ATTRIBUTES)
|
|
{
|
|
return;
|
|
}
|
|
FILE *out;
|
|
if (fopen_s(&out, "environ.ini", "w") != 0)
|
|
return;
|
|
fputs(kEnvironIniDefault, out);
|
|
fclose(out);
|
|
std::cout << "[boot] wrote a default content\\environ.ini (all options "
|
|
"commented out -- edit to taste)" << std::endl << std::flush;
|
|
}
|
|
|
|
static void BTLoadEnvironIni(void)
|
|
{
|
|
BTWriteDefaultEnvironIni();
|
|
|
|
FILE *file;
|
|
if (fopen_s(&file, "environ.ini", "r") != 0)
|
|
return;
|
|
|
|
char line[1024];
|
|
int applied = 0, skipped = 0;
|
|
while (fgets(line, sizeof(line), file))
|
|
{
|
|
// strip EOL + trailing blanks
|
|
int n = (int)strlen(line);
|
|
while (n > 0 && (line[n-1] == '\n' || line[n-1] == '\r' ||
|
|
line[n-1] == ' ' || line[n-1] == '\t'))
|
|
line[--n] = 0;
|
|
|
|
const char *p = line;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (*p == 0 || *p == '#' || *p == ';') // blank or comment
|
|
continue;
|
|
|
|
const char *eq = strchr(p, '=');
|
|
if (eq == NULL || eq == p) // no key, or no '=' at all
|
|
{
|
|
++skipped;
|
|
continue;
|
|
}
|
|
|
|
// The real environment wins: only set what is not already set, so a
|
|
// launcher or a shell export beats the file.
|
|
char key[256];
|
|
size_t klen = (size_t)(eq - p);
|
|
if (klen >= sizeof(key)) { ++skipped; continue; }
|
|
memcpy(key, p, klen);
|
|
key[klen] = 0;
|
|
while (klen > 0 && (key[klen-1] == ' ' || key[klen-1] == '\t'))
|
|
key[--klen] = 0;
|
|
if (getenv(key) != NULL)
|
|
continue;
|
|
|
|
// ONE-SHOT variables never load from the file (merge review
|
|
// 2026-07-26). BT_JOYCONFIG is deliberately DELETED from the process
|
|
// environment after the wizard runs (the #66 one-shot) so the
|
|
// front-end relaunch chain cannot re-run it -- but a file line would
|
|
// re-arm it in every relaunched child, since by then there is no real
|
|
// env var left to win over the file. Result: the capture wizard at
|
|
// every mission start. environ.ini is for persistent settings;
|
|
// anything designed to clear itself cannot live in it.
|
|
if (_stricmp(key, "BT_JOYCONFIG") == 0)
|
|
{
|
|
++skipped;
|
|
continue;
|
|
}
|
|
|
|
putenv(p);
|
|
++applied;
|
|
}
|
|
fclose(file);
|
|
|
|
std::cout << "[boot] environ.ini: " << applied << " setting(s) applied"
|
|
<< (skipped ? ", " : "") << (skipped ? std::to_string(skipped) : std::string())
|
|
<< (skipped ? " line(s) ignored" : "") << std::endl << std::flush;
|
|
}
|
|
|
|
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
|
{
|
|
// Boot tick for the relaunch storm damper (btl4console.cpp, issue #33):
|
|
// generations that die young relaunch with a 5 s pause.
|
|
{
|
|
extern unsigned long gBTBootTick;
|
|
gBTBootTick = GetTickCount();
|
|
}
|
|
|
|
// Crash self-report (see BTCrashFilter above) -- armed before anything
|
|
// else so even boot crashes leave a stack in the log.
|
|
SetUnhandledExceptionFilter(BTCrashFilter);
|
|
|
|
// Sit in content\ before ANYTHING resolves a relative path -- the log file
|
|
// below included, so a bare launch logs where the launchers log.
|
|
BTEnsureContentDirectory();
|
|
|
|
// BT_CRASHTEST=1: deliberately AV after the log opens -- verifies the
|
|
// crash filter's forensic path end-to-end on any machine.
|
|
|
|
// Heap-corruption hunt (env BT_HEAPCHECK=1; default OFF -- ~100x slower): validate
|
|
// the whole debug heap on EVERY alloc/free, so an out-of-bounds write is caught at
|
|
// the very next heap op (a stack near the culprit) instead of much later at some
|
|
// unrelated free. Route the CRT report to the debugger so cdb breaks at detection.
|
|
if (getenv("BT_HEAPCHECK"))
|
|
{
|
|
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF);
|
|
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
|
|
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
|
|
}
|
|
|
|
// Bring-up: route CRT asserts (e.g. fread stream!=nullptr) to the debugger
|
|
// instead of a modal MessageBox, so cdb breaks at the assert and we can dump
|
|
// the faulting stack in a headless run. Gated; default OFF (no behavior change).
|
|
if (getenv("BT_ASSERT_TO_DEBUGGER"))
|
|
{
|
|
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
|
|
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
|
|
}
|
|
|
|
// Redirect the engine's std::cout/DEBUG_STREAM to a log file. BT_LOG names the
|
|
// file (default btl4.log) -- REQUIRED for multi-instance runs from one cwd
|
|
// (instance 2 would truncate instance 1's log).
|
|
std::ofstream logfile;
|
|
// BT_LOG_APPEND=1: keep prior generations' lines (the lobby-loop
|
|
// relaunch truncates the shared BT_LOG otherwise -- multi-generation
|
|
// forensics need the whole trail).
|
|
logfile.open(getenv("BT_LOG") ? getenv("BT_LOG") : "btl4.log",
|
|
(getenv("BT_LOG_APPEND") != NULL && *getenv("BT_LOG_APPEND") == '1')
|
|
? (std::ios::out | std::ios::app) : std::ios::out);
|
|
std::cout.rdbuf(logfile.rdbuf());
|
|
|
|
if (getenv("BT_CRASHTEST") != NULL && *getenv("BT_CRASHTEST") == '1')
|
|
{
|
|
*(volatile int *)0 = 0; // BT_CRASHTEST: exercise BTCrashFilter
|
|
}
|
|
|
|
// FIRST-BREATH line, flushed IMMEDIATELY (field 2026-07-23: a player's
|
|
// exe died repeatedly with a 0-byte join.log -- indistinguishable from
|
|
// "never ran"). After this line, an empty log can only mean the exe was
|
|
// killed before WinMain (antivirus / loader) -- an external cause, not
|
|
// ours. Cheap forensics: version, PID, args, generation markers.
|
|
std::cout << "[boot] btl4 " << BT_VERSION_STRING << " pid="
|
|
<< (unsigned)GetCurrentProcessId()
|
|
<< " args='" << (lpCmdLine ? lpCmdLine : "") << "'"
|
|
<< (getenv("BT_FE_LOOP") ? " (relaunched generation)" : " (first process)")
|
|
<< std::endl << std::flush;
|
|
|
|
// Say so when we had to go looking for content\ -- a player who launched
|
|
// the exe directly should see WHY their settings appeared where they did.
|
|
if (gBTCwdFixNote != NULL)
|
|
{
|
|
char here[MAX_PATH] = { 0 };
|
|
GetCurrentDirectoryA(MAX_PATH, here);
|
|
std::cout << "[boot] cwd: " << gBTCwdFixNote << " -> " << here
|
|
<< std::endl << std::flush;
|
|
}
|
|
|
|
// The player's settings, before ANYTHING reads the environment (the
|
|
// platform profile, the joystick wizard, the layout resolver...).
|
|
BTLoadEnvironIni();
|
|
|
|
if (lpCmdLine && (strstr(lpCmdLine, "-fit") != 0 ||
|
|
strstr(lpCmdLine, "-windowed-fullscreen") != 0))
|
|
{
|
|
gBTFitDisplay = 1;
|
|
}
|
|
|
|
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
|
|
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
|
|
// the player moves for each pod control and write the joystick section
|
|
// of content\bindings.txt, then the boot continues into the game with
|
|
// the new bindings live (PadRIO loads the file after this point).
|
|
if (getenv("BT_JOYCONFIG") != NULL && *getenv("BT_JOYCONFIG") != '0')
|
|
{
|
|
extern int BTJoyConfigWizard(void);
|
|
BTJoyConfigWizard();
|
|
//
|
|
// ONE-SHOT (input-path audit row 10): joyconfig.bat sets BT_JOYCONFIG=1
|
|
// *and* BT_FE_SOLO=1, so the wizard's process hands off to the front end,
|
|
// which CreateProcess()es the next generation with the CURRENT environment
|
|
// (btl4console.cpp clears only BT_FE_EGG/PODS/SECS/LOOP). The child then
|
|
// re-entered the wizard and blocked on console input BEHIND the 3D window
|
|
// -- an unrecoverable hang for anyone who configures a joystick. Clear it
|
|
// in the Win32 environment (what CreateProcess inherits, matching the
|
|
// console's own SetEnvironmentVariableA pattern) the moment the wizard is
|
|
// done, so exactly one generation ever runs it.
|
|
//
|
|
SetEnvironmentVariableA("BT_JOYCONFIG", NULL);
|
|
}
|
|
|
|
// MP wire-format probe (env BT_NET_PROBE=1): print the exact packet constants
|
|
// the console emulator (tools/btconsole.py) must speak, computed from OUR build
|
|
// (message IDs chain across class enums at compile time -- never hand-compute).
|
|
if (getenv("BT_NET_PROBE"))
|
|
{
|
|
std::cout << "[netprobe] ReceiveEggFileMessageID=" << (int)NetworkManager::ReceiveEggFileMessageID
|
|
<< " sizeof(NetworkPacketHeader)=" << sizeof(NetworkPacketHeader)
|
|
<< " sizeof(Receiver__Message)=" << sizeof(Receiver__Message)
|
|
<< " sizeof(ReceiveEggFileMessage)=" << sizeof(NetworkManager::ReceiveEggFileMessage)
|
|
<< " sizeof(Time)=" << sizeof(Time)
|
|
<< " NetworkManagerClientID=" << (int)NetworkClient::NetworkManagerClientID
|
|
<< " RunMissionMessageID=" << (int)Application::RunMissionMessageID
|
|
<< " sizeof(RunMissionMessage)=" << sizeof(Application::RunMissionMessage)
|
|
<< " ApplicationClientID=" << (int)NetworkClient::ApplicationClientID
|
|
<< "\n" << std::flush;
|
|
return 0;
|
|
}
|
|
|
|
// BGF loader probe (env BT_PROBE_BGF=<name>[,<name>...] or =ALL): load the named
|
|
// model(s) directly through the engine's LoadBgfFile -- NO app, NO renderer, NO
|
|
// mechs -- then exit. Isolates the loader: if this crashes/corrupts, the bug is
|
|
// in bgfload.cpp itself (H1); if hundreds of iterations are clean, the in-game
|
|
// corruption comes from elsewhere and the loader's frees only DETECT it (H2).
|
|
// Repeats each load BT_PROBE_N times (default 8). cwd must be the pod BT dir
|
|
// (the loader indexes VIDEO\ relative to cwd). Combine with BT_HEAPCHECK=1.
|
|
if (const char *probe = getenv("BT_PROBE_BGF"))
|
|
{
|
|
const int reps = getenv("BT_PROBE_N") ? atoi(getenv("BT_PROBE_N")) : 8;
|
|
std::vector<std::string> names;
|
|
if (_stricmp(probe, "ALL") == 0)
|
|
{
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA("VIDEO\\GEO\\*.BGF", &fd);
|
|
if (h != INVALID_HANDLE_VALUE)
|
|
{
|
|
do { names.push_back(fd.cFileName); } while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::string s(probe);
|
|
size_t p = 0;
|
|
while (p < s.size())
|
|
{
|
|
size_t c = s.find(',', p);
|
|
if (c == std::string::npos) c = s.size();
|
|
if (c > p) names.push_back(s.substr(p, c - p));
|
|
p = c + 1;
|
|
}
|
|
}
|
|
std::cout << "[probe] BGF probe: " << names.size() << " model(s) x " << reps
|
|
<< " reps, heapcheck=" << (getenv("BT_HEAPCHECK") ? 1 : 0) << "\n" << std::flush;
|
|
int fails = 0;
|
|
for (size_t i = 0; i < names.size(); ++i)
|
|
{
|
|
for (int r = 0; r < reps; ++r)
|
|
{
|
|
BgfData data;
|
|
bool ok = LoadBgfFile(names[i], data);
|
|
if (r == 0 || !ok)
|
|
std::cout << "[probe] " << names[i] << " rep " << r
|
|
<< (ok ? " OK" : " FAIL") << " verts=" << data.verts.size()
|
|
<< " idx=" << data.indices.size() << " tris=" << data.tris
|
|
<< (ok ? "" : (" err=" + data.error)) << "\n" << std::flush;
|
|
if (!ok) ++fails;
|
|
if (!_CrtCheckMemory())
|
|
{
|
|
std::cout << "[probe] *** HEAP CORRUPT after " << names[i]
|
|
<< " rep " << r << " ***\n" << std::flush;
|
|
return 3;
|
|
}
|
|
}
|
|
}
|
|
std::cout << "[probe] DONE: heap clean, fails=" << fails << "\n" << std::flush;
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PLATFORM PROFILE -- dev (default) vs pod. This is NOT a code fork: the
|
|
// profile just selects WHICH environment preset the existing video/gauge/input
|
|
// code reads (the pod multi-surface path already exists -- FindBestAdapterIndices
|
|
// / SVGA16::BuildWindows / L4GaugeRenderer -- gated on these env vars).
|
|
// dev = single 800x600 window + keyboard (no L4GAUGE -> MakeGaugeRenderer
|
|
// returns NULL, btl4app.cpp:353). The working dev build.
|
|
// pod = multi-surface gauges/MFDs (L4GAUGE set) + RIO cockpit I/O, mirroring
|
|
// content/SETENV.BAT (the authentic pod preset). The `if getenv==NULL`
|
|
// guard lets a real pod environment (SETENV.BAT) override every value.
|
|
// Select with env BT_PLATFORM=pod|dev (robust; primary) or a `-platform pod`
|
|
// launcher arg. ⚠ A FULL pod run needs the pod's 2 adapters / 7 monitors + RIO
|
|
// & plasma serial + GAUGE\L4GAUGE.INI (Phase-8, on real hardware). On a dev box
|
|
// `-platform pod` ATTEMPTS the path (FindBestAdapterIndices degrades to the
|
|
// primary adapter) -- selectable for bring-up, not fully validated off-pod.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// MENU-MODE detection FIRST (glass): the front-end process must NOT
|
|
// apply a platform profile -- its putenv()s would leak into the
|
|
// relaunched mission child and defeat the child's own preset (found
|
|
// live: a direct btl4.exe start ran the menu under the DEV profile,
|
|
// the child inherited L4CONTROLS=KEYBOARD, and the glass preset could
|
|
// never select PAD -- no PadRIO, no panel, plasma only).
|
|
//
|
|
int gBTPlatformPod = 0;
|
|
int gBTPlatformGlass = 0;
|
|
{
|
|
const char *pe = getenv("BT_PLATFORM");
|
|
int platform_dev = 0;
|
|
if (pe && _stricmp(pe, "pod") == 0) gBTPlatformPod = 1;
|
|
if (pe && _stricmp(pe, "glass") == 0) gBTPlatformGlass = 1;
|
|
if (pe && _stricmp(pe, "dev") == 0) platform_dev = 1;
|
|
// Convenience: also accept a raw `-platform pod` on the command line,
|
|
// parsed off lpCmdLine independently of L4Application::ParseCommandLine.
|
|
const char *pf = lpCmdLine ? strstr(lpCmdLine, "-platform") : 0;
|
|
if (pf)
|
|
{
|
|
char pv[16] = { 0 };
|
|
if (sscanf(pf + 9, " %15s", pv) == 1 && _stricmp(pv, "pod") == 0)
|
|
gBTPlatformPod = 1;
|
|
if (pv[0] && _stricmp(pv, "glass") == 0)
|
|
gBTPlatformGlass = 1;
|
|
if (pv[0] && _stricmp(pv, "dev") == 0)
|
|
platform_dev = 1;
|
|
}
|
|
//
|
|
// DESKTOP DEFAULT = GLASS (2026-07-22, "competing keymaps" fix): every
|
|
// launcher that forgot BT_PLATFORM=glass booted the DEV profile -- no
|
|
// PadRIO, the bring-up keyboard bridge owning the keys ("the old
|
|
// keymap again": play bats, steam bat, then the operator console,
|
|
// each needed the same one-line fix). Glass is now the DEFAULT when
|
|
// no platform is chosen, so omission can never resurrect the old
|
|
// input stack. BT_PLATFORM=dev opts back into the legacy DEV profile
|
|
// for debugging; the FUTURE pod cabinet must set BT_PLATFORM=pod in
|
|
// its SETENV.BAT (contract recorded in context/glass-cockpit.md --
|
|
// note this changes the old "bare boot == cabinet shape" assumption).
|
|
//
|
|
if (!gBTPlatformPod && !gBTPlatformGlass && !platform_dev)
|
|
gBTPlatformGlass = 1;
|
|
}
|
|
|
|
// MENU GATE (2026-07-21, amended 2026-07-22): the miniconsole menu runs
|
|
// on a zero-arg GLASS boot. With glass now the desktop DEFAULT, a bare
|
|
// double-click of btl4.exe opens the menu -- the desired desktop UX.
|
|
// The pod cabinet opts out via BT_PLATFORM=pod (SETENV.BAT contract);
|
|
// BT_PLATFORM=dev keeps the old bare-boot wait-for-egg shape.
|
|
int fe_menu_mode = 0;
|
|
{
|
|
int fe_has_egg = lpCmdLine && strstr(lpCmdLine, "-egg") != NULL;
|
|
int fe_has_net = lpCmdLine && strstr(lpCmdLine, "-net") != NULL;
|
|
fe_menu_mode = !fe_has_egg && !fe_has_net && getenv("BT_FE_EGG") == NULL
|
|
&& (gBTPlatformGlass || getenv("BT_FE_MENU") != NULL);
|
|
}
|
|
|
|
if (fe_menu_mode)
|
|
{
|
|
// the front end owns this process; no profile putenvs (see above)
|
|
}
|
|
else if (gBTPlatformPod)
|
|
{
|
|
// POD profile. RIO cockpit input, which falls back to KEYBOARD when the
|
|
// serial port isn't present (e.g. a dev box: "RIO initialization failed!
|
|
// Shutting Down Serial Port!" -> keyboard).
|
|
if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=RIO,KEYBOARD");
|
|
|
|
// The multi-surface gauges/MFDs are a POD-HARDWARE capability: each surface
|
|
// needs its OWN fullscreen D3D device on one of the pod's 2 video cards
|
|
// (separate adapters), which can't be created on a typical dev box -- the
|
|
// L4GaugeRenderer/SVGA16 path bails there. So `-platform pod` does NOT
|
|
// auto-enable them; it stays bootable everywhere as single-window + the pod
|
|
// INPUT profile. On the REAL pod the gauges come from content/SETENV.BAT
|
|
// (which sets L4GAUGE + the gauge/adapter env); the existing, untouched
|
|
// FindBestAdapterIndices / SVGA16 / L4GaugeRenderer path then runs.
|
|
// To force-test the surfaces off-pod, set L4GAUGE explicitly (needs >=2 real
|
|
// display ADAPTERS, not just monitors). FOLLOW-UP for a dev-visible pod HUD:
|
|
// composite the MFDs/gauges into the single window ("MFD compositing on dev").
|
|
if (getenv("L4GAUGE") != NULL && getenv("L4PLASMA") == NULL)
|
|
putenv("L4PLASMA=com2"); // only when the pod env already asked for gauges
|
|
}
|
|
#ifdef BT_GLASS
|
|
else if (gBTPlatformGlass)
|
|
{
|
|
// GLASS profile (dev tooling, step 2e): the desktop cockpit --
|
|
// PadRIO input (XInput+keyboard, content\bindings.txt), the
|
|
// dev-composited MFD surfaces, and the desktop plasma window.
|
|
// Each only when not already set, so a developer env overrides.
|
|
if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=PAD,KEYBOARD");
|
|
if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1");
|
|
if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN");
|
|
|
|
// The SECONDARY-DISPLAY LAYOUT (surround / per-display windows / docked
|
|
// strip / separate window) is resolved once, below, after the shared
|
|
// defaults land -- it used to be half-decided here, which is how
|
|
// BT_COCKPIT=0 came to mean "per-display windows" despite being
|
|
// documented as the dock-bottom opt-out (the dock was unreachable under
|
|
// glass). This block now only sets the profile's env presets.
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
// DEV profile (default): keyboard controls, single 800x600 window.
|
|
if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=KEYBOARD");
|
|
}
|
|
|
|
// Shared renderer/config defaults (both profiles). L4DPLCFG=BTDPL.INI gives
|
|
// DPLReadEnvironment the real BT environment (paths/fog/lights/ambient) instead
|
|
// of the dpldflt.ini fallback.
|
|
if (getenv("L4DPLCFG") == NULL) putenv("L4DPLCFG=BTDPL.INI");
|
|
if (getenv("DPLARG") == NULL) putenv("DPLARG=1");
|
|
if (getenv("MULTISAMPLE") == NULL) putenv("MULTISAMPLE=0");
|
|
if (getenv("TARGETFPS") == NULL) putenv("TARGETFPS=60");
|
|
if (getenv("MAXPARTICLES")== NULL) putenv("MAXPARTICLES=8192");
|
|
|
|
// DEV-COMPOSITE GAUGES (opt-in, default OFF): BT_DEV_GAUGES wakes the (otherwise
|
|
// dormant) gauge renderer so the pod's gauges/MFDs render + can be composited into
|
|
// the dev window. Setting L4GAUGE makes MakeGaugeRenderer build the renderer; on a
|
|
// dev box the pod multi-surface (SVGA16 per-adapter fullscreen devices) stays at 0
|
|
// surfaces (guarded in SVGA16::Update), and a composite pass in the main renderer
|
|
// blits the CPU-rastered pixelBuffer as a window inset. See docs/GAUGE_COMPOSITE.md.
|
|
if (getenv("BT_DEV_GAUGES") != NULL && getenv("L4GAUGE") == NULL)
|
|
putenv("L4GAUGE=640x480x16");
|
|
|
|
// -------------------------------------------------------------------------
|
|
// SECONDARY-DISPLAY LAYOUT -- resolved ONCE, here, now that every profile
|
|
// putenv has landed. It used to be decided TWICE (the glass profile block
|
|
// picked BT_PAD_PANEL/BT_GLASS_PANELS; the window-sizing block further down
|
|
// re-derived cockpit-vs-dock from the same env with its own copy of the
|
|
// precedence) and the boot banner read NEITHER -- it announced "per-display
|
|
// cockpit windows" for every glass boot, including the surround default.
|
|
// The split also broke BT_COCKPIT=0: documented as the dock-bottom opt-out,
|
|
// it actually landed on the per-display windows, leaving the docked strip
|
|
// unreachable under the glass profile.
|
|
//
|
|
// One resolver, one answer, consumed by the banner, the pad-panel decision
|
|
// and the sizing block. Precedence:
|
|
// BT_GLASS_PANELS!=0 per-display cockpit windows (explicit only)
|
|
// BT_DEV_GAUGES_WINDOW the legacy separate MFD window
|
|
// BT_DEV_GAUGES_DOCK the docked bottom strip
|
|
// BT_COCKPIT=0 ...also the docked strip (the documented opt-out)
|
|
// (nothing) COCKPIT SURROUND -- the glass default
|
|
// -------------------------------------------------------------------------
|
|
enum BTGlassLayout { GlassLayoutNone, GlassLayoutCockpit, GlassLayoutPanels,
|
|
GlassLayoutDock, GlassLayoutWindow };
|
|
int glassLayout = GlassLayoutNone;
|
|
if (!fe_menu_mode && getenv("BT_DEV_GAUGES") != NULL)
|
|
{
|
|
int panels = 0;
|
|
#ifdef BT_GLASS
|
|
{ extern int BTGlassPanelsActive(); panels = BTGlassPanelsActive(); }
|
|
#endif
|
|
const char *ck = getenv("BT_COCKPIT");
|
|
if (panels) glassLayout = GlassLayoutPanels;
|
|
else if (getenv("BT_DEV_GAUGES_WINDOW")) glassLayout = GlassLayoutWindow;
|
|
else if (getenv("BT_DEV_GAUGES_DOCK")) glassLayout = GlassLayoutDock;
|
|
else if (ck != NULL && ck[0] == '0') glassLayout = GlassLayoutDock;
|
|
else glassLayout = GlassLayoutCockpit;
|
|
|
|
// The surround and the per-display windows carry their own buttons; the
|
|
// dock strip and the separate MFD window don't, so the single combined
|
|
// pad panel supplies them (else the whole 72-button field is unclickable).
|
|
if ((glassLayout == GlassLayoutDock || glassLayout == GlassLayoutWindow)
|
|
&& getenv("BT_PAD_PANEL") == NULL)
|
|
putenv("BT_PAD_PANEL=1");
|
|
}
|
|
static const char *kGlassLayoutName[] = {
|
|
"no dev gauges",
|
|
"cockpit surround",
|
|
"per-display cockpit windows [BT_GLASS_PANELS]",
|
|
"docked gauge strip",
|
|
"separate MFD window"
|
|
};
|
|
|
|
std::cout << "[boot] platform profile: "
|
|
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
|
|
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
|
|
: gBTPlatformGlass ? "GLASS (PadRIO + plasma window)"
|
|
: "DEV (single window + keyboard)");
|
|
if (!fe_menu_mode)
|
|
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
|
|
std::cout << std::endl << std::flush;
|
|
|
|
#ifdef BT_GLASS
|
|
//
|
|
// MINICONSOLE FRONT END (glass step 3): a zero-mission-arg launch (no
|
|
// -egg, no -net) runs the menu INSTEAD of the game, then RELAUNCHES
|
|
// this exe with the chosen mission's real arguments -- the per-mission
|
|
// process-relaunch pattern (reconstructed gBT* globals do not survive
|
|
// in-process re-init). BT_FE_* env vars arm the mission process's
|
|
// LocalConsole marshal; BT_FE_LOOP returns post-mission exits to the
|
|
// menu. The console-wire ladder itself is 100% stock -- the engine
|
|
// sees a real connected console (btl4console.cpp).
|
|
//
|
|
{
|
|
if (fe_menu_mode)
|
|
{
|
|
BTFeLaunchSpec fe_spec;
|
|
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
|
|
{
|
|
return 0; // quit from the menu
|
|
}
|
|
char fe_arguments[192];
|
|
char fe_value[64];
|
|
SetEnvironmentVariableA("BT_FE_LOOP", "1");
|
|
SetEnvironmentVariableA("BT_PLATFORM", "glass");
|
|
switch (fe_spec.mode)
|
|
{
|
|
case BTFeLaunchRawSolo:
|
|
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
|
sprintf(fe_arguments, "-egg %s -platform glass", fe_spec.eggPath);
|
|
break;
|
|
case BTFeLaunchJoinLan:
|
|
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
|
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
|
break;
|
|
case BTFeLaunchJoinRelay:
|
|
// BT_FE_JOIN (join.bat): the callsign/mech request rides env
|
|
// into the relaunched pod; L4NET's SEAT_REQUEST carries it to
|
|
// the relay, which writes it into the session egg. Args = the
|
|
// universal join contract (OPERATOR.EGG + port 1501); the
|
|
// operator console overrides the port per LOCAL instance via
|
|
// BT_FE_JOINPORT (two instances on one box can't share -net).
|
|
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
|
SetEnvironmentVariableA("BT_CALLSIGN", fe_spec.joinCallsign);
|
|
SetEnvironmentVariableA("BT_MECH", fe_spec.joinVehicle);
|
|
{
|
|
const char *join_port = getenv("BT_FE_JOINPORT");
|
|
sprintf(fe_arguments, "-egg OPERATOR.EGG -net %d -platform glass",
|
|
join_port != NULL ? atoi(join_port) : 1501);
|
|
}
|
|
break;
|
|
#ifdef BT_STEAM
|
|
case BTFeLaunchJoinSteam:
|
|
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
|
SetEnvironmentVariableA("BT_STEAM_NET", "1");
|
|
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
|
|
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
|
|
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
|
break;
|
|
case BTFeLaunchHostSteam:
|
|
SetEnvironmentVariableA("BT_STEAM_NET", "1");
|
|
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
|
|
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
|
|
// falls into the marshal-armed default
|
|
#endif
|
|
default: // Solo / HostLan: the marshal armed via env handoff
|
|
SetEnvironmentVariableA("BT_FE_EGG", fe_spec.eggPath);
|
|
SetEnvironmentVariableA("BT_FE_PODS", fe_spec.podList);
|
|
sprintf(fe_value, "%d", fe_spec.missionSeconds);
|
|
SetEnvironmentVariableA("BT_FE_SECS", fe_value);
|
|
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
|
break;
|
|
}
|
|
{
|
|
// The user just clicked JOIN/LAUNCH in the menu -- never
|
|
// storm-damp a human (btl4console.cpp, issue #33).
|
|
extern int gBTRelaunchUserInitiated;
|
|
gBTRelaunchUserInitiated = 1;
|
|
}
|
|
BTFE_RelaunchSelfAndExit(fe_arguments); // never returns
|
|
}
|
|
#ifdef BT_STEAM
|
|
//
|
|
// The Steam transport (step 4b): BT_STEAM_NET=1 (set by the lobby
|
|
// launch path, or by hand for testing) brings up FakeIP + SDR
|
|
// before the engine opens any connection. Degrades to Winsock.
|
|
//
|
|
if (getenv("BT_STEAM_NET") != NULL && *getenv("BT_STEAM_NET") != '0')
|
|
{
|
|
extern int BTSteamNet_Install();
|
|
BTSteamNet_Install();
|
|
}
|
|
#endif
|
|
//
|
|
// A marshal-armed mission process: start the LocalConsole worker
|
|
// (it retries until our own console listener is up).
|
|
//
|
|
const char *fe_egg = getenv("BT_FE_EGG");
|
|
if (fe_egg != NULL && fe_egg[0])
|
|
{
|
|
const char *fe_pods = getenv("BT_FE_PODS");
|
|
const char *fe_secs = getenv("BT_FE_SECS");
|
|
BTLocalConsole_Start(fe_egg,
|
|
fe_pods ? fe_pods : "",
|
|
fe_secs ? atoi(fe_secs) : 600);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// DEBUG (bring-up, default OFF): force the player mech to walk full-ahead with
|
|
// no keypress so locomotion + camera-follow can be verified in a headless run.
|
|
// BT_FORCE_THROTTLE may carry a VALUE: +1 full run, ~0.3 walk, -1 reverse (default 1.0).
|
|
if (getenv("BT_FORCE_THROTTLE") != NULL)
|
|
{
|
|
gBTDrive.forced = 1;
|
|
float v = (float)atof(getenv("BT_FORCE_THROTTLE"));
|
|
gBTDrive.forcedThrottle = (v != 0.0f) ? v : 1.0f; // bare "1"/empty -> full ahead
|
|
}
|
|
if (getenv("BT_FORCE_FIRE") != NULL) gBTDrive.fireForced = 1;
|
|
|
|
// (environ.ini is loaded much earlier now -- see BTLoadEnvironIni.)
|
|
|
|
// Version scheme: 4.10 = the 1995 arcade release; 4.11 = this win32
|
|
// reconstruction; build = git commit count, hash pins exact source
|
|
// ('+' = built from an uncommitted tree). Stamped by tools/btversion.cmake.
|
|
std::cout << "BattleTech " << BT_VERSION_FULL
|
|
<< " (win32 reconstruction of Tesla 4.10)" << std::endl << std::flush;
|
|
|
|
#ifdef BT_EXPIRE
|
|
// TESTER-BUILD EXPIRY TRIPWIRE (compile gate BT_EXPIRE, default ON --
|
|
// see CMakeLists). Distributed zips go stale and stray; refuse to run
|
|
// BT_EXPIRE_DAYS after the build day (btversion.h BT_BUILD_UNIX, UTC
|
|
// midnight). Dev builds re-stamp every build, so this never fires
|
|
// locally. Quality gate, not DRM.
|
|
{
|
|
const time_t expires = (time_t)BT_BUILD_UNIX
|
|
+ (time_t)BT_EXPIRE_DAYS * 86400;
|
|
const time_t now = time(0);
|
|
if (now >= expires)
|
|
{
|
|
std::cout << "[boot] TEST BUILD EXPIRED (built " << BT_BUILD_DATE
|
|
<< ", " << BT_EXPIRE_DAYS << "-day test window)"
|
|
<< std::endl << std::flush;
|
|
char expired_text[256];
|
|
sprintf(expired_text,
|
|
"This BattleTech test build (%s, built %s) has expired.\n\n"
|
|
"Test builds stop working %d days after they are made so\n"
|
|
"stale copies don't wander around.\n\n"
|
|
"Ask the operator for the current build.",
|
|
BT_VERSION_STRING, BT_BUILD_DATE, (int)BT_EXPIRE_DAYS);
|
|
MessageBoxA(NULL, expired_text,
|
|
"BattleTech -- test build expired", MB_OK | MB_ICONWARNING);
|
|
return 0;
|
|
}
|
|
int days_left = (int)((expires - now + 86399) / 86400);
|
|
std::cout << "[boot] test build window: " << days_left
|
|
<< " day(s) left (built " << BT_BUILD_DATE << ")"
|
|
<< std::endl << std::flush;
|
|
}
|
|
#endif
|
|
|
|
// CPU pin (timing stability). BT_AFFINITY overrides the mask; =0 disables --
|
|
// required for multi-instance runs (two instances pinned to core 0 starve
|
|
// each other, and the L4NET connect-retry loop busy-waits).
|
|
{
|
|
DWORD_PTR mask = getenv("BT_AFFINITY")
|
|
? (DWORD_PTR)strtoul(getenv("BT_AFFINITY"), 0, 0) : (DWORD_PTR)1;
|
|
if (mask != 0)
|
|
SetThreadAffinityMask(GetCurrentThread(), mask);
|
|
}
|
|
|
|
// Parse the command line so the egg (mission descriptor) source is set:
|
|
// btl4.exe -egg <file.egg>
|
|
// With no network common address (single-user mode), L4NetworkManager reads
|
|
// GetEggNotationFileName() and posts the egg locally -- no real net needed.
|
|
int argc;
|
|
LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
if (!L4Application::ParseCommandLine(argc, argv, L4Application::ParseToken))
|
|
{
|
|
std::cout << "ParseCommandLine failed (need: -egg <file>)" << std::endl << std::flush;
|
|
return 1;
|
|
}
|
|
|
|
Start_Registering();
|
|
|
|
// Create the main window (single 800x600 view for dev; pod multi-monitor is Phase 8).
|
|
WNDCLASS wc;
|
|
if (!hPrevInstance)
|
|
{
|
|
wc.style = 0;
|
|
wc.lpfnWndProc = (WNDPROC)WndProc;
|
|
wc.cbClsExtra = 0;
|
|
wc.cbWndExtra = 0;
|
|
wc.hInstance = hInstance;
|
|
wc.hIcon = LoadIcon((HINSTANCE)NULL, IDI_APPLICATION);
|
|
wc.hCursor = LoadCursor((HINSTANCE)NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
|
wc.lpszMenuName = NULL;
|
|
wc.lpszClassName = L"MainWndClass";
|
|
if (!RegisterClass(&wc)) return FALSE;
|
|
}
|
|
|
|
//
|
|
// Window/backbuffer resolution (task #20): the engine already parses
|
|
// `-res W H` (L4APP.cpp:165) for the D3D backbuffer; the window must match
|
|
// or the render is stretched. Parse the same args here for the WINDOW
|
|
// (client area = render size); default stays the authentic pod 800x600.
|
|
// Launch with e.g. `-egg DEV.EGG -res 1600 1200` for a 4x-pixel view.
|
|
//
|
|
int winW = 800, winH = 600;
|
|
{
|
|
const char *rp = lpCmdLine ? strstr(lpCmdLine, "-res") : 0;
|
|
if (rp != 0)
|
|
{
|
|
int w = 0, h = 0;
|
|
if (sscanf(rp + 4, " %d %d", &w, &h) == 2 && w >= 320 && h >= 240)
|
|
{
|
|
winW = w;
|
|
winH = h;
|
|
}
|
|
}
|
|
}
|
|
// DOCK-BOTTOM (single-window gauges, 2026-07-12): under BT_DEV_GAUGES the
|
|
// gauge strip is APPENDED below the world view in THIS window (strip height
|
|
// = width x 480/1320, the panel's design aspect); BT_DEV_GAUGES_WINDOW=1
|
|
// restores the old separate MFD window.
|
|
{
|
|
extern int gBTGaugeDockBottom;
|
|
extern int gBTGaugeCockpit;
|
|
extern int gBTCockpitCanvasW, gBTCockpitCanvasH;
|
|
extern int BTGaugeStripHeightFor(int width);
|
|
extern void BTCockpitCanvasFor(int viewW, int viewH, int *cw, int *ch);
|
|
// The mode came from the ONE resolver above (glassLayout) -- BT_GLASS_PANELS
|
|
// keeps the world window at its normal size (the per-display windows own the
|
|
// gauges: no cockpit surround, no bottom strip).
|
|
{
|
|
if (glassLayout == GlassLayoutCockpit)
|
|
{
|
|
gBTGaugeCockpit = 1;
|
|
// -res W H = the WORLD VIEW size in cockpit mode; default 900x500.
|
|
int viewW = 900, viewH = 500;
|
|
if (strstr(lpCmdLine ? lpCmdLine : "", "-res") != 0) { viewW = winW; viewH = winH; }
|
|
// Clamp the CANVAS to the work area (minus the window frame): shrink
|
|
// viewH (floor 400) then viewW (floor 640) until it fits (1080p tight).
|
|
RECT wa = { 0, 0, 1920, 1080 };
|
|
SystemParametersInfo(SPI_GETWORKAREA, 0, &wa, 0);
|
|
RECT fr = { 0, 0, 1000, 1000 };
|
|
AdjustWindowRect(&fr, WS_OVERLAPPEDWINDOW, FALSE);
|
|
int availW = (wa.right - wa.left) - ((fr.right - fr.left) - 1000);
|
|
int availH = (wa.bottom - wa.top) - ((fr.bottom - fr.top) - 1000);
|
|
int cw, ch;
|
|
BTCockpitCanvasFor(viewW, viewH, &cw, &ch);
|
|
while (ch > availH && viewH > 400) { viewH -= 20; BTCockpitCanvasFor(viewW, viewH, &cw, &ch); }
|
|
while (cw > availW && viewW > 640) { viewW -= 20; BTCockpitCanvasFor(viewW, viewH, &cw, &ch); }
|
|
winW = cw; winH = ch;
|
|
gBTCockpitCanvasW = winW; // windowed backbuffer = client canvas
|
|
gBTCockpitCanvasH = winH;
|
|
//
|
|
// LETTERBOX INTENT, decided HERE rather than at device creation:
|
|
// the first WM_SIZE arrives before the device exists, and the
|
|
// world-aspect calc needs to know whether the canvas will be
|
|
// scaled uniformly (aspect = the view's own) or stretched to
|
|
// the client (aspect = the stretched one). Deciding it late
|
|
// gave a -fit boot aspect=3.14 on an ultrawide -- applied on
|
|
// the first frame, since nothing resizes the window again.
|
|
// L4VIDEO clears the flag if it cannot actually get COPY.
|
|
{
|
|
extern int gBTCockpitLetterbox;
|
|
const char *ms = getenv("MULTISAMPLE");
|
|
gBTCockpitLetterbox = (ms == NULL || atoi(ms) == 0) ? 1 : 0;
|
|
}
|
|
std::cout << "[cockpit] view " << viewW << "x" << viewH
|
|
<< " canvas " << winW << "x" << winH << std::endl << std::flush;
|
|
}
|
|
else if (glassLayout == GlassLayoutDock)
|
|
{
|
|
gBTGaugeDockBottom = 1;
|
|
// Readability default (user-reported: the strip at 800 wide is a
|
|
// 0.6x downscale, hard to read): a WIDER world region (the
|
|
// projection is aspect-correct for any shape) buys strip pixels --
|
|
// 1100x600 world + 1100x400 strip = a 1000-tall window that fits a
|
|
// 1080p desktop with the panel at 83% of design size. -res still
|
|
// overrides both.
|
|
if (strstr(lpCmdLine ? lpCmdLine : "", "-res") == 0)
|
|
{
|
|
winW = 1100;
|
|
winH = 600;
|
|
}
|
|
winH += BTGaugeStripHeightFor(winW);
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// -fit (alias -windowed-fullscreen): borderless over the whole monitor.
|
|
// The cockpit canvas letterboxes inside it at one uniform scale, so a
|
|
// wider-than-canvas monitor gets black bars rather than a stretch -- the
|
|
// same deal a dragged window gets, just without the chrome. Only useful
|
|
// with a layout that HAS a canvas to fit; other modes keep their window.
|
|
//
|
|
extern int gBTFitDisplay; // parsed early (the menu exits before here)
|
|
int fitDisplay = gBTFitDisplay;
|
|
|
|
RECT wr = { 0, 0, winW, winH };
|
|
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
|
|
|
DWORD winStyle = WS_OVERLAPPEDWINDOW;
|
|
int winX = 0, winY = 0;
|
|
if (fitDisplay)
|
|
{
|
|
RECT mon = { 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) };
|
|
MONITORINFO mi;
|
|
memset(&mi, 0, sizeof(mi));
|
|
mi.cbSize = sizeof(mi);
|
|
POINT origin = { 0, 0 };
|
|
HMONITOR hmon = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
|
|
if (GetMonitorInfo(hmon, &mi))
|
|
mon = mi.rcMonitor;
|
|
winStyle = WS_POPUP | WS_CLIPCHILDREN;
|
|
winX = mon.left;
|
|
winY = mon.top;
|
|
wr.left = 0; wr.top = 0;
|
|
wr.right = mon.right - mon.left;
|
|
wr.bottom = mon.bottom - mon.top;
|
|
std::cout << "[cockpit] -fit: borderless " << (wr.right - wr.left) << "x"
|
|
<< (wr.bottom - wr.top) << " (canvas letterboxes inside)"
|
|
<< std::endl << std::flush;
|
|
}
|
|
|
|
//
|
|
// Window identity (MP dev): tag the title with the -net port so a player
|
|
// running two nodes side-by-side can tell the windows apart when
|
|
// reporting ("node 1501" = A / player 1, "node 1601" = B / player 2).
|
|
//
|
|
wchar_t winTitle[64];
|
|
{
|
|
int netPort = 0;
|
|
const char *np = lpCmdLine ? strstr(lpCmdLine, "-net") : 0;
|
|
if (np != 0)
|
|
sscanf(np + 4, " %d", &netPort);
|
|
if (netPort > 0)
|
|
swprintf(winTitle, 64, L"BattleTech %S \x2014 node %d",
|
|
BT_VERSION_STRING, netPort);
|
|
else
|
|
swprintf(winTitle, 64, L"BattleTech %S", BT_VERSION_STRING);
|
|
}
|
|
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, winStyle,
|
|
winX, winY, wr.right - wr.left, wr.bottom - wr.top,
|
|
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
|
if (!hWnd) return FALSE;
|
|
|
|
ShowWindow(hWnd, nShowCmd);
|
|
|
|
{
|
|
std::cout << "[boot] opening btl4.res..." << std::endl << std::flush;
|
|
StreamableResourceFile resources("btl4.res", version);
|
|
Check(&resources);
|
|
std::cout << "[boot] resources OK; creating ApplicationManager..." << std::endl << std::flush;
|
|
|
|
// Frame rate (task #20 perf): TARGETFPS was read with no null check --
|
|
// atoi(NULL) when unset -> garbage frameRate -> the APPMGR frame pacer
|
|
// (end_of_frame = now + 1/frameRate) degenerated and the game ran at a
|
|
// timer-quantized ~15 FPS (66-70ms frames measured) on ANY hardware.
|
|
// Default 60; env TARGETFPS still overrides (clamped to sanity).
|
|
int target_fps = 30; // the pod's authentic rate; frame work (~20-40ms in
|
|
// this build) misses 60Hz beats -> jitter, but holds
|
|
// 30 rock-steady. TARGETFPS env still overrides.
|
|
{
|
|
const char *tf = getenv("TARGETFPS");
|
|
if (tf != 0)
|
|
{
|
|
int v = atoi(tf);
|
|
if (v >= 15 && v <= 240)
|
|
target_fps = v;
|
|
}
|
|
}
|
|
std::cout << "[boot] target frame rate: " << target_fps << " fps" << std::endl << std::flush;
|
|
ApplicationManager *app_manager =
|
|
new ApplicationManager(hInstance, hWnd, (Scalar)target_fps);
|
|
Register_Object(app_manager);
|
|
std::cout << "[boot] ApplicationManager OK; creating BTL4Application..." << std::endl << std::flush;
|
|
|
|
Application *new_app = new BTL4Application(hInstance, hWnd, &resources);
|
|
btl4App = new_app;
|
|
Register_Object(new_app);
|
|
std::cout << "[boot] BTL4Application OK; StartApplication..." << std::endl << std::flush;
|
|
app_manager->StartApplication(new_app);
|
|
std::cout << "[boot] StartApplication returned; entering RunMissions..." << std::endl << std::flush;
|
|
|
|
app_manager->RunMissions();
|
|
std::cout << "[boot] RunMissions returned (mission loop exited)." << std::endl << std::flush;
|
|
|
|
// MATCHLOG AUTO-UPLOAD (2026-07-22): in relay mode, send this peer's
|
|
// match forensic log back to the operator's relay (saved under the
|
|
// relay's matchlogs\ folder). No-op for mesh/solo or when the
|
|
// matchlog never armed; the local file stays either way.
|
|
{
|
|
extern void BTRelayUploadMatchLog(void);
|
|
BTRelayUploadMatchLog();
|
|
}
|
|
|
|
// LOBBY LOOP (2026-07-23): a round ended by the operator/clock
|
|
// relaunches this pod straight back into the join wait -- the arcade
|
|
// between-rounds flow (pods rebooted to attract mode; players stayed
|
|
// put). BT_SEAT_CLAIM carries our seat tag so the relay gives us the
|
|
// SAME seat back (static ordering, real-pod style); BT_CALLSIGN /
|
|
// BT_MECH ride the inherited environment so the choice persists.
|
|
// Closing the window still exits for good (flag set only by
|
|
// StopMission).
|
|
if (gBTMissionStoppedByConsole && getenv("BT_RELAY") != NULL)
|
|
{
|
|
extern const char *BTRelaySelfTag(void);
|
|
const char *seat_tag = BTRelaySelfTag();
|
|
if (seat_tag != NULL && seat_tag[0] != 0)
|
|
SetEnvironmentVariableA("BT_SEAT_CLAIM", seat_tag);
|
|
char rejoin_arguments[512];
|
|
int m = 0;
|
|
for (const char *s2 = lpCmdLine;
|
|
s2 != NULL && *s2 && m < (int)sizeof(rejoin_arguments) - 1; ++s2)
|
|
rejoin_arguments[m++] = *s2;
|
|
rejoin_arguments[m] = 0;
|
|
std::cout << "[boot] round ended -- relaunching into the join wait"
|
|
<< std::endl << std::flush;
|
|
BTFE_RelaunchSelfAndExit(rejoin_arguments); // never returns
|
|
}
|
|
|
|
#ifdef BT_GLASS
|
|
//
|
|
// Front-end loop (glass step 3): a menu-launched mission returns
|
|
// to the menu on exit. (Marshal missions normally relaunch from
|
|
// the marshal thread at clock expiry; this tail covers raw-solo
|
|
// and early exits.)
|
|
//
|
|
if (getenv("BT_FE_LOOP") != NULL)
|
|
{
|
|
std::cout << "[fe] mission over -- relaunching the menu"
|
|
<< std::endl << std::flush;
|
|
BTFE_RelaunchSelfAndExit("");
|
|
}
|
|
#endif
|
|
|
|
btl4App = NULL;
|
|
Unregister_Object(app_manager);
|
|
delete app_manager;
|
|
}
|
|
|
|
Stop_Registering();
|
|
return Exit_Code;
|
|
}
|