The automation harness

July 12, 2026 · View on GitHub

Bringing up a game — load it, make it reach a menu or level, and figure out why it froze or rendered black — is the core debugging loop. BottleShip wraps that loop in an automation harness: fluent, self-judging verbs you can drive from the command line or the browser console, plus structured introspection that survives the multi-megabyte-per-second log firehose.

Running it

bun tools/harness.ts up        # cold start: launch Chrome + dev servers, reach "ready"
bun tools/harness.ts report    # one structured snapshot of the whole machine state
bun tools/harness.ts shot      # screenshot the (offscreen) canvas

up starts the dev server and a log server, launches Chrome with the flags the engine needs (including an autoplay policy so audio unlocks without a user gesture — audio-gated games stall silently otherwise), opens the bare emulator page, and waits until everything is healthy.

In the browser console the same capability is on window.__BS__.harness.

Driving a game

The harness exposes a fluent, self-checking DSL. A bring-up script reads like the steps a human would take, and each verb asserts its own success:

harness()
  .openWgb('/apps/mygame.wgb')
  .waitForEvent('dialogShow')
  .click('Play')
  .tickFrames(120)
  .expectSurfaceNonBlack('primary')
  .run()

Verbs cover loading bundles, waiting for events, synthetic input (clicks, keys), advancing frames, and asserting on rendered surfaces and engine state.

Seeing what happened

The canvas is an OffscreenCanvas the main thread can't read directly, and the guest generates far too much log output to grep. So the harness gives you structured views instead:

  • report() — the firehose-immune snapshot. One plain object with CPU registers, the module-labelled guest call stack, the recent WinAPI call ring (the last thunks that ran), the unimplemented-stub registry, recent page faults, and thread states. This is the first thing to pull for any non-standard situation (froze / vanished / black frame / wild EIP).
  • Stub registry. The usual reason a game "gracefully vanishes" is that it called an export or vtable slot with no implementation, got a garbage return, and took an "unsupported → exit" branch. The stub registry names exactly which unimplemented call it was and who called it.
  • Log tools. Instead of grepping the stream: a template-deduped summary (the same stub called 10,000× becomes one ranked row), signal→event watchers that block until a specific message appears, and time-windowed captures. A durable log-server archives the stream to disk.
  • Surfaces & textures. Dump a specific guest surface or texture to a PNG when a screenshot of the composited canvas isn't enough.

Reverse-engineering the guest

For understanding the guest binary itself, a warm RE service (Ghidra headless behind an HTTP daemon) is available through tools/re/ — decompile a function, resolve a live EIP back to a function name, or export a symbol map to load breakpoints from.

Diagnostic discipline

Confirm with data — a dump, a logged value, a report() — not by reasoning about how you think GDI or a vtable is laid out. Multi-DC composites, the canvas-vs-selected-bitmap distinction, and COM vtable topology all mis-model easily; a dump settles it.