SimCity 2000 Network Edition (demo) — 2KCLIENT.EXE / 2KSERVER.EXE

Apps: simcity2000_net (client), simcity2000_net_server. Files under test/binaries/candidates/simcity-2000-network-edition-demo/installed/. Both images load at their preferred base 0x400000, so VA == runtime address.

Headless three-seat route (server + two clients)

C="--no-build --tick-ms-per-batch=1 --app=simcity2000_net --max-batches=100000000 \
   --no-close --stuck-after=100000000 --quiet-api --max-seconds=220"
node tools/vlan-pair.js --log-dir=$T --stagger-ms=12000 \
  -- --no-build --app=simcity2000_net_server --batch-size=100000 --max-batches=1000000 \
     --max-seconds=250 --no-close --stuck-after=100000000 --quiet-api \
     --input=100:post-cmd:57600,1400:click:180:313,2900:click:231:288 \
  -- ${=C} --input=<client A list> -- ${=C} --input=<client B list>

Client join list (A; B adds 31200:dlg-set-edit:1015:Deputy): 50:click:316:276,7500:click:200:120,15000:click:315:256,22500:click:265:343,31000:dlg-set-edit:1014:10.0.0.1,31500:click:408:232, plus dlg-cmd:1/dlg-cmd:2 every 3000 batches from 150000 to dismiss the join notices and the January budget dialog (every 15000 is too sparse — a budget dialog then swallows the input that follows it). The city is live around batch ~290000; ~1950 batches/s on the dev box, so budget --max-seconds accordingly (150 s ends at ~290000, too early).

The server overflows its own stack if a login ack is slow. While it waits, it appends one . per tick to "Awaiting Player's Ack of Login Packet" in a fixed stack buffer. On 2026-09-25, at load ~176, it then called a function pointer the dots had overwritten (call [ebp-0x44] at 0x4170dc → EIP 0x2e2e, ASCII ".."). That was unreachable at server batch 3254, and both clients then said "Could Not Connect".

The first client is "Mayor", the second "Deputy". Chat (post-cmd 32789) propagates and paints on the other client since 294a49e2.

UI is app-drawn — no windows, no WM_COMMAND

The tool palettes, their flyouts and the top toolbar are drawn by the app itself onto its own surfaces; opening a flyout creates no window (only a SetCapture on the frame). So dump-windows/post-cmd cannot reach them, and flyout pixels left in the unpainted area below the view are stale, not the live menu (the headless png while the button is held never shows the flyout).

Selecting a flyout item = press on the palette button, drag, release on the item. Route the drag through x=100 so it does not cross the lower palette (x 130–225), which opens that palette's flyout on hover instead. The status line at (7,123) names the selected tool — read it to confirm.

Where (640x480, city not maximized) What
click 18,175 Demolish (bulldozer button default)
drag 18,175 → 100,195 Level ($25)
click / drag 18,200 → 100,200 Water ($100)
drag 18,200 → 100,224 Trees ($3)
lower palette 140/165/190/213,193 power / police / school / parks; up-arrows at y=176 open flyouts above (Oil/Hydro/Coal; Marina/Stadium/Zoo/Large/Small Park)
top toolbar 262,56 land-ownership layer toggle (wireframe map); 285/310/333/356 are further layer toggles

Buy Land — the Owner Tool palette button (red arrow, 18,319; confirmed)

The palette flyouts are built by 0x418f46(group): group-1 indexes the byte table 0x41b664, which selects a case in the jump table 0x41b5f8. Each case names its group and adds (bitmap key, label, tool number) items via 0x430910 / 0x469460:

group case name (VA) items
1 0x418f8f Network Tool (0x4c8b68)
2 0x4191ad Dozer Tool (0x4c8c4c) Demolish, Level, ...
3 0x4193f8 Water Tool (0x4c8a04) Water, Trees, ...
4 0x41950d Owner Tool (0x4c8b3c) Buy (bmp 1013, tool 0x0d), Sell (1014, 0x0e), then 0x43 / 0x42
5 0x41972f Zone Tool (0x4c8bf8)
… 0x4199c3 / 0x419bec / 0x419e15 Police Fire / School / Parks

Correction (2026-09-28, live city): 18,223 is not the Owner button. Enlarged, the left palette reads:

y button
175 bulldozer
200 water drop
223 water tower (red-checkered tank on a stand)
247 road
271 tunnel
295 zone house
319 red arrow

Clicking 18,223 set the status line to "Water Tower: $250", and funds stayed $30,000.

Confirmed (2026-09-28, same day): the red arrow at 18,319 is the Owner Tool, and a click selects Buy Land.

Older leads, kept for reference:

"Buy Land (P)" at 0x4c76cc is a network protocol command name (the (P) / (H) table at 0x4c7c74). "Buy Land"/"Sell Land" at 0x4c8dec/0x4c8de0 are tool names (tool numbers 0x0c / 0x0d, table built at 0x41c3e2), grouped with Demolish/Dezone/Lower/Raise/Level. buyland.bmp/selland.bmp are loaded at 0x42c23d keyed 1013 / 1014 — bitmap keys for app-drawn buttons, not control or command IDs (no dialog or message map uses 1013).

Water on land the player does not own does nothing — no cost, no error. The "You don't own this land" text (0x4cd430, used at 0x4403c5 with caption "Place Error") never fired in any attempt. Drags to bulldozer-flyout rows 123/147/171/292/316 and clicks in the ownership layer bought nothing. The 0x41c3e2 name→number map is a lookup by label, not the UI.

Known render oddity

The city view paints only rows ~44–205 of the MDI client; everything below is black and never repainted (so stale flyout pixels persist there).

Correction (2026-09-25): 69d6aa0b's commit message and its comment in $handle_DefWindowProcA name this app as the case where a registered msctls_statusbar32 with no guest comctl32 behind it answered WM_GETFONT / SB_GETBORDERS with garbage, laid the bar out 267px tall and left the view 163px high. That was observed only in a scratch worktree without the app's DLLs mounted. The DefWindowProc answers themselves are correct for any MFC app with no comctl32.

Update (2026-09-25, same day): the real route has no guest comctl32 either — the manifest mounts only mfc40, Msvcrt40, DPLAY, OLEPRO32 and Webster — so it takes the same path. On main (after 69d6aa0b) a single client lays out its status bar as 628x18 at y=412 of the 640x480 frame (--app=simcity2000_net ... --input=2950:dump-windows, ~3000 batches, no server needed), so the view gets the full height. The black band's 161-row view matches the 163px symptom, so it was most likely this layout bug.

Confirmed (2026-09-28): in a three-seat run on main (6723deb6), both clients' city views fill the MDI client down to the status bar at y≈458. There is no black band. It was the status-bar layout.

That run:

Route notes from these runs: