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.
9.3 KiB
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) |
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 |
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]:
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]:
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]:
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 isServerSystem::SetVisibleOwner 0x0075b880(trapped live on Spica), reached from the sensor phase viaRefreshVisibleOwnerIfKnown 0x0075bd70. The looser gate this section correctly predicted isServerSystem::IsKnownTo 0x00746390=IsVisibleToor (2-bit sensor contact atServerSystem+0x24, which is not serialised, and the player has researchedCCC_AdvSens). Spica's entry belongs to a Species-4 NPC pseudo-player and every one of those starts the game withCCC_AdvSens; Bismol's belongs to a real empire and none of those has it in any corpus save. The resulting model predicts all 158NVOrecords 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 |