TShn is refreshed to Frame for every (system, player) that satisfies ServerSystem::IsKnownTo 0x00746390 -- IsVisibleTo OR (2-bit sensor contact at ServerSystem+0x24 AND CCC_AdvSens). Not AFlags alone, which is why lane E3 could not fit it. The writer chain was confirmed live, frame by frame, from a hardware watchpoint on Spica's NVO node; the refresh runs twice per End Turn (driver phase 24 and combat-done phase 25). Also: the trade and spy containers read out of a live game for the first time (both empty, zero traps -- the workload confirmation two lanes lacked); Player.Status's predicate named as ReqCL != 0 && Elim == 0, with Species != 4 shown to be a corpus coincidence; and all ten command ModCount handlers named, two of them only reachable on a turn-1 workload. Corrects objects/layouts.md's ServerSystem PID@0x274 row and closes system-visibility-record.md 7. Runs: build w3tshn-bb81d3d-20260908T2007Z, configs shim.cfg.w3tshn / w3mod / w3control. Rule 19 control passed -- the armed run reproduced the determinism oracle byte for byte.
171 lines
9.3 KiB
Markdown
171 lines
9.3 KiB
Markdown
# 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
|
||
|
||
> **CLOSED by lane W3, 2026-09-08 — see `nvo-tshn-visible-owner.md`.** `NVO.TShn`'s writer is
|
||
> `ServerSystem::SetVisibleOwner 0x0075b880` (trapped live on Spica), reached from the sensor phase
|
||
> via `RefreshVisibleOwnerIfKnown 0x0075bd70`. The looser gate this section correctly predicted is
|
||
> `ServerSystem::IsKnownTo 0x00746390` = `IsVisibleTo` **or** (2-bit sensor contact at
|
||
> `ServerSystem+0x24`, which is *not serialised*, **and** the player has researched `CCC_AdvSens`).
|
||
> Spica's entry belongs to a Species-4 NPC pseudo-player and every one of those starts the game with
|
||
> `CCC_AdvSens`; Bismol's belongs to a real empire and none of those has it in any corpus save.
|
||
> The resulting model predicts all 158 `NVO` records across the 11 saves, including Bismol.
|
||
> The section below stands as written — it is what made the case checkable.
|
||
|
||
|
||
`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 |
|