run the game from inside the prefix, so it can save a replay

`Run-time error '76': Path not found` on finishing a game. Error 76 is VB6's
"path not found", and the path it could not find was the game's own.

Confining the prefix removed `z:`, which left `C:` as the only drive — and the
executable was in the plugin checkout, which `C:` does not cover. So wine gave
the game `\\?\unix\home\pastilhas\...\Minesweeper X.exe` as its own location
(confirmed with `winepath`), VB6 file I/O cannot write to a path of that shape,
and the first thing that needs to is the auto-saved recording. Which happens at
the END of a game, so the failure arrived after somebody had played.

The comment in prefix.ts already claimed "the game keeps its own directory, its
settings and its replays". It did not. It does now: `installGame` copies the
68 KB executable into `C:\Minesweeper` on every start and the game runs from
there, with cwd set to match so relative writes stop landing in C:\windows.

Copied rather than symlinked — a symlink out of drive_c is a door out of the
prefix, which is the exact thing confinePrefix exists to close. The cost is
that replays now live inside prefix/, so deleting it to unwedge wine takes them
along; that is in the README rather than left to be discovered.

Verified end to end rather than by inspection: set Auto-Save Recording to All,
played a beginner game to a loss, and
`Beg 1.16 - Aug 23 2026 22.09.18 PM (Lost).mvr` appeared in C:\Minesweeper.
Before this, that same moment was the error box. The default is "High scores",
which is why an ordinary losing game never reproduced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
This commit is contained in:
2026-08-23 22:10:30 +00:00
co-authored by Claude Opus 5
parent 26c8dd3e1a
commit 105068170c
4 changed files with 68 additions and 12 deletions
+12 -5
View File
@@ -1,7 +1,7 @@
import type { Subprocess } from 'bun';
import { existsSync, rmSync } from 'node:fs';
import { GAME_EXE, RUN_DIR, VNC_SOCKET, ensureDirs } from './paths';
import { WINE_ENV, confineAndVerify, ensurePrefix, prefixReady } from './prefix';
import { GAME_DIR, GAME_EXE, GAME_IN_PREFIX, RUN_DIR, VNC_SOCKET, ensureDirs } from './paths';
import { WINE_ENV, confineAndVerify, ensurePrefix, installGame, prefixReady } from './prefix';
import { run, waitFor } from './run';
/**
@@ -212,8 +212,8 @@ export async function status(): Promise<SessionStatus> {
};
}
const spawn = (cmd: string[], env: Record<string, string>, logName: string): Proc => {
const proc = Bun.spawn(cmd, { env: { ...process.env, ...env }, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
const spawn = (cmd: string[], env: Record<string, string>, logName: string, cwd?: string): Proc => {
const proc = Bun.spawn(cmd, { env: { ...process.env, ...env }, cwd, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
// Wine and x11vnc both say useful things only when something is wrong. Prefixed so `pm2 logs
// officer-minesweeper` shows which process is complaining.
for (const stream of [proc.stdout, proc.stderr]) {
@@ -270,8 +270,15 @@ async function startInner(log: (line: string) => void): Promise<SessionStatus> {
// ── the game ──
if (!alive(game)) {
// Into the prefix first. Run from the plugin directory the game has no DOS path of its own, and the
// first thing that needs one is saving a replay — which happens at the END of a game, so the failure
// arrives after somebody has played rather than when they start.
await installGame();
log('launching Minesweeper X under wine');
game = spawn(['wine', GAME_EXE], env, 'wine');
// cwd as well as the exe path: without it wine reports "could not open working directory" and starts
// the process in C:\windows, so anything the game writes relatively lands there instead.
game = spawn(['wine', GAME_IN_PREFIX], env, 'wine', GAME_DIR);
const drew = await waitFor(async () => !!(await windowGeometry()), 30_000);
if (!drew) {