3.2 KiB
W2 — the watchpoint module, and what it answered
Companion to docs/W2-predictions.md (written first) and to
sots-re/findings/control-flow/watchpoints-modcount-status.md (the full report).
What was added
src/shim/hooks/watchpoints.{h,cpp} — hardware data-write watchpoints on the live game.
- One MinHook detour in the whole module, on
StrategyServer::ApplyAllTurnCommands(0x0078f6a0), used only to learnSon the turn thread. Debug registers are per-thread, so the arming point has to run on that thread and precede the writes; A2 established that this function is the End-Turn command flush and that itsthisis theSframe. - DR0-DR3 as 4-byte data-write breakpoints, set with
GetThreadContext/SetThreadContexton the calling thread and read back and logged, plus a vectored exception handler that records EIP, the written value,DR6, the thread id, theEBPchain and the first four code-looking words aboveESP. - A canary self-test: DR3 is pointed at a word the shim owns, that word is written once, and the handler must report exactly one trap before any game address is believed. An arm that silently failed is indistinguishable from "nothing writes this" (method rule 1).
- A background flusher, because the game is usually killed rather than quit and
DLL_PROCESS_DETACHis not guaranteed. - Config:
watch=on|off,watch.players=0..2,watch.out=<path>.shim.cfg.w2watchandshim.cfg.w2controldiffer only inwatch=, so the pair is a real rule-19 control.
The watchpoints modify no code, so the instrument's only patched bytes are the single arming detour's five.
Rule 19: the control was taken and it passed
One End Turn from ref-turn2.sav with all four watchpoints armed reproduced the determinism oracle
byte for byte — (Autosave EndTurn).sav bb4fd9ac…, (Autosave).sav 978041ac…. The prediction
in W2-predictions.md Part 2 ("the watchpoint is byte-neutral, unlike a detour, because a #DB on
a data write is a trap taken after the store retires and no code is modified") held.
What it answered
ModCount(S+0x8): exactly 12 writes per End Turn on this save, twice, values 13→24 and 25→36 with no gap. Lane A2's prediction confirmed, including both predicted addresses (0x007dc6f0,0x007d92ca), the zero at0x007b9e20, and the wholeOnMessage → ApplyAllTurnCommands → ApplyTurnCommandBatchcall chain.Frame(S+0xc): exactly one write per End Turn, fromBeginProcessTurn + 0x2a, value becoming the turn number. This settles theModCount-vs-Framenaming dispute in lane A2's favour against both lane T andaddresses.json.Player.Status: the writer the S31 regression was missing isStrategyNetworkClient::OnMessage + 0xa15, writing 4 afterProcessTurnreturns and before the autosave. Lane T2's "there is no writer between tail phase 31 and the autosave" is falsified.
Reusing it
Change the four addresses computed in WatchOnApplyAll; anything reachable from S at the flush is
one line. Keep the canary self-test and keep the ref-turn2 oracle control — together they cost
about two minutes and they are what makes the numbers evidence rather than output.