//===========================================================================// // 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 #include // CommandLineToArgvW #include // _CrtSetDbgFlag (heap validation, gated by BT_HEAPCHECK) #include #include #include #include #include #include #include // LoadBgfFile (BGF probe) -- via game/fwd shim #include // 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=[,...] 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 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 // 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 )" << 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 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; }