lane E3: the per-system visibility record read from the instruction stream, and the prediction before the build

NVE is a per-(system, player) map of {ETS = turn of sighting, Eid = encounter type
present}. Primary writer 0x00756300, called from tail phase 17 under the gate
(AFlags >> PlyrIdx) & 1 -- byte-decoded at 0x007cf7a7..0x007cf7ce, not from the
decompiler. Three further accessors named, including the intel-sharing copy that
keeps the older sighting stamp. operator[] has exactly three callers and none of
them erases, so the record is a memory, not a state.

Also names ltis's writer (driver phase 29, AFlags != 0 -> ltis = Frame), closing
half of board.md's 'TShn/ltis: NOTHING NAMES THEIR WRITER'. TShn's own gate is
still unnamed and demonstrably NOT AFlags -- Spica is the counterexample.

Resolves lane B5's flagged indirect edge: SetExploredBy's vft[0x1c] is
ServerSystem vtable slot 7 at 0x007480b0, and it writes only an unserialised
runtime mask.

Prediction committed before the engine change exists: 204 -> 158 (46 closed, 32
of them the brief's target) and 103 -> 87 (16 closed), 0 regressed, with a
falsification section naming the corpus save that separates AFlags from VFlags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
This commit is contained in:
alex 2026-09-08 13:26:46 -04:00
parent fbe3c7bd86
commit 55ea86f0d5
2 changed files with 266 additions and 0 deletions

View file

@ -0,0 +1,106 @@
# Prediction: the visibility phase, before it is built
Lane E3, 2026-09-08. Written and committed **before** the engine change exists (rule 2).
Model: `findings/subsystems/system-visibility-record.md`.
Baseline, measured on `sots-engine` main `0592104` this morning:
```
turn1-state -> turn2-state 204 leaf differences
turn2-state -> turn3-state 103 leaf differences
```
## The model, as three rules
1. **T17 `RecordObservation`.** For every player `p` and system `s` with
`s.AFlags & (1 << p.PlyrIdx)`: set `NVE[p] = { EPid = p's handle id, ETS = frame,
Eid = the encounter id at s }`. `frame` is the post-increment frame, i.e. the turn number
the autosave carries. Entries are ordered by `PlyrIdx` ascending (a `std::map`). Nothing is
ever erased.
2. **T21 `SetExploredBy`.** `s.EFlags |= s.AFlags`.
3. **S29 `ltis`.** If `s.AFlags != 0` then `s.ltis = frame`.
`Eid` is taken from an existing `NVE` entry when the system already has one (the field it
mirrors is fixed for the life of the game); otherwise from a fleet at the system with
`FtEnc != 0`; otherwise `-1`.
`AFlags` is **read from the save, not recomputed**. Justification, and it is checkable: neither
reference pair moves an `AFlags` leaf, so the loaded value is the value the phase would see.
## Predicted leaves closed
### `turn1-state -> turn2-state` (the 32-leaf target)
Eight systems — 64 Hyperion, 112 Gamma Cephei, 288 Ke'Dolarra, 304 Koa'Vo, 336 Kaa'Vaalu,
400 Markab, 448 Kea'Pono, 480 Ko'Rorkor — are exactly the systems with `AFlags != 0`.
| leaves | rule | count |
|---|---|---:|
| `Sys[*]/NVE` count `0 -> 1` | 1 | 8 |
| `Sys[*]/EPid` only-in-B | 1 | 8 |
| `Sys[*]/ETS` only-in-B | 1 | 8 |
| `Sys[*]/Eid` only-in-B | 1 | 8 |
| **the brief's target** | | **32** |
| `Sys[*]/EFlags` `0 -> {16,128,16,16,16,16}` on 64, 304, 336, 400, 448, 480 | 2 | 6 |
| `Sys[*]/ltis` `1 -> 2` on all eight | 3 | 8 |
| **total predicted closed** | | **46** |
Predicted `EPid` values, per system, in order: 528, 16, 32, 576, 528, 528, 528, 528.
Predicted `Eid` values: 5, -1, -1, -1, 3, 3, 4, 4. Predicted `ETS`: 2 everywhere.
Expected result: **204 -> 158, closed 46, regressed 0.**
### `turn2-state -> turn3-state`
The entries already exist, so only their stamps move.
| leaves | rule | count |
|---|---|---:|
| `Sys[*]/ETS` `2 -> 3` on the same eight | 1 | 8 |
| `Sys[*]/EFlags` | 2 | 0 (already equal to `AFlags`) |
| `Sys[*]/ltis` `2 -> 3` on the same eight | 3 | 8 |
| **total predicted closed** | | **16** |
Expected result: **103 -> 87, closed 16, regressed 0.**
`EPid` and `Eid` must be **re-emitted unchanged**; if either moves, the model has regressed a
leaf that agreed by construction, and that is a failure of rule 3, not a rounding detail.
## Falsification
Each of these is a way the model is wrong and the symptom that would show it.
* **The gate is `VFlags`, not `AFlags`.** On both reference pairs `VFlags == AFlags` on every
system, so the pairs cannot separate them. The discriminator already in the corpus is
`zuul-turn23-fleet23.sav` Bismol: `VFlags = 2`, `AFlags = 0`, `ETS` frozen at 22 while the
save is turn 23. A `VFlags` model refreshes it to 23. **Symptom:** a host test that replays
Bismol's row fails. That test is written.
* **The gate is `EFlags`.** Same corpus discriminator: Bismol has `EFlags = 2`.
* **`ETS` is the sighting turn, set once, not refreshed.** Then `turn2 -> turn3` closes 0 of
its 8, and the eight `ETS 2 -> 3` leaves stay. **Symptom:** pair 2 closes 8 instead of 16.
* **`Eid` is re-derived from the fleet every turn rather than parked on the system.** Invisible
on this corpus — no encounter fleet dies in it. **Symptom:** none available; that is why §8
of the model doc names the workload instead of claiming the rule.
* **The `Eid` test is `FtFlg & 0x10`, not `FtEnc != 0`.** Invisible on this corpus; the six
encounter fleets agree on both. **Symptom:** none available.
* **`EPid` is the player index, not the handle id.** Then all eight `EPid` leaves regress to
0..7 instead of closing. **Symptom:** 8 closed becomes 8 regressed, and the run's closed
count drops to 38 / 16.
* **The map is ordered by insertion, not by key.** Invisible: every system in the corpus has
exactly one entry. **Symptom:** none available; the ordering is asserted from the container
type, and §8 names the two-empire-contact workload that would exercise it.
* **`ltis` is not the frame but something that merely equals it here.** `ltis` is `1` at
turn 1 and `2` at turn 2 on the same systems, which is consistent with a great many
counters. The reading is a 41-byte function body; if it is wrong the 8 `ltis` leaves regress
rather than close.
* **The phase runs before the frame increment.** Then every stamp is one low and 24 leaves
regress across the two pairs.
## What the model deliberately does not write
* `NVO.TShn` (10 leaves per pair). Its gate is not `AFlags` — Spica has `AFlags == 0` and
`TShn` still moves. Formula not held; **evaluated and reported, not written** (lane S2's
rule).
* `rcex` (6 leaves per pair). Unexplained.
* `NVs` / `PlayerView`. Count 0 on both reference pairs; the `PlayerView` refresh loop is a
separate phase and no leaf of it diverges on either pair.

View file

@ -0,0 +1,160 @@
# The per-system visibility record: `NVE`, `ltis`, and the explored mask
Lane E3, 2026-09-08. Target: the 32 diverging `nve` leaves on the reference pair.
Status of each claim is marked **[V] instruction-verified**, **[D] decompiler-only**, or
**[H] hypothesis** (rule 6).
---
## 1. What the record is
`Sys.NVE` is a `std::map<int playerIdx, {int16 ETS, int32 Eid}>` at `ServerSystem+0x284/+0x288`.
It is **"what this player last saw at this system"**: the turn of the sighting and the
random-encounter type that was there. Its sibling `NVO` is the same idea for *ownership*
(`TShn` = turn, `OID` = owner as last seen), and `NVs`/`PlayerView` is the same idea for the
*colony's numbers*. Three maps, one concept, one key.
Node layout, reconciling `struct-recovery.md` §1.2 with the accessors below: MSVC `_Tree` nodes
put `_Myval` at `+0xc`, so the key (player index) is `node+0xc` and the **8-byte value is
`node+0x10`**:
| value offset | wire name | type | meaning |
|---|---|---|---|
| +0x0 | *(not serialised)* | int16 | the turn the record was last **touched** |
| +0x2 | `ETS` | int16 | the turn the **sighting** was made |
| +0x4 | `Eid` | int32 | the random-encounter type id present, `-1` for none |
The key is written to the wire as `EPid`, the player **handle id** (16, 32, 528 …), not the
index. That reconciles the isnil byte at `+0x19` with an 8-byte value. **[V]** (arithmetic
over the accessors in §2, each of which reads a named offset).
## 2. Every writer and reader of the map
| addr | what it is | body |
|---|---|---|
| `0x00756300` | **`ServerSystem::RecordObservation(p, encId)`** — the primary writer | `f = (int16)this->owner(+0x10)->Frame(+0x8); rec = NVE[p->PlyrIdx(+0x28)]; rec[0] = (f<<16)|f; rec[1] = encId;` **[D]** |
| `0x007536a0` | `ServerSystem::CopyObservationTo(p, srcRec)` — intel sharing | `rec = NVE[p->PlyrIdx]; rec.touched = (int16)Frame; rec.ETS = src.ETS; rec.Eid = src.Eid;` — the *sighting* stamp is copied, only the touch stamp is refreshed **[D]** |
| `0x00754d90` | `ShareObservation(systhis, from, to)` | `a = Find(from); b = Find(to); if (a && (!b || b->ETS < a->ETS)) CopyObservationTo(to, a);` — newer sighting wins **[D]** |
| `0x0074d360` | `ServerSystem::FindObservation(p)` | map find on `p->PlyrIdx`, returns `node+0x10` or null **[D]** |
| `0x0074f830` | `ServerSystem::LastSeenEncounterId(p)` | map find, returns `node+0x14` (`Eid`) or `-1` **[D]** |
| `0x00752730` | the map's `operator[]` | 3 callers only: the two writers above and `ServerSystem::Read` **[V]** (cross-reference query) |
**There is no eraser.** `operator[]` has exactly three callers and none of them removes an
entry, so once a player has an entry at a system it keeps it for the rest of the game. That is
what makes the record a *memory* rather than a *state*.
## 3. Where the primary writer is called from
Tail phase 17, `FUN_007cf560` (the "rebuild every `PlayerView`" phase). Disassembled at
`0x007cf7a7..0x007cf7ce` — **[V]**, byte-decoded, not from the decompiler:
```
mov edx,[esi+0x44] ; systems.begin
mov edi,[edx+ebx*4] ; edi = sys
mov eax,[ebp-0x14] ; player
push eax
mov ecx,edi ; this = sys
call 0x00743fb0 ; ServerSystem::IsVisibleTo(sys, player)
test al,al
jz skip
mov eax,[edi+0x184] ; the encounter type id parked on the system
mov ecx,[ebp-0x14] ; player
push eax ; arg: encId
push ecx ; arg: player
mov ecx,edi ; this = sys
call 0x00756300 ; RecordObservation
```
Outer loop is over **players**, inner over **systems**. The gate is one instruction:
* `FUN_00743fb0(sys, p)` = `(sys->AFlags(+0xd4) >> p->PlyrIdx(+0x28)) & 1` **[V]**.
`AFlags` — not `VFlags`, not `EFlags`. `combat-retreat-pipeline.md` §5 already had `AFlags` as
the *derived, non-sticky* union `FFlags | GFlags | isOwner`, recomputed on every fleet arrival
and departure. So the record is refreshed exactly while the player still has something at the
system, and freezes the moment the last fleet leaves and the player does not own it.
## 4. `Eid`'s source
`sys->+0x184` is a `StarSystem` member initialised to `-1` in the constructor
(`FUN_00752ea0`, `param_1[0x61] = 0xffffffff`) **[D]**, and set by
`FUN_007887c0(sys, encId)` — the encounter-placement routine, which refuses unless the system
is unowned, planetless and still `-1` **[D]**. **It is not on the wire.** It is fixed for the
life of the game, so the only observable it can be recovered from is the encounter fleet
sitting at the system, whose `FtEnc` carries the same id.
Across all 11 saves, `Eid == FtEnc` of the fleet at the system, for every one of the six
encounter fleets in the corpus, and `-1` everywhere else. The `Defenses` fleet at Koa'Vo has
`FtEnc == 0` and `FtFlg == 0x400`, and its system's `Eid` is `-1`; every real encounter fleet
has `FtEnc > 0` and `FtFlg & 0x10`. **Nothing in the corpus separates "`FtEnc > 0`" from
"`FtFlg & 0x10`" as the test, and nothing separates either from the true rule, which reads a
field that is not saved at all.** Labelled **[H]**; §8 names the workload.
## 5. `ltis` — a second writer, in the other driver
Driver phase 29, `FUN_00743ec0` (41 B, whole body) **[D]**:
```c
if (this->AFlags(+0xd4) != 0)
this->ltis(+0x2c8) = this->owner(+0x10)->Frame(+0x8);
```
`ltis` is the system-level twin of `ETS`: same clock, same gate, but the gate is
`AFlags != 0` for **any** player rather than for a particular one, and the value is a full
int rather than an int16. `board.md` records `TShn`/`ltis` as *"NOTHING NAMES THEIR WRITER"* —
this names `ltis`'s. `TShn` is still unnamed and is **not** the same rule (§7).
## 6. The explored mask, and the one thing `OnExploredChanged` does
Tail phase 21, `FUN_007a3c60`, per system × per player **[D]**:
```c
if (IsVisibleTo(sys, p) && !IsExploredBy(sys, p)) {
SetExploredBy(sys, p, true); // 0x007438b0: EFlags |= 1 << PlyrIdx
NodeGraphRegister(...); // 0x006e4620
PostEvent("EVENT_FLEET_EXPLORED", ...);
}
```
so **`EFlags |= AFlags`**, once per turn, in the tail.
`SetExploredBy`'s trailing `vft[0x1c](p, wasSet, on)` — the indirect edge `lane-b5.json` flagged
as unresolved — resolves through `Game::ServerSystem`'s primary vftable `0x00a2044c` slot 7 to
**`0x007480b0`** (`tools/vtable_map.py`). Its whole body **[D]**:
```c
if (!wasSet && on && server->Frame(+0x8) > 1) {
FUN_00747a20(p->PlyrIdx, 1);
if (p->PlyrIdx < 15) this->+0x2a4 |= 1 << p->PlyrIdx;
}
```
`+0x2a4` is past `NVs` and is not in the serialised table: a runtime "newly explored this turn"
mask, not save state. **The edge is resolved and it writes nothing that reaches the wire.**
Note the `Frame > 1` guard — turn 1 is special-cased throughout this subsystem.
## 7. What this does NOT explain
`NVO.TShn` moves on **more** systems than `NVE.ETS` does. On `turn1 -> turn2`, Spica has
`AFlags == 0`, no `NVE` entry and no `ltis` move — and its `TShn` still goes 1 -> 2. So `NVO`
has a looser gate than `AFlags`, and it is **not** `VFlags`, `EFlags` or `FFlags` either (all
zero at Spica). `TShn`'s writer remains unnamed. Conversely `zuul-turn23-fleet23.sav`'s Bismol
has `AFlags == 0` and freezes `TShn`, `ETS` **and** `ltis` at 22 together — so on that save the
two rules agree. Whatever `NVO`'s gate is, it admits Spica and excludes Bismol.
`rcex` (int64) moves `0 -> 65536` on the same six systems on `turn1 -> turn2` and back
`65536 -> 0` on `turn2 -> turn3`. `65536 = 1 << 16`, and Koa'Vo takes `1 << 28`. It is a
per-system word that toggles with the encounter/visibility cluster and it is **not** modelled
here.
## 8. Workloads that would settle the open items
| open item | workload |
|---|---|
| whether `Eid` is `sys->+0x184` (fixed at generation) or re-read from the fleet each turn | a save where an encounter fleet is **destroyed** at a system that stays visible: `+0x184` keeps the id, a fleet-derived rule drops to `-1` |
| whether the `Eid` source test is `FtEnc > 0` or `FtFlg & 0x10` | any save with a fleet carrying `FtEnc > 0` and `FtFlg & 0x10` clear, or the converse |
| `NVO.TShn`'s gate | a watchpoint on `ServerSystem+0x274`'s map during one turn — rule 18; Spica vs Bismol is already the discriminating pair |
| more than one player observing one system | every save in the corpus has **single-bit** `AFlags`. A two-empire contact save would exercise the map ordering (ascending `PlyrIdx`) and the `EPid` handle lookup for a second entry |
| `ShareObservation` (alliance intel) | no save has two players in an alliance; the newer-sighting-wins rule has never executed |
| `rcex` | unassigned |