# 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 learn `S` on 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 its `this` is the `S` frame. - DR0-DR3 as 4-byte data-write breakpoints, set with `GetThreadContext`/`SetThreadContext` on the calling thread and **read back and logged**, plus a vectored exception handler that records EIP, the written value, `DR6`, the thread id, the `EBP` chain and the first four code-looking words above `ESP`. - 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_DETACH` is not guaranteed. - Config: `watch=on|off`, `watch.players=0..2`, `watch.out=`. `shim.cfg.w2watch` and `shim.cfg.w2control` differ **only** in `watch=`, 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 1. `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 at `0x007b9e20`, and the whole `OnMessage → ApplyAllTurnCommands → ApplyTurnCommandBatch` call chain. 2. `Frame` (`S+0xc`): **exactly one** write per End Turn, from `BeginProcessTurn + 0x2a`, value becoming the turn number. This settles the `ModCount`-vs-`Frame` naming dispute in lane A2's favour against both lane T and `addresses.json`. 3. `Player.Status`: the writer the S31 regression was missing is `StrategyNetworkClient::OnMessage + 0xa15`, writing 4 after `ProcessTurn` returns 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.