sots-engine/docs/W3-watchpoints.md
alex 9e914ef358 W3: watch.mode=tshn -- arm the NVO/TShn record and the trade+spy containers from the same S
Second mode in lane W2's watchpoint module. No second hook: only different arithmetic on the S the
ApplyAllTurnCommands detour already holds.

- picks the target system by predicate at arm time (AFlags == 0, NVO non-empty) and logs all 28
  systems, so the choice is auditable rather than a hard-coded pointer;
- probes both candidate ServerSystem bases and logs how many systems validate under each, which
  settled a documentation dispute (+0x274: 9, +0x26c: 0) by measurement;
- prints the trade-route and spy-program vector triples, which is the workload-confirmation
  instrument two earlier lanes lacked.

Rule 19 control passed: the armed run reproduced the determinism oracle byte for byte.
2026-09-08 16:38:55 -04:00

70 lines
4.2 KiB
Markdown

# W3 — a second watch mode, and what the two runs answered
Companion to `docs/W3-predictions.md` (written and committed before the build) and to
`sots-re/findings/subsystems/nvo-tshn-visible-owner.md` +
`sots-re/findings/control-flow/w3-containers-status-handlers.md` (the full reports).
Builds on lane W2's module without changing how it works.
## What was added
`watch.mode=modcount|tshn` in `src/shim/hooks/watchpoints.{h,cpp}`. **No second hook, no second
detour, no change to the VEH or to the arming mechanism** — only different arithmetic on the `S`
the existing `ApplyAllTurnCommands` detour already holds. `modcount` is W2's set and stays the
default so its run remains reproducible; `tshn` arms:
| slot | target |
|---|---|
| 0 | the `TShn` word of an `NVO` map record on a system with `AFlags == 0` |
| 1 | that map's `_Mysize` |
| 2 | trade-route vector `_Mylast` (`*(S+4+0x154) + 0x40`) |
| 3 | spy-program vector `_Mylast` (`*(S+4+0x158) + 0x14`) |
Two things about the `tshn` arming are worth keeping if the module is extended again:
- **The target is chosen by predicate at arm time, not hard-coded**, and all 28 systems are logged
with name, `AFlags` and both map sizes. A hard-coded pointer that lands on the wrong object
produces the same empty hit list as "nothing writes this" (method rule 1).
- **The `ServerSystem` base is probed, not assumed.** Two published accounts disagreed by 8 about
where the `NVO` map lives, because `ServerSystem::Write` runs on the IStreamable subobject at
`+0x8`. The code tries both candidate bases across every system and logs how many validated under
each (`+0x274`: 9 systems; `+0x26c`: 0). That turned a documentation dispute into a measurement.
Configs: `shim.cfg.w3tshn`, `shim.cfg.w3mod`, `shim.cfg.w3control`. `w3tshn` and `w3control` differ
in exactly one key (`watch=`), which is what makes the rule-19 control real.
## Rule 19: the control was taken again, and it passed again
One End Turn from `ref-turn2.sav` with the four `tshn` watchpoints armed reproduced the determinism
oracle byte for byte — `(Autosave EndTurn).sav` `bb4fd9ac…`, `(Autosave).sav` `978041ac…`, both
identical to the pre-run files. So the four *new* addresses are as neutral as W2's four were. That
mattered: W2 proved a data breakpoint is neutral for one address set, not for all of them, and this
lane's set includes a heap node that the game reallocates freely.
The second run (turn-1 workload, `watch.mode=modcount`) has **no oracle of its own** and says so.
## What the two runs answered
1. **`NVO.TShn`'s writer, trapped live**, with its whole call chain confirmed frame by frame from
the recorded `ebp` chain: `RefreshVisibleOwnerIfKnown 0x0075bd70` → `RecordVisibleOwner
0x0075bca0` → `SetVisibleOwner 0x0075b880` → `NVO::operator[] 0x0075a890` → the store
`0x0075b961`. The gate is `ServerSystem::IsKnownTo 0x00746390`, which is `IsVisibleTo` **or**
(sensor contact ∧ `CCC_AdvSens`) — not `AFlags` alone, which is why lane E3 could not fit it.
The resulting model predicts all 158 `NVO` records in the 11-save corpus with zero mismatches,
including the single frozen one.
2. **Four traps, not one, and the extra pair is a result**: the refresh runs twice per End Turn, in
driver phase 24 and again in combat-done phase 25. Both phase attributions confirmed for free.
3. **The trade and spy containers, read out of a live game for the first time.** Both managers
non-null, both vectors default-constructed with all three pointers zero, zero traps on either
`_Mylast` across the turn. The workload is *absent*, measured — which is the confirmation
instrument two earlier lanes needed and did not have.
4. **All ten command `ModCount` handlers are now named**, including two that only exist on a turn-1
workload (`OnCommand_SetResearchProject`, inlined at `0x0088fe0a`, and
`OnCommand_CreateDesign 0x00882910`).
## Reusing it
`watch.mode` is the extension point. To watch something else, add a mode and one arming function;
everything else — the canary, the VEH, the flusher, the hit format — is unchanged. 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.