`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
145 lines
8.5 KiB
Markdown
145 lines
8.5 KiB
Markdown
# Minesweeper
|
|
|
|
Minesweeper X, running under wine on a headless X display, streamed into an Officer panel.
|
|
|
|
It is a game. It is also the smallest honest proof that Officer can put an arbitrary X11 program in a
|
|
panel — a framebuffer, a process on it, a VNC server reading it, a client in the browser — and it is the
|
|
first plugin to use the platform's per-plugin websocket relay.
|
|
|
|
## What it needs
|
|
|
|
Six host binaries, declared in `manifest.ts` and checked before install: `wine`, `Xvfb`, `x11vnc`,
|
|
`xdotool`, `xdpyinfo`, `winetricks`.
|
|
|
|
The awkward one is wine. Minesweeper X is a **32-bit** PE (machine `0x014c`, Visual Basic 6), so the host
|
|
needs i386 multiarch and a `win32` prefix. A 64-bit-only wine cannot load it, and the failure is a bare
|
|
`Bad EXE format`. On Debian or Ubuntu:
|
|
|
|
```
|
|
sudo dpkg --add-architecture i386
|
|
sudo apt update
|
|
sudo apt install wine wine32:i386 xvfb x11vnc x11-utils xdotool winetricks
|
|
```
|
|
|
|
## The game
|
|
|
|
[Minesweeper X](https://minesweepergame.com/download/minesweeper-x.zip) — the competition build, with
|
|
timing and replay extras. `game/Minesweeper X.exe` is committed; it is 68 KB and it is the thing being
|
|
run rather than state produced by running it.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| zip sha256 | `21089193924919ef876a5b464f06bed9cc149867c51e40c259288ebe623014f0` |
|
|
| exe sha256 | `e414c207adaa14999b0464c3d0a701f20ed11282d5ed0f84b27b62d25389f661` |
|
|
|
|
The download needs a browser `User-Agent`; the site answers `403` to curl's default.
|
|
|
|
## Where its state lives
|
|
|
|
`DATA_PATH/minesweeper/` — never in this directory, which `plugins:update` will `git pull` over.
|
|
|
|
- `prefix/` — the wine prefix, ~600 MB, built on first `POST /start`. Deleting it is still the standard fix
|
|
for a wedged wine and the next start rebuilds it — but it now takes the replays with it, see below.
|
|
- `prefix/drive_c/Minesweeper/` — where the game RUNS from, and where its replays land. The executable is
|
|
copied here on every start; `C:` is the only drive the prefix has, so this is the only directory the game
|
|
has a writable path to. Launched from the plugin checkout instead, its own path came back as
|
|
`\\?\unix\home\…` and finishing a game raised `Run-time error '76': Path not found` while it tried to
|
|
auto-save the recording.
|
|
- `run/` — pid state, logs, and `vnc.sock`. **Mode 0700, and that is load-bearing.**
|
|
|
|
## Two gaps, both from having no window manager
|
|
|
|
Worth knowing before you conclude something is broken. Neither is hard to fix; neither is fixed.
|
|
|
|
**Some dialogs cannot be closed.** There is no window manager, so no window has a title bar. A dialog with
|
|
its own OK button is fine — the run-time error box is — but Extras → Auto Options has neither an OK button
|
|
nor a Cancel, and its only exit was the title bar. Restarting the sidecar clears it.
|
|
|
|
**The panel sends no keyboard events at all.** `MinesweeperPanel` forwards pointer events and nothing else,
|
|
so Escape does not reach the game and a Custom board cannot be typed in. Adding keys to the RFB client is
|
|
the smaller half of this; wine also expects a window manager to hand it focus.
|
|
|
|
## Two things worth knowing before you change anything
|
|
|
|
**x11vnc has no password, and that is correct — but only because it also has no port.** It binds a unix
|
|
socket inside `run/`, mode 0700. The kernel refuses every other account before x11vnc is asked anything,
|
|
so a password would be a second lock on a door only one person can reach. The version this replaced bound
|
|
`127.0.0.1:5907` with `-nopw`, which is a different claim entirely — every local account could open it and
|
|
take the display.
|
|
|
|
**`-unixsock` does not remove the TCP listener, and the first alpha shipped believing it did.** It adds a
|
|
unix socket beside the port; x11vnc was on `0.0.0.0:5900` and `[::]:5900` with no password, reachable from
|
|
the whole tailnet and the whole LAN, under a comment saying there was no port at all. Three flags close it
|
|
and each one earns its place: `-rfbport 0` for IPv4, `-rfbportv6 0` for IPv6 — `-rfbport 0` alone leaves
|
|
`[::]:5900` up — and `-noipv6` for outgoing sockets.
|
|
|
|
The check that matters is `ss -lntp` against the **running** process. Reading the arguments cannot show a
|
|
listener that exists because a flag is missing, and running the same arguments by hand did not reproduce
|
|
it either.
|
|
|
|
**The game is centred on a fixed stage, and the whole stage is what you see.** The framebuffer is 560x400
|
|
whatever the difficulty, `sidecar/session.ts` keeps the window in the middle of it, and the panel draws the
|
|
framebuffer entire — scaled to fit, never cropped. So changing difficulty moves the board rather than
|
|
resizing the panel, and the surround is teal (`-solid`) rather than the root's black.
|
|
|
|
**The stage is small because the panel scales it by whole numbers.** Pixel art at a fractional scale gets
|
|
uneven cells — some three screen pixels wide, their neighbours two — which reads as "not quite right"
|
|
rather than as blur, and `image-rendering: pixelated` does not help because unevenness is not blur. So the
|
|
panel picks the largest whole number of DEVICE pixels per source pixel and every pixel of empty stage is
|
|
panel the game does not get: in an 1866-device-pixel panel, 800 wide lands on 2x and 560 wide lands on 3x.
|
|
560x400 is expert (504x342) plus a margin. A custom board bigger than that is clipped at the right and
|
|
bottom rather than shrunk, with the menu bar kept reachable.
|
|
|
|
It used to park the window at the origin and have the client crop to the window's size, polled every four
|
|
seconds. That resized the panel under the player, and it was off by the 3x29 wine puts between the root
|
|
origin and the window — a black strip along the top and a row of cut-off cells along the bottom, worst on
|
|
expert. `GET /status` still reports the window geometry; nothing consumes it now, it is a diagnostic.
|
|
|
|
**The wine prefix is confined to `C:` on every start.** A fresh prefix maps the whole filesystem to `Z:`
|
|
and symlinks the profile into the real home, and Minesweeper X has Open and Save dialogs for its replays.
|
|
That is why this was an ungrantable `execution` capability in the platform. `sidecar/prefix.ts` closes it
|
|
instead, which is what makes the plugin's permission safe to grant. Wine restores `z:` whenever it updates
|
|
a prefix, so the confinement is re-asserted rather than done once.
|
|
|
|
## Contract
|
|
|
|
```
|
|
GET /api/minesweeper/status { installed, running, window, error? }
|
|
POST /api/minesweeper/start idempotent; builds the prefix on a cold install (minutes)
|
|
POST /api/minesweeper/stop
|
|
/api/plugin-ws/minesweeper the display, as RFB
|
|
```
|
|
|
|
## Where this came from
|
|
|
|
Built first as a standalone directory, `officer-dev/wine-minesweeper`, and folded in here when it became a
|
|
plugin. Two things from that work are worth keeping, because both are judgement calls somebody will
|
|
otherwise make again from scratch.
|
|
|
|
**Wine came from Ubuntu noble, not the WineHQ repository.** The distribution package is older — 9.0 — and
|
|
that is fine for a 2017 VB6 application. The alternative was adding a third-party apt source, with its key
|
|
and its update cadence, to a machine that runs a bitcoin wallet and a mail store, for a game of Minesweeper.
|
|
If a future version genuinely needs newer wine, that trade is worth revisiting; until then it is not.
|
|
|
|
**Enabling i386 pulled four krb5 libraries forward** within noble-updates. That was checked before
|
|
installing rather than after: nothing moved near libc, systemd, openssl or postgres. Worth re-checking on
|
|
any host where `dpkg --add-architecture i386` is new, since it is the one step here that changes how the
|
|
whole machine resolves packages and is never undone by uninstalling the plugin.
|
|
|
|
### The original verification baseline
|
|
|
|
From the standalone build, on Ubuntu noble with wine 9.0. Useful as a comparison when something feels wrong
|
|
rather than as a test to run:
|
|
|
|
- The game launches in about a second and draws the classic beginner board at **152x214**.
|
|
- A synthetic `xdotool` click opens a region and starts the timer — so **input reaches the game**, which is
|
|
the part that matters for a playable stream and the part a screenshot cannot tell you.
|
|
- x11vnc completes an **RFB 003.008** handshake and reports an 800x600 32bpp framebuffer.
|
|
- Cold start, display through to game window, in under two seconds. That excludes building the prefix,
|
|
which happens once and takes minutes.
|
|
|
|
The shell prototype that ran all this (`bin/session.sh`) is not kept. Everything it knew — waiting for a
|
|
usable display rather than a live pid, parking the window at the origin, and clearing the whole wine
|
|
process family on stop — is in `sidecar/session.ts`, with the reasoning attached to the code that depends
|
|
on it.
|