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
160 lines
8.5 KiB
Markdown
160 lines
8.5 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
|
||
|
||
`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 |
|