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) <noreply@anthropic.com>
This commit is contained in:
+104
-4
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user