From 8408165d92516075809794fbe2a607bdc8141547 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 26 Jul 2026 02:41:46 -0500 Subject: [PATCH] Find content\ ourselves: a bare exe launch no longer scatters config and dies Field report: `.\btl4.exe -fit` from build\Release opens the mission console, then the mission crashes on Launch. ROOT CAUSE (pre-existing landmine, newly reachable). 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 never showed. A bare launch of the exe therefore 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 had never been created -- and killed the mission generation the menu launched (the child inherits the parent's cwd). Recent work made that path inviting rather than obscure: glass is the desktop default, a zero-arg launch opens the menu, and -fit is documented as something you pass on the command line. FIX: BTEnsureContentDirectory() runs before anything resolves a relative path (the log file included, so a bare launch logs where the launchers log). If cwd has no BTL4.RES, probe from the exe's own directory -- ..\..\content for the shipped layout, plus the flattened and in-content cases -- and SetCurrentDirectory there. Says so in the boot line when it had to go looking. Belt and braces: the environ.ini writer refuses to write unless BTL4.RES is beside it, so a failed probe cannot scatter a settings file either. ALSO: -fit was silently dropped when the menu relaunched into a mission. The front end rebuilds the child's command line from scratch, and the flag was parsed at window creation -- code the menu process exits long before reaching. Parsed early into gBTFitDisplay now, and appended to the relaunch. Verified: bare `btl4.exe -fit` from build\Release leaves ZERO files next to the exe, writes environ.ini + btl4.log into content\, and the mission child's exact command line (-net 1501 -platform glass) from that same wrong cwd now logs "cwd: found the content directory from the exe path", loads resources and enters RunMissions instead of dying. Co-Authored-By: Claude Opus 5 (1M context) --- game/btl4main.cpp | 108 +++++++++++++++++++++++++++++++++++-- game/glass/btl4console.cpp | 17 ++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/game/btl4main.cpp b/game/btl4main.cpp index 049e8cb..3d60afd 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -342,6 +342,80 @@ static const char *kEnvironIniDefault = "# 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; @@ -350,6 +424,14 @@ static void BTWriteDefaultEnvironIni(void) 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; @@ -424,6 +506,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // 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. @@ -475,10 +561,26 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR 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 @@ -1078,10 +1180,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // 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. // - int fitDisplay = 0; - if (lpCmdLine && (strstr(lpCmdLine, "-fit") != 0 || - strstr(lpCmdLine, "-windowed-fullscreen") != 0)) - fitDisplay = 1; + extern int gBTFitDisplay; // parsed early (the menu exits before here) + int fitDisplay = gBTFitDisplay; RECT wr = { 0, 0, winW, winH }; AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE); diff --git a/game/glass/btl4console.cpp b/game/glass/btl4console.cpp index e090429..6dff2a3 100644 --- a/game/glass/btl4console.cpp +++ b/game/glass/btl4console.cpp @@ -189,6 +189,23 @@ void command_line[n++] = (WCHAR)*s; } } + // + // Carry -fit into the child. This command line is built from scratch, so + // a display flag the player passed US is not in `arguments` -- without + // this, `btl4.exe -fit` gave a borderless menu and then a WINDOWED + // mission (field report 2026-07-26). + // + { + extern int gBTFitDisplay; + if (gBTFitDisplay) + { + static const WCHAR fit[] = L" -fit"; + for (const WCHAR *s = fit; *s && n < MAX_PATH + 250; ++s) + { + command_line[n++] = *s; + } + } + } command_line[n] = 0; STARTUPINFOW startup;