Z: the unnamed counter at S+0x8 is ModCount, and my players flag was my own error
StrategyServer::Write tags both words itself: S+0x8 is ModCount and S+0xc is Frame. addresses.json has the name on the wrong word and lane T's PhaseCounter is the one the wire calls ModCount. The saves confirm it independently -- ModCount 0/12/24 across turn1/2/3-state, 241/412 across Zuul 16/23 -- and those deltas are exactly the 12-44 per turn measured live. So the 'writer nobody has identified' question dissolves: it is a modification counter, it scales with the empire, and there is no single writer to find. The players=8 flag is withdrawn. The offset is right, pinned by the ctor's four-vector enumeration at 0x0085b120 with no frame arithmetic needed, and the count is right: the vector is empires + one rebel-AI per empire species + four NPC pseudo-players, so 8 on the Human saves and 7 on the Zuul ones against a lobby that says 2 in both. My draft claimed the hook read 8 on both saves. It read 7 on the Zuul one. I generalised from one run without re-reading the other, and a check aimed at something else caught it.
This commit is contained in:
parent
888f318d8c
commit
9c37635ca3
3 changed files with 102 additions and 25 deletions
|
|
@ -623,14 +623,17 @@ same word. Given §6's inference that the message is delivered every turn, `S+0x
|
|||
per turn while `S+0xc` (`ModCount`) advances once. They are not in lockstep, and a reimplementation must not
|
||||
treat `S+0x8` as a turn number. It reads as a **server-phase / driver-invocation counter**.
|
||||
|
||||
> **MEASURED by lane Z, 2026-09-08 — "at least twice" is the right phrasing, and the real number is 12 to
|
||||
> 44.** Read live at hook entry over eight turns on two saves, `S+0x8` advances **12–14 times per turn** on
|
||||
> an early two-colony game and **16–44** on a turn-19 Zuul one, of which the two drivers account for 2.
|
||||
> Both drivers were hooked, so the rest come from a writer nobody has identified, between the post-turn
|
||||
> autosave and the next `ProcessTurn`. `S+0xc` advances by exactly 1 per turn over the same records. The
|
||||
> conclusion here is strengthened: `S+0x8` is not a turn number, it is not a driver-invocation counter, and
|
||||
> it is not even a constant per turn — it scales with the size of the game state.
|
||||
> `findings/control-flow/tail-rng-ledger.md` §5.
|
||||
> **NAMED and MEASURED by lane Z, 2026-09-08 — the word is `ModCount`, and it moves 12 to 44 times a
|
||||
> turn.** `StrategyServer::Write` tags it itself: `0x0079fb2f lea edx,[edi+0x08]; push "ModCount"` and
|
||||
> `0x0079fb40 lea eax,[edi+0x0c]; push "Frame"`, with `edi = S`. So **`S+0x8` is `ModCount`** — the word
|
||||
> this section says "has never been named" — **and `S+0xc` is `Frame`**, the turn. (`addresses.json` has
|
||||
> `StrategyServer_off_ModCount` on the wrong word; the name belongs to lane T's
|
||||
> `StrategyServer_off_PhaseCounter`.) Measured live over eight turns on two saves, `S+0x8` advances **12–14
|
||||
> times per turn** on an early two-colony game and **16–44** on a turn-19 Zuul one, of which the two drivers
|
||||
> account for 2 — and the saves agree independently (`ModCount` 0 → 12 → 24 across turn1/2/3-state, 241 → 412
|
||||
> across Zuul turns 16 → 23). A modification counter is exactly what that looks like, so "which writer bumps
|
||||
> the rest" has no single answer. `S+0x8` is not a turn number, not a driver-invocation counter and not a
|
||||
> constant per turn. `findings/control-flow/tail-rng-ledger.md` §5.
|
||||
|
||||
### 7.2 Turn results and turn events are not where lane T said
|
||||
|
||||
|
|
|
|||
|
|
@ -189,10 +189,32 @@ Two things fall out of the same records and are worth more than the phrasing:
|
|||
|
||||
---
|
||||
|
||||
## 5. Correction to `combat-done-tail.md` §7.1 — and to this lane's own prediction
|
||||
## 5. `S+0x8` has a name, and it is `ModCount` — correcting `combat-done-tail.md` §7.1, lane T §0.1, `addresses.json`, and this lane's own prediction
|
||||
|
||||
Lane K wrote that `S+0x8` "advances **at least twice** per turn". That is correct and this lane's prediction
|
||||
that it advances **exactly** twice is **wrong**. Observed `S+0x8` at hook entry:
|
||||
Lane K wrote that `S+0x8` "advances **at least twice** per turn" and that "the word at `S+0x8` has never been
|
||||
named" (lane T §0.1). The first is right and this lane's prediction that it advances **exactly** twice is
|
||||
**wrong**. The second is now answered — by `StrategyServer::Write`'s own wire tags:
|
||||
|
||||
```
|
||||
0079fb2f lea edx,[edi+0x08] ; push "ModCount" ; edi = S -- the same edi that indexes
|
||||
0079fb40 lea eax,[edi+0x0c] ; push "Frame" ; the players vector at +0x54
|
||||
```
|
||||
|
||||
So **`S+0x8` is `ModCount` and `S+0xc` is `Frame`**, the turn number. `ghidra/addresses.json` has the name on
|
||||
the wrong word: its `StrategyServer_off_ModCount = 0x8` is the stored-frame offset of `S+0xc`, which the wire
|
||||
calls `Frame` — `turn-spine.md` was right to call it that and lane T flagged the clash without being able to
|
||||
settle it. The name `ModCount` belongs to the word lane T recorded as `StrategyServer_off_PhaseCounter = 0x4`.
|
||||
|
||||
Confirmed three ways, and the third is the satisfying one. **From the saves:**
|
||||
|
||||
| save | `Frame` | `ModCount` |
|
||||
|---|---|---|
|
||||
| turn1-state / turn2-state / turn3-state | 1 / 2 / 3 | 0 / 12 / 24 |
|
||||
| z-t6-endturn / z-t6-autosave (this lane's bracket) | 5 / 6 | 50 / 62 |
|
||||
| zuul-turn16-noderoute / zuul-turn23-fleet23 | 16 / 23 | 241 / 412 |
|
||||
|
||||
`Frame` is the turn; `ModCount` moves **+12 per turn** on the early Human game and averages **+24** on the
|
||||
Zuul one. **From the live trace**, `S+0x8` at hook entry:
|
||||
|
||||
| save | turn | `ProcessTurn` entry | tail entry | tail's callees | increments to the next turn |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -205,13 +227,20 @@ that it advances **exactly** twice is **wrong**. Observed `S+0x8` at hook entry:
|
|||
| zuul-noderoute | 19 | 290 | 291 | 292 | **44** |
|
||||
| zuul-noderoute | 20 | 334 | 335 | 336 | — |
|
||||
|
||||
The two drivers account for **2 of 12 to 44** increments per turn, and the count is not a constant — it
|
||||
grows with the size of the game state (12–14 on a turn-3 two-colony empire, 16–44 on a turn-19 Zuul one).
|
||||
The rest happen between the post-turn autosave and the next `ProcessTurn`, from a writer this lane did not
|
||||
identify (both drivers are hooked, so it is neither of them). Lane K's operational conclusion is
|
||||
strengthened, not weakened: **`S+0x8` must never be treated as a turn number** — and it is not a
|
||||
driver-invocation counter either. `S+0xc` (`ModCount`) reads 3, 4, 5, 6 and 17, 18, 19, 20 over the same
|
||||
records and is the turn counter.
|
||||
The live deltas (12, 14, 12 on the Human game; 16, 21, 44 on the Zuul one) sit exactly where the saves'
|
||||
`ModCount` deltas say they should. The two turn drivers account for **2 of 12 to 44** increments; the rest
|
||||
are spread across the turn and mostly fall between the post-turn autosave and the next `ProcessTurn`.
|
||||
|
||||
That is no longer a mystery to be chased — **it is what a modification counter is for**. `S+0x8` is not a
|
||||
turn number, not a driver-invocation counter and not a constant per turn: it counts state mutations, so it
|
||||
scales with the size of the empire, and asking "which writer is responsible" has no single answer. Lane K's
|
||||
operational conclusion stands and is now explained rather than merely observed. `S+0xc` (`Frame`) reads 3, 4,
|
||||
5, 6 and 17, 18, 19, 20 over the same records and is the turn counter.
|
||||
|
||||
**For the integrator:** this is a name collision to reconcile, not a new entry. `StrategyServer_off_ModCount`
|
||||
(0x8, stored frame) and `StrategyServer_off_PhaseCounter` (0x4, stored frame) are the two words above with
|
||||
their names swapped; `ghidra/addresses.d/lane-z.json` records the evidence under
|
||||
`StrategyServer_wire_ModCount_vs_Frame` rather than adding a third name for either word.
|
||||
|
||||
## 6. Corrections to `combat-done-tail.md` §3 and §6.1
|
||||
|
||||
|
|
@ -298,11 +327,24 @@ would have said so by construction rather than by anyone noticing.
|
|||
* **A turn with a genuinely empty encounter vector was not observed** (§4). Both saves produce exactly one
|
||||
sighting encounter on every turn. The tail-runs-every-turn claim is settled; the no-encounters variant is
|
||||
still an inference, now a much narrower one.
|
||||
* **`players` reads 8 on a 2-player save.** `(S+0x54)` enumerated as a `ServerPlayer*` vector gives 8 on
|
||||
every record of a game the lobby shows as 2 players. Either the server always allocates a fixed slot
|
||||
count, or this offset enumerates something else (spare capacity is the failure this campaign has already
|
||||
paid for once, at `S+0x64`). **Nothing in the ledger depends on it** — it is decoration on the argument
|
||||
record — but it should not be reused until someone resolves it.
|
||||
* ~~`players` reads 8 on a 2-player save, and may be the `S+0x64` bug again.~~ **Resolved, and the flag was
|
||||
my own error.** The offset is right and so is the count. `StrategyServer`'s base-class ctor 0x0085b120
|
||||
(entered with `ecx = S+4`) zero-initialises four consecutive vectors as three-word triples with the fourth
|
||||
word skipped — `+0x40/+0x50/+0x60/+0x70` raw, 0x10 apart, allocator-last — which enumerates the players
|
||||
triple as literally `{S+0x54, S+0x58, S+0x5c}` with no frame arithmetic at all, and puts the fleets vector
|
||||
at `S+0x64` exactly where B4 measured it. Five NPC accessors at 0x00788de0ff bounds-check an index against
|
||||
`([S+0x58] − [S+0x54]) >> 2` and then index `_Myfirst`, which is a third confirmation.
|
||||
|
||||
**The vector is not the lobby's player list.** It is `#empires + one rebel-AI per distinct empire species
|
||||
+ 4 NPC pseudo-players` (Alien Menace, Peacekeeper Enforcer, Von Neumann, Independent Colony — all species
|
||||
4). `Sim.NumPlrs` reads **8** in the Human saves (two species) and **7** in every Zuul save (one species),
|
||||
against a `Summary.Players` array with **2** entries in both. Both numbers are right; they count different
|
||||
things.
|
||||
|
||||
**And my draft of this document was wrong about my own data.** It said the hook read 8 "on both saves". It
|
||||
did not: the trace reads 8 on `ref-turn2` and **7** on `zuul-turn16-noderoute`, matching each save's
|
||||
`NumPlrs` exactly. I generalised from one run without re-reading the other, and it took a check aimed at
|
||||
something else to catch it.
|
||||
* **Which of the 12-to-44 `S+0x8` increments per turn come from where** (§5), and what they scale with.
|
||||
* The direct-call sweeps behind "node-line decay's only RNG site is the `Chance(0.5f)`", "its downstream
|
||||
pair draws nothing" and "`ProcessNodeSpaceTravel` draws nothing" are **worth less than they look**, and
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"name": "StrategyServer_NodeLineDecay_FleetSkipIsPostDraw",
|
||||
"addr": "0x007ae0b2",
|
||||
"convention": "site",
|
||||
"prototype": "site, and a CORRECTION to findings/control-flow/combat-done-tail.md §3, which says \"the roll is skipped for a line if any fleet with flag 0x20000 is targeting it\". IT IS NOT: the fleet scan begins HERE, at 0x007ae0b2, which is 0x1d bytes AFTER the Chance(0.5f) call at 0x007ae0a5 and is reached only when the roll SUCCEEDED (`test al,al; je 0x007ae1e2` at 0x007ae0aa). The scan therefore cannot change the draw count -- it suppresses only the collapse (the 0x007a92e0 / 0x007a4700 pair), never the draw. The straight-line order in loop 1 is: expiry test -> DRAW -> roll gate -> fleet gate -> collect. Lane K's headline claim, one NextFloat per expired node line per turn, survives intact and is now pinned to a concrete expiry formula (NodePath_RemainingLife)",
|
||||
"prototype": "site, and a CORRECTION to findings/control-flow/combat-done-tail.md \u00a73, which says \"the roll is skipped for a line if any fleet with flag 0x20000 is targeting it\". IT IS NOT: the fleet scan begins HERE, at 0x007ae0b2, which is 0x1d bytes AFTER the Chance(0.5f) call at 0x007ae0a5 and is reached only when the roll SUCCEEDED (`test al,al; je 0x007ae1e2` at 0x007ae0aa). The scan therefore cannot change the draw count -- it suppresses only the collapse (the 0x007a92e0 / 0x007a4700 pair), never the draw. The straight-line order in loop 1 is: expiry test -> DRAW -> roll gate -> fleet gate -> collect. Lane K's headline claim, one NextFloat per expired node line per turn, survives intact and is now pinned to a concrete expiry formula (NodePath_RemainingLife)",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
||||
},
|
||||
|
|
@ -79,6 +79,38 @@
|
|||
"prototype": "int (StarFleet* fleet, int npid) // 234 B. Returns 3 exactly when the fleet's FRONT waypoint (the deque at fleet+0xc4, element +0x10) names this npid -- i.e. the fleet is currently riding this line; 0/1/2/4 otherwise. Node-line decay drops a rolled line when any 0x20000-flagged fleet returns 3 for it",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_wire_ModCount_vs_Frame",
|
||||
"addr": "0x0079fb2f",
|
||||
"convention": "note",
|
||||
"prototype": "NAME CORRECTION, from StrategyServer::Write's own wire tags. At 0x0079fb2f `lea edx,[edi+0x08]; push \"ModCount\"` and at 0x0079fb40 `lea eax,[edi+0x0c]; push \"Frame\"`, with edi = S (the same edi that indexes the players vector at +0x54). So in the S frame **S+0x8 is ModCount and S+0xc is Frame**, i.e. in the stored (S+4) frame +0x4 is ModCount and +0x8 is Frame. `StrategyServer_off_ModCount = 0x8` therefore carries the WRONG NAME: that word is Frame, the turn number. The word it names is the one lane T recorded as StrategyServer_off_PhaseCounter = 0x4 and lane K called 'never named' -- it has a name, and it is ModCount. CONFIRMED FROM THE SAVES, which is an independent instrument: Frame reads 1/2/3 on turn1/2/3-state, 16 on zuul-turn16, 23 on zuul-turn23, while ModCount reads 0/12/24/241/412. And CONFIRMED LIVE: lane Z measured S+0x8 advancing 12, 14, 12 per turn on the early Human game (the saves say +12/turn) and 16, 21, 44 on the Zuul one (the saves say ~24/turn average). A modification counter is exactly what those numbers look like, and it explains why only 2 of the 12-44 increments come from the two turn drivers. Integrator: reconcile StrategyServer_off_ModCount / StrategyServer_off_PhaseCounter rather than adding a third name",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08; wire tags from the instruction stream, values from save_reader over five saves, deltas from the live trace)"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_ctor_VectorBlock",
|
||||
"addr": "0x0085b120",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (void* rawBase /* = S+4 */) // the StrategyServer base-class ctor, called from StrategyServer::StrategyServer 0x007d78d0 at 0x007d7905 as `lea ecx,[esi+0x4]`. It zero-initialises FOUR CONSECUTIVE std::vectors as three-word triples with the fourth word skipped: raw +0x40/+0x44/+0x48, +0x50/+0x54/+0x58, +0x60/+0x64/+0x68, +0x70/+0x74/+0x78, then `lea ecx,[esi+0x80]` for the entity hash. That is the campaign's `{_Myfirst,_Mylast,_Myend,_Alval}` = 0x10 allocator-last shape (method rule 5) enumerated four times in a row, and it independently pins StrategyServer_off_Players = 0x50 and _off_Fleets = 0x60 in the raw frame WITHOUT any frame arithmetic -- the ctor is entered with ecx = S+4, so the players triple is literally {S+0x54, S+0x58, S+0x5c}. This is the enumeration that closes the 0x60-vs-0x64 question the campaign paid for once",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_NPCPlayerAccessors",
|
||||
"addr": "0x00788de0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "ServerPlayer* (StrategyServer* this /*S frame*/) // five sibling accessors at 0x00788de0, 0x00788e10, 0x00788e40, 0x00788e70, 0x00788ea0, one per NPC pseudo-player index word at S+0x1b8/0x1bc/0x1c0/0x1c4/0x1c8 (the five words the ctor sets to -1 at 0x007d79fe..0x007d7a16, and the save's NPCm/NPCo/NPCi/NPCv/NPCa). Each is `idx = this->+0x1b8; if (idx < 0) return 0; first = [this+0x54]; last = [this+0x58]; if (idx >= (last-first)>>2) return 0; return first[idx];` -- a bounds check against the players vector's size followed by an index off _Myfirst, which is a third independent confirmation that S+0x54/S+0x58 are _Myfirst/_Mylast. THE PLAYER VECTOR IS NOT THE LOBBY'S PLAYER LIST: it is #empires + one rebel-AI per distinct empire species + 4 NPC pseudo-players (Alien Menace, Peacekeeper Enforcer, Von Neumann, Independent Colony, all Species 4). Hence NumPlrs 8 on the Human saves (two species) and 7 on the Zuul ones (one species), against a lobby that says '2 Players' in both -- Summary.Players counts EMPIRE SLOTS and is also right",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08; counts cross-checked against Sim.PlayerIDs in seven saves and against the live trace)"
|
||||
},
|
||||
{
|
||||
"name": "sizeof_Game_ServerPlayer",
|
||||
"offset": "0x000003e0",
|
||||
"convention": "offset",
|
||||
"prototype": "sizeof(Game::ServerPlayer) = 0x3e0, from the two `push 0x3e0` + operator new sites that precede the ctor call: 0x007865b3 (the bare factory reached through the class-registry word at 0x00a26078) and 0x0078a2f5 (the save loader, which also sets +0x8 = S+4 and inserts into the entity hash at S+0x84). The ctor itself is 0x008803d0 -- NOTE that findings/control-flow/turn-driver.md \u00a73 cites 0x00880474 as 'the ServerPlayer constructor', which is an address INSIDE it; the instruction there is `mov WORD [esi+0x3b4],0x100`, a 16-bit store, so it sets ResErrRoll(+0x3b4) = 0 and cta(+0x3b5) = 1, not '+0x3b4 = 1' as that note reads",
|
||||
"status": "verified",
|
||||
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue