Rodent's Revenge (WEP2, 16-bit)

test/binaries/wep16/WEP2/RODENT.EXE — Microsoft Entertainment Pack 2, 1991, Christopher Lee Fraley, written in Visual Basic 1 (NE, links VBRUN100 plus the pack's FIELD100 custom control and WEPUTIL). App id wep16_rodent; it is on the desktop (DESKTOP_APPS) as of 2026-08-25.

There is a second Rodent's Revenge in the tree — Rodent2000 in test/binaries/wep32-community/, a 2002 VB6 remake. Both editions are playable; the original remains the desktop edition and the remake is separately selectable as rodent2000. See rodent2000.md for the remake's OLE picture startup fix and gameplay command.

High Scores close corruption and invisible saved names (2026-09-10)

Two independent bugs reproduced in non-isolated Chrome with the compatibility WASM artifact explicitly forced (tail-call support selects the artifact; SharedArrayBuffer availability does not).

Verification: test-win16-user-scratch.js checks all DGROUP bytes, the four-slot ring, DRAWITEM fields, and EnumFonts far pointers. The window-surface regression checks Win16 implicit/explicit and Win32 dialog clips. A compat-browser probe substituted the real WepScore API call for WepFame with a test score of 9999, typed PhoneProbe through browser keyboard events, inspected both real score lists and their pixels, and closed back to the idle game with ESP 0x1179ea. This exercises WEPUTIL's actual entry/save/viewer code, not a naturally earned game-over or Safari keyboard policy. Probe: /private/tmp/rodent-compat-close.js with SCORE_PROBE=1; capture: /private/tmp/rodent-score-saved.png.

The broader test-win16-dialog.js Solitaire suite still fails its final Cancel/closed-screenshot assertion, identically with the pre-fix HEAD artifact and the new build. It is not a green-suite claim; the scratch, clipping matrix, native caret, and actual WEPUTIL resource tests pass.

The board-profile presentation area is now used for both Fit and Fill, including modal windows, and accounts for the CSS safe-area insets and visual viewport top. A host already below the status bar does not double the inset; keyboard-open retains the frozen area and landscape retains its side rails. Single-app and touch-control tests cover these bounds. The phone preview uses Chrome with an explicitly simulated 47px top inset, not an actual Safari capture.

Idle CPU: Win16 WaitMessage busy loop (2026-09-10)

Browser block tracing found a repeating 34-block pump, ending at 0x001e0395, with USER ordinal 112 (WaitMessage) as the last API on successive slices. The handler set only yield_flag, leaving reason 0: the host immediately posted another MessageChannel slice instead of sleeping. This was an explicit wait misimplemented as polling, not a missing clock-spin heuristic. Absolute PCs describe this particular NE load, not a stable module map.

The handler now completes its Pascal CS:IP return and sets queue park 15. The host's stack-neutral clear-yield resumes the caller, with input waking the sleep immediately and timers bounding its duration (still capped at 50ms). Do not use the Win32 message-wait resume here: its stdcall pop corrupts the 16-bit stack. test-win16-wait-message.js checks the return PC and exact stack cleanup before and after wake.

The first browser rerun fixed idle but left playing at ~83% of one core: _parkedSleepMs suppressed sleep for 120ms after every input event, even when the guest had just explicitly parked again. Fresh spin/queue parks now bypass that grace period; new input still cancels the sleep via _wakeStep. test-browser-park-sleep.js pins this single-use exception.

Apple M1, headless Chrome 152, visible 390x664 phone viewport, 15-second renderer-process CPU deltas: before idle 101.1%, paused 101.9%, playing 102.2–102.8%; after both fixes idle 0.66%, paused 0.60%, playing 2.22–2.56%. Gameplay uses real F2/F3 transitions and arrow pulses every 150ms; screenshots verify a changed board and the actual paused message. These are desktop Chrome measurements, not iPhone battery measurements. Host load was above the quiet-box threshold, so percentages are indicative, not a fine-grained benchmark. Raw before/final results and screenshots are under /private/tmp/rodent-cpu-*; source/cache version 302 on the isolated LAN build.

Reaching gameplay headlessly

Phone board presentation (2026-09-10)

Fill contains the full 276x276 FIELD100 board including its cyan one-tile perimeter. Its window-local bounds are x=3, y=78 within the 282x357 form. Portrait reserves 204 CSS pixels below the board for the D-pad and New Game; Fill/Keyboard sit directly above New Game. Landscape centres the intact square between left D-pad and right action controls, using the available height. Rotation preserves the window's guest backing extent so a shorter viewport cannot cut away the bottom rows before presentation. Fit retains window chrome; dialogs and open menus temporarily use the composed window bounds in Fill.

Portrait now omits the 12px wall column on each side (252x276 crop), preserving all playable cells and the top/bottom wall. Landscape retains the full square. Two-finger separation continuously interpolates window/board presentation and snaps to the nearer endpoint on release; cancellation restores the starting mode. Keyboard opening freezes control anchors and counteracts the parent view's caret shift, allowing the keyboard to cover the controls.

The app deals a board at launch, but a game only starts from the menu:

node test/run.js --app=wep16_rodent --max-batches=11000 --no-close \
  --input='6000:click:203:71,7000:click:230:92,8500:png:/tmp/a.png,\
8600:keydown:38,8700:keyup:38,8900:keydown:38,9000:keyup:38,10500:png:/tmp/b.png'

click:203:71 is the Game menu, click:230:92 is New Game. Arrow keydowns then push a column of blocks and the score climbs (5 → 12 over three keys, 1317 px changed in a 232x264 box). Menu-bar hit points at 640x480: Game 203,71, Options 253,71, Help 300,71; Game popup items at y = 92 / 112 / 133 / 173.

A 16-bit app reports 0 API calls and prints no host census when it is healthy — that is not a sign of a dead run.

Browser Worker startup (fixed 2026-08-30)

With the experimental Threads switch enabled, the NE executable was loaded in slot 0's Worker but its NE DLLs were loaded into the idle main-thread WASM instance. That split the selector/module state: RODENT's first far import into VBRUN100 remained unresolved and trapped at EIP=0x100010.

The guest-worker protocol now runs loadWin16Dlls beside the instance that loaded the NE task. Its VBRUN100 selector, app-local modules, and far-import fixups therefore share one arena. test/test-worker-guest.js keeps Threads on, requires the Worker backend, and checks the rendered green Rodent board.

Browser Worker keyboard focus (fixed 2026-08-30)

The renderer's Worker-owned WASM is only a browser-side shadow. Mouse clicks updated its focus global, while slot 0 kept $focus_hwnd == 0; a following arrow therefore reached the top-level form instead of the 276x276 VB picture child that implements the board. The frame stayed alive and its timer kept firing, which made this look like frozen rendering rather than wrong input.

Renderer focus changes now ride on the next slot-0 slice. The Worker performs the normal set_focus notification and then mirrors USER's focus bookkeeping before it dequeues a following key. Opening Game > New Game consequently leaves live focus on 0x10005, matching cooperative execution. The Worker browser regression holds Right and requires the board to change; the 2026-08-30 run changed 221 pixels with no trap. The reviewed frame has intact chrome, menu, mouse counter, cyan border, olive floor, and green block field.

Nothing owns the keyboard until you click (fixed 2026-08-31)

Reported from a real iPhone: the on-screen dpad (lib/touch-controls.js) does nothing. It is not the overlay and it is not the Worker focus bug above — the overlay reaches renderer.handleKeyDown with the right vk every time.

get_focus_hwnd() is 0 from launch until a press lands in the client area. Nothing in our activation chain records a focus window: ShowWindow sends WM_ACTIVATEAPP/WM_ACTIVATE/WM_SETFOCUS to the form but writes no focus, and --trace-win16 shows VBRUN100 calling USER.23 GETFOCUS exactly once, getting 0, and never calling SetFocus at all. With focus 0 the shared routing rule in lib/host-window.js (inputEventHwnd) sends every key to main_hwnd — the 0x10001 VB form, which ignores arrows. The board is 0x10005.

It is not touch-specific and not browser-specific. Headless, with no click:

node test/run.js --app=wep16_rodent --max-batches=11000 --no-close --quiet-api \
  --input='6000:keydown:113,6100:keyup:113,7000:png:/tmp/a.png,\
8600:keydown:38,8700:keyup:38,8900:keydown:38,9000:keyup:38,10500:png:/tmp/b.png'

gave 0 of 307200 pixels differ. Setting $focus_hwnd to 0x10005 by hand made the same keystrokes play the game, so the routing was right and only the seed was missing.

P._seedKeyboardFocus in lib/renderer-input.js now runs on each handleKeyDown: when nothing holds the focus and the deep child at the client centre covers at least half the top-level client area (and is not WS_DISABLED), that child is given the focus. Rodent's 276x276 board inside a 292x350 frame is 79% of the client, so it qualifies; a frame whose children are a toolbar and a status strip does not, and single-window games keep the old main_hwnd routing unchanged. The same command now changes 944 px and F2 alone deals a real game. test/test-keyboard-focus-seed.js pins all five cases.

Menu inventory and what each item does

Driven by click, item by item, on 2026-08-25. menu-sweep.js cannot do this one: RODENT.EXE has no RT_MENU (ne-dump.js --menus-json returns {}) because VB builds the menu at runtime from the form.

Menu Item Verdict
Game New Game (F2) works — deals mouse, cats, blocks
Game Pause (F3) works — "Paused. Press F3 To Continue."
Game High Scores… partial — the Hall of Fame dialog paints (trophies, OK, Clear Scores) and VBRUN then raises Control array element '0' does not exist over it
Game Exit works — resolves VB1's dynamic KERNEL WritePrivateProfileString lookup and closes cleanly
Options Level… partial — "Enter Starting Level: (1-50)" with Ok/Cancel paints, but the input box is not there: typing a digit and pressing Ok leaves the title at [1]
Options Snail / Slow / Medium / Fast / Blazing work — the radio check follows the selection (verified Slow → Blazing)
Help Index (F1) works — RODENT.HLP renders with live hyperlinks
Help How to Play works — own topic
Help Commands works — own topic
Help Using Help works — Help Topics dialog, Contents/Index tabs, 12 topics
Help About Rodent's Revenge… works — full WEP splash, author, VB credit

The High Scores error is a VBRUN100 runtime error raised by the guest, not an emulator trap. The former Exit error was different: VBRUN dynamically asks GetProcAddress(KERNEL, "WRITEPRIVATEPROFILESTRING"); the API existed as KERNEL.129 but was absent from the emulator's by-name export table, so the false NULL became VB error 35 (Sub or Function not defined). Mapping that name to the existing ordinal makes both the title-bar close button and Game > Exit close cleanly. The Level… input box is likely related to the remaining control-array shortfall one form over.

VBRUN100 VERR during phone interaction (fixed 2026-09-09)

An actual iPhone run trapped at linear 0x001401a4, bytes 0f 00 26 ad 91: VERR word ptr [91ad] in a loaded VBRUN100 segment. The two-byte decoder previously knew LAR but not the adjacent protected-mode selector-readability probe, so it emitted $th_bad_opcode and Safari reported RuntimeError: Unreachable code should not be executed.

Handler 447 now implements both register and memory forms. A mapped Win16 selector is readable under the descriptor model already synthesized by LAR; an unmapped selector is not, and VERR reports that result through ZF while preserving the other materialized arithmetic flags. The focused selector test executes the same disp16 memory encoding and checks both valid and invalid answers.

High Scores viewer check did not cover name entry (2026-09-09)

The Game > High Scores window is a viewer, not the new-score name prompt. A current dump-windows / dlg-dump run shows 20 static score/name labels, three trophy buttons, OK, and disabled Clear Scores; there is no Edit control. USER focus is correctly on the OK button (hwnd=0x1000f, control class 1, id 1). Consequently there is no USER caret to blink and the browser's caret-gated phone keyboard correctly stays closed. Adding a synthetic caret or forcing the keyboard here would send text to a push button. A real post-game name prompt, if reached, must be checked separately because it is a different form/path.

New-score caret missing from the compositor (fixed locally 2026-09-10)

The reported bug concerns earning a score and entering a name, not the viewer above. WEPUTIL.DLL's RT_DIALOG 200 says "You have achieved a high score!" / "Please enter your name:" and contains Edit id 500, a decorative IndentBox, and OK. ENTERDLGPROC (export 1002, segment 1:0x10bb) sets the name text and returns TRUE from WM_INITDIALOG; it relies on USER's initial focus.

Loading that exact resource through the Win16 template converter reproduces the mismatch: USER focus and its visible caret both name Edit 500, but renderer.caretRect() is null. Resource-created controls have WAT window records, not entries in renderer.windows; _paintCaretOverlay discarded the caret when that JavaScript record was absent. This also withheld the caret signal used by the phone keyboard and touch-overlay pass-through.

The compositor now resolves these children through WAT's existing absolute geometry exports and requires visible ancestry back to an owned renderer window. No synthetic caret, focus override, or extra rendering surface is introduced. Blink-off retains the keyboard anchor; hidden/destroyed/orphaned controls do not. test-win16-score-caret.js uses the original DLL resource and checks Edit 500 focus, caret, text insertion, and compositor visibility; test-renderer-native-caret.js covers blink phases, visibility, ancestry, ownership, and the existing JavaScript-backed window path.

Verification is resource-level plus automated input regressions. The bounded Chrome gameplay run reached score 188 but not game over; it does not establish end-to-end score saving. Actual iPhone keyboard appearance remains to verify on the local build. Main d61fb67f was subsequently merged into the 8087 worktree; its centralized cache identity and automatic test discovery replace the original renderer-201 cache bump and explicit test-list additions. Caret, resource typing, and mobile touch regressions pass after integration. No production deployment.

Status panel regression (fixed 2026-08-25, dec5373c)

The gray strip above the board — mouse count, stopwatch, score — turned into a white band at some point after 2026-08-22. Bisected to 8fdf2f5f, which fixed a latent i32.and-with-a-boolean in BeginPaint's class-brush fill (the fill had never actually run) and in the same move let it fire for a top-level window on the creation-time erase seed. A VB form registers its class with COLOR_WINDOW+1 and paints its real BackColor itself, so that second erase is white, over content the app had already drawn: 7645 px against the reviewed v86 capture.

dec5373c gates that seed on WS_CHILD. That left the 32x32 stopwatch child erased white the same way, one level down — 605 px — because children genuinely do need the seed: IdleWild's IWINFO pane is white in Win98 for exactly that reason and test-win16-wep1-gameplay asserts it.

The fix is ordering, not the seed. $win16_rearm_visible_child_erases (src/09e-win16-api.wat) re-arms the deferred NC sequence for every visible guest-wndproc child when a hidden parent is shown, and the erase bit was being cashed in lazily at that child's first BeginPaint — after VBRUN had already stamped the stopwatch into the picture control. USER erases when a window is shown, before the app draws. Erasing right there at re-arm time and clearing bit 1 restores that order: the capture is now pixel-identical (0 of 307200) to the reviewed 2026-08-22 wine-assembly reference, and IdleWild still gets its white background because its erase merely happens first instead of last.

The reviewed native reference is test/output/win16-v86-comparison/wep16_rodent/native.png (native.json has the v86 provenance); the 2026-08-22 wine-assembly capture beside it is a good before-image for this area. Native runs at 4-bit VGA, so the board reads tan and teal there against our olive and green — that difference is the palette, not a bug.

Stopwatch AutoRedraw padding (fixed 2026-08-31)

The v86 reference shows the analog stopwatch only after gameplay starts. The VB1 runtime draws that 32x32 PictureBox through a 34x34 AutoRedraw memory bitmap, then copies it into the parent status strip. If the compositor keeps the previous 32x32 child surface attached, that stale white surface covers the fresh parent pixels and the clock appears as a blank square.

$gdi_win16_autopresent_child_bitmap therefore accepts VBRUN100 child backing bitmaps up to two pixels larger than the child in each dimension. The match stays tight enough to avoid attaching unrelated sprite sheets, while covering Rodent's padded stopwatch bitmap.