The core 'single-player without the operator console' capability: a
zero-argument launch now builds its own mission and plays.
- game/reconstructed/btl4fe.{hpp,cpp}: the BattleTech catalog (8 maps,
8 mechs + variants, colors) and BTFeMission_WriteEgg -- emits the exact
console egg format (verified against content/MP.EGG): [mission],
[ordinals] + the 4 static rank-place plasma bitmaps, [pilots],
per-pilot loadout sections, and [largebitmap]/[smallbitmap] with
GDI-rendered 128x32 / 64x16 pilot-name plasma bitmaps.
- L4APP.H: SetEggNotationFileName / SetNetworkCommonFlatAddress setters
(from RP412) so the front end feeds the standard -egg load path.
- btl4main.cpp WinMain: front-end mode -- no -egg and no -net builds
frontend.egg locally (BTFrontEnd_Run) and boots it. -egg/-net runs
are untouched.
Verified: no-args launch -> '[frontend] built frontend.egg' -> mission
loads and runs; the generated egg is structurally identical to MP.EGG
(32x32/16x16 name bitmaps with real glyph pixels, all sections present).
Remaining in Phase 5 (deferred): the interactive on-screen catalog menu,
the in-process LocalConsole marshal (mission clock + StopMission at
expiry + single-binary loop), and the score-intake/results screen (new
for BT -- no mission-end flow exists yet). (Phase 5 of the roadmap)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
513 lines
22 KiB
C++
513 lines
22 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 <crtdbg.h> // _CrtSetDbgFlag (heap validation, gated by BT_HEAPCHECK)
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#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;
|
|
|
|
//===========================================================================//
|
|
// 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_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).
|
|
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));
|
|
}
|
|
return 0;
|
|
case WM_CLOSE:
|
|
DestroyWindow(hWnd);
|
|
return 0;
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
|
}
|
|
|
|
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
|
{
|
|
// 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;
|
|
logfile.open(getenv("BT_LOG") ? getenv("BT_LOG") : "btl4.log");
|
|
std::cout.rdbuf(logfile.rdbuf());
|
|
|
|
// 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.
|
|
// ---------------------------------------------------------------------------
|
|
int gBTPlatformPod = 0;
|
|
{
|
|
const char *pe = getenv("BT_PLATFORM");
|
|
if (pe && _stricmp(pe, "pod") == 0) gBTPlatformPod = 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 (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
|
|
}
|
|
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");
|
|
|
|
std::cout << "[boot] platform profile: "
|
|
<< (gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
|
|
: "DEV (single window + keyboard)")
|
|
<< std::endl << std::flush;
|
|
|
|
// 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;
|
|
|
|
// Optional environ.ini overrides (same convention as RP).
|
|
FILE *file;
|
|
char line[1024];
|
|
if (fopen_s(&file, "environ.ini", "r") == 0)
|
|
{
|
|
while (!feof(file))
|
|
{
|
|
if (fgets(line, sizeof(line), file))
|
|
{
|
|
for (int i = (int)strlen(line); i >= 0; i--)
|
|
if (line[i] == '\n' || line[i] == '\r') line[i] = 0;
|
|
putenv(line);
|
|
}
|
|
}
|
|
fclose(file);
|
|
}
|
|
|
|
std::cout << "BattleTech v4.10 (reconstructed port)" << std::endl << std::flush;
|
|
|
|
// 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;
|
|
}
|
|
|
|
//
|
|
// 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.
|
|
// Build one locally -- the in-game front end assembles a mission egg from
|
|
// the default loadout and points the standard -egg load path at it. The
|
|
// interactive on-screen menu and the in-process marshal layer on top of
|
|
// this (docs/BT412-ROADMAP.md Phase 5). `-egg`/`-net` runs are untouched.
|
|
//
|
|
{
|
|
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] built frontend.egg from the default loadout"
|
|
<< std::endl << std::flush;
|
|
else
|
|
std::cout << "[frontend] egg build FAILED -- falling through"
|
|
<< std::endl << std::flush;
|
|
}
|
|
}
|
|
|
|
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 BTGaugeStripHeightFor(int width);
|
|
if (getenv("BT_DEV_GAUGES") != 0 && getenv("BT_DEV_GAUGES_WINDOW") == 0)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
RECT wr = { 0, 0, winW, winH };
|
|
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
|
|
|
//
|
|
// 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 \x2014 node %d", netPort);
|
|
else
|
|
wcscpy(winTitle, L"BattleTech");
|
|
}
|
|
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, WS_OVERLAPPEDWINDOW,
|
|
0, 0, 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;
|
|
|
|
btl4App = NULL;
|
|
Unregister_Object(app_manager);
|
|
delete app_manager;
|
|
}
|
|
|
|
Stop_Registering();
|
|
return Exit_Code;
|
|
}
|