SkiFree (ski32.exe, Microsoft Entertainment Pack)

test/binaries/entertainment-pack/ski32.exe, app id ski32. Image base 0x400000; single module, no DLLs of its own, so runtime VA == original VA (nothing here needs module+0x arithmetic).

Everything below is read straight out of the binary with the repo's own tools; the commands are given so each number can be re-derived rather than trusted.

Window class and wndproc

node tools/pe-imports.js test/binaries/entertainment-pack/ski32.exe --dll=USER32
node tools/disasm_fn.js test/binaries/entertainment-pack/ski32.exe 0x405440 90

USER32's IAT starts at 0x40a100. RegisterClassA is import [1] (0x40a104), called once at 0x4054dd; the WNDCLASS it fills sits on the stack and takes style = 0x2023 and lpfnWndProc = 0x405800.

VA What
0x405800 main wndproc
0x405986, 0x406924 the two call [0x40a150] (DefWindowProcA) sites

0x405800 splits three ways: msg <= 0x24 goes through a jump table at 0x4059c4/0x4059e0; above that, 0x405915 compares against 0x200 and peels off WM_KEYDOWN and WM_CHAR by subtraction:

00405915  cmp eax, 0x200
0040591a  ja  0x405986          ; -> DefWindowProc
0040591c  jz  0x405969          ; WM_MOUSEMOVE
0040591e  mov ecx, eax
00405920  sub ecx, 0x100
00405926  jz  0x40594d          ; WM_KEYDOWN -> call 0x406170
00405928  sub ecx, 0x2
0040592b  jnz 0x405995          ; default
0040592d  ...                   ; WM_CHAR   -> call 0x406780

WM_CHAR is never spelled as the literal 0x102. A byte search for the constant (node tools/find_bytes.js … --imm32=0x102) returns 0 hits and reads as "this app does not use WM_CHAR", which is wrong — the subtract chain above is how it tests for it. Do not conclude anything from the absence of a message constant in this binary.

Keyboard: two tables, and the character one is where the game lives

WM_KEYDOWN — 0x406170

00406170  lea eax, [ecx-0xd]      ; ecx = wParam (virtual key)
00406174  cmp eax, 0x65
00406177  ja  0x4061b1            ; default
0040617b  mov dl, [eax+0x4063bc]  ; slot index, vk 0x0d..0x72
00406181  jmp [0x4063a8+edx*4]    ; five arms

Dump both tables with node tools/dump_va.js test/binaries/entertainment-pack/ski32.exe 0x4063bc 102 and … 0x4063a8 20. Slot 4 is the default arm (0x4061b1), and only four virtual keys are anything else:

vk key arm
0x0d RETURN 0x40619e
0x1b ESCAPE 0x406188
0x71 F2 0x4061ab
0x72 F3 0x406198

That is the complete virtual-key vocabulary. There are no arrow keys: the skier follows the mouse pointer, which is why the app's touch layout has no dpad.

WM_CHAR — 0x406780

00406780  lea eax, [ecx-0x58]     ; ecx = wParam (character)
00406783  cmp eax, 0x21
00406786  ja  0x40684a            ; bare `ret`
0040678e  mov cl, [eax+0x40686c]  ; slot index, chars 0x58..0x79
00406794  jmp [0x40684c+ecx*4]

The table covers characters 0x580x79 only, and slot 7 is the do-nothing arm. Everything the game exposes to the keyboard beyond the four keys above is here, and all of it is case-sensitive by construction — 'F' (0x46) is below the range and is discarded before the table is even consulted.

char arm What
'f' 0x66 0x4067a0 toggles the speed flag at 0x40c670 (setz dl of its own value — a toggle, not a hold)
'r' 0x72 0x4067b3 tail-calls 0x401060 with [0x40c63c] and 0x40c6b0
't' 0x74 0x40679b jmp 0x401000
'x' 0x78 0x4067c3 moves the object at [0x40c72c] (fields +0x40/+0x42/+0x44) by +2 on one axis via 0x402390
'X' 0x58 0x4067e5 same, -2
'y' 0x79 0x406807 same, other axis +2
'Y' 0x59 0x406829 same, other axis -2

Named data

VA What
0x40c670 speed ("fast") flag, 0 or 1, toggled by 'f'
0x40c67c set once the game object exists; both key handlers bail when it is 0
0x40c72c pointer to the object 'x'/'y' reposition

Reproduction: proving the speed key works

Speed is a rate, so no screenshot can show it. Watch the flag instead:

node test/run.js --app=ski32 --quiet-api --no-close \
  --max-batches=900 --max-seconds=120 \
  --watch=0x40c670 --watch-log \
  --input='300:keydown:0x46,320:keyup:0x46,500:keypress:0x66,700:keypress:0x66'

Measured 2026-09-13: the watchpoint fires at batches 500 and 701 (the two characters) and never in the 300–500 window (the virtual key). --input's keypress: action is the one that produces WM_CHAR; keydown:/keyup: never do — see lib/renderer-input.js, where handleKeyPress is a separate entry point that the browser's own keypress event drives.

Dead ends / corrections

See also