A: the AIAgent CD blocks - derivation, addresses, and what stays a hypothesis

Game::StrategyAIAgent::Streamable typed end to end. The writer is entirely
unconditional; the branch the decompiler shows is an inlined vector destructor
whose operator delete is marked noreturn. dsh is a std::map's _Mysize, so it is
the (pid, trns) count and not a scalar; lnat and lat are scalars, not elements.
AISystem::Write never reads its object, so every AISys body on disk is empty
regardless of game state. StreamableEnum<T> is a frame holding one int, which
makes SysMem/mts/nalat arrays of frames rather than of ints.

Named coverage 98.0% -> 99.9% on all four saves; state_checksum.py unchanged
and still PROVED. Section 7 lists the ten element layouts no save exercises and
says what workload would settle each.

29 addresses in the per-lane fragment; merges to 669 with no duplicate name.
This commit is contained in:
alex 2026-09-08 07:33:33 -04:00
parent e1bdbe6edb
commit 805409ec11
2 changed files with 496 additions and 0 deletions

View file

@ -0,0 +1,260 @@
# Typing the `AIAgent` custom-data blocks
Lane A, 2026-09-08. Host/static only — VM140 was held by lane O and the game was never run.
Picks up `findings/objects/wire-schema-closeout.md` §4b, the job lane W left deliberately.
## Headline: named coverage 98.0 % → 99.9 %
The honest metric is `CoverageArchive`'s split of items a field **names** from items a generic
`Node` merely **carries**. Byte-identical round trip held throughout, on all four saves.
| save | before | after | opaque before → after | still opaque after |
|---|---|---|---|---|
| turn1-state | 98.0 % | **99.9 %** | 748 → 37 | `<rest:CD>` 35, `RNG` 2 |
| turn2-state | 98.0 % | **99.9 %** | 748 → 37 | `<rest:CD>` 35, `RNG` 2 |
| turn3-state | 98.0 % | **99.9 %** | 748 → 37 | `<rest:CD>` 35, `RNG` 2 |
| zuul-turn5-species5 | 98.4 % | **99.9 %** | 521 → 37 | `<rest:CD>` 35, `RNG` 2 |
Conformance: **86 shapes bound, 838 items matched, 0 MISMATCH** (was 74 / 769 / 0). Twelve new
bindings, every one of them a full match with **zero wire-only and zero shape-only items** — the
recovered sequence and the shape agree item for item, which is unusual and is the point of §2.
`ctest` **35/35**. `tools/clean_room_check.sh` **OK** (run as a separate command). `test_save`
still skips cleanly with `SOTS_SAVES_DIR` unset. `verify/state-checksum/state_checksum.py --strict`
still reports **`coverage: PROVED`** on all four saves with `0 error, 0 warn` — unchanged, because
nothing on the Python side was touched (§6).
The remaining `CD` items are the one `TurnCommands_v5` body per save, which is lane W's proven
blocker and still needs a save with issued orders. `RNG` is correctly opaque. `Attrib` closed as a
side effect (§3).
## 1. The block is one unconditional sequence — there is no branch to read
`Game::StrategyAIAgent::Streamable::Write` is at **`0x006c6f00`**. The decompiler presents it with a
large `if` wrapped around everything from `lnat` onwards, which looks exactly like the conditional
the campaign method warns about. It is not one. The `if` is an **inlined `std::vector` destructor**
for the temporary vector `nalat` is written from, and this database has `operator delete` marked
noreturn, so Ghidra could not join the two paths and hoisted the continuation into the null branch.
The disassembly settles it:
```
0x006c72ce cmp [ebp-0x50],ebx ; _Myfirst == 0 ?
0x006c72d1 je 0x006c72e8 ; skip the delete
0x006c72d3 ... _Orphan_all / operator delete
0x006c72e8 ... push "lnat"; call WriteInt
```
Both paths converge at `0x006c72e8`. **Every one of the 36 items is written unconditionally.** That
is why this block, unlike `TurnCommands_v5`, aligns perfectly: the recovery's "all branches" view
and the actual single-record view are the same view. All eleven `AIAgent` blocks across the four
saves have byte-for-byte the same top-level item order.
This is the opposite outcome to lane W's §4a and for a concrete reason, not luck.
## 2. What each item is, and how the count/element structure was settled
The recovery gives tags and disk primitives; it does not give the **count/element** structure,
because a linear pass flattens loops. Each of these came from reading the loop in `Write`.
| item | what it is | evidence |
|---|---|---|
| `AIAttr` | `StreamableHelper<Game::AttribMap>` | §3 |
| `AITurnPris` | `AIWeightMap<StreamableEnum<AIPurposeID>>` | §4 |
| `AISit` | `Game::AISituation` | two weight maps, `0x0069a220` |
| `AIPlyHat` | `AIWeightMap<StreamableEnum<uint>>` | §4 |
| `prs2` | `VectorHelper<Game::AIPlayerRequestStamp>` | §5 |
| **`dsh`** | **a count, not a scalar** | §2a |
| `pid`, `trns` | the `dsh` element: an int and a `VectorHelper<int>` | §2a |
| `NBStab` | count = span **>> 3** over the vector at `+0x38..+0x3c` | loop writes `BStabPl` at elem+0, `BStabTn` at elem+4 |
| `NMBlst` | count = span **>> 3** over `+0x48..+0x4c` | loop writes `MBlstSy` at elem+0, `MBlstTn` at elem+4 |
| `AIDNG` | one nested `DesignNameGen`, **not** a loop body | single helper call at `0x006c710f` |
| `AIHivJ`, `SDFlT` | plain ints at `+0x104`, `+0x368` | — |
| `nalat` | `VectorHelper<StreamableEnum<uint>>` over a **temporary** vector built at the call site | §4a |
| **`lnat`, `lat`** | **plain scalar members** at `+0x58`, `+0x5c` | §2b |
| `AINumSys` | a **computed** count of the systems the following loop actually emits | both loops walk the same list under the same `*(x+8) == 3` test |
| `AISysID`, `AISys` | the `AINumSys` element pair | — |
| `NCmbR` | count = span >> 2 over the pointer vector at `+0x94`'s `+0x28..+0x2c` | each element writes one `CmbR` frame |
| `NumCL` | count = span **/ 0xc** over `+0x1ac..+0x1b0` | loop writes `CLTn`(+0), `CLSyID`(+4), `CLPlID`(+8) |
| `NPrv` | count = span >> 3 over `+0x32c..+0x330` | loop writes `NPrvId`(int, +0), `NPrvVa`(float, +4) |
| `NTecS` | count = span >> 2 over `+0x13c..+0x140` | loop writes `TecS` |
| `fct` | plain int at `+0x1f0` | — |
| `apr` | `VectorHelper<Game::AIAutoPeaceRun>` at `+0x1f8` | — |
Two of these are also **confirmed by the data**, not only by the disassembly: `NPrv` is 1 or 2 in
our saves and is followed by exactly that many `(NPrvId, NPrvVa)` pairs; `NTecS` is 2 or 3 and is
followed by exactly that many `TecS`. `dnnc` inside `AIDNG` is 0, 1 or 2 and is followed by exactly
that many `(dnid, dnnm)` pairs — the zuul save's two-entry case is what makes it a count rather
than a coincidence.
### 2a. `dsh` is a `std::map`'s `_Mysize`, so it is the element count
The recovery lists `dsh` as a scalar member and `pid`/`trns` as elements of nothing. Taking that at
face value would desynchronise the reader, because there is no other count before the pairs. What
`Write` actually does is:
```
local = *(agent + 0x240) ; WriteInt("dsh", &local)
node = *(agent + 0x23c) ; red-black walk until back at the head
WriteInt("pid", node + 0x0c)
VectorHelper<int>("trns", node + 0x10)
```
`+0x23c` is `_Myhead` and `+0x240` is `_Mysize` — the same head@0 / size@4 layout the campaign
already pinned on `ServerSystem::NVO` (`0x274` / `0x278`). The node's `_Isnil` is at `+0x21`, which
puts `_Myval` at `+0x0c` and makes it `pair<const int, std::vector<int>>` — 4 + 16 = 0x14 bytes,
landing `_Color` exactly on `+0x20`. So the container is `std::map<int, std::vector<int>>` and
**`dsh` is its size**. Structurally it has to be: a reader has no terminator and no other number.
### 2b. `lnat` and `lat` are scalars, and the data agrees
The recovery marks both `[element]`. They are two ordinary `WriteInt`s of `agent+0x58` and
`agent+0x5c`, immediately after the `nalat` frame. The saves confirm it independently: `nalat`'s
count is 0 in all eleven blocks and `lnat`/`lat` are present anyway, which no element reading
survives.
More generally the `[element]` flag is unreliable for this class — it is also set on `AIDNG`,
`nalat`, `AINumSys` and `apr`, which are not loop bodies. It does not affect the conformance check,
which aligns on tag and disk primitive only, so `objects/streams.json` was left alone; this is the
same class of recovery artefact as lane W's open item 4 (`SVSOCrowDefenders::dsys`).
## 3. `Game::AttribMap` — and `Attrib` fell out with it
`AttribMap::Write` (`0x0059ee60`) writes `*(this+8)` as an anonymous count, then walks the tree at
`*(this+4)` writing **two anonymous strings** per node — key at `node+0x0c`, value at `node+0x28`,
`_Isnil` at `node+0x45`. Key and value are both `Mars::String` (0x1c), so `_Myval` is 0x38 bytes and
`_Color` lands on `+0x44`: `std::map<Mars::String, Mars::String>`, exactly.
Lane W recorded `Attrib` in `Game::StrategyServer` as "an empty frame in every save; there is
nothing to type". It is not empty — it is a frame holding a count of 0, which is why it showed as
**2** opaque items. Typing it as `AttribMap` closes both, on all four saves.
The entry pair is a hypothesis in the behavioural sense: the map is empty everywhere we can look.
The *type* is certain, from the writer.
## 4. `Mars::StreamableEnum<T>` is a frame, and that corrects two of lane W's shapes
`AIWeightMap<K>` (four instantiations, one wire shape) writes an anonymous count then, per entry,
an anonymous **key frame** and an anonymous float. The key frame's body is what matters:
* `StreamableHelper<StreamableEnum<T>>::Write` is `0x0054b6e0` and writes **exactly one anonymous
int**. So a `StreamableEnum` member is a *frame containing an int*, never a bare int.
* `StreamableHelper<Game::ShipSectionID>` writes two anonymous ints (`0x0056ed50`), which is the
key for the `AIWeightMap<ShipSectionID>` instantiation and matches the `dnid` frames on disk
(`{2, 44}`, `{5, 37}`, …).
The consequence reaches outside this block. `VectorHelper<StreamableEnum<uint>>::Write`
(`0x0042f7f0`) writes a count and then **n anonymous frames**, one int each. Lane W typed
`Game::SVSOSwarmQueen::SysMem` and `Game::SVSOVonNeumann::mts` as `std::vector<int32_t>` — a framed
array of plain ints — reasoning from the decorated helper name. The name is right; the element
framing is not. Both are now `std::vector<StreamEnum>`, along with `nalat` here.
**This changes no bytes and closes no items**: all three have count 0 in every save, which is
exactly why the error was invisible. It is a correction to an unexercised layout, and it stays an
unexercised layout — see §7. Lane W's own note flagged these three as hypotheses; this is what
checking one of them looked like.
### 4a. `nalat` is computed, not stored
`nalat`'s vector is built on the stack at the call site (`FUN_006c0360` over the members at `+0x18`
and `+0x1c`) and destroyed immediately after. It is a derived list, not a member, so its content is
not recoverable from a static read of the agent's fields.
## 5. `Game::AIPlayerRequestStamp` has no wire-table entry, by construction
`prs2` is `VectorHelper<Game::AIPlayerRequestStamp>` (`0x0069b310`): a count of 8-byte elements,
each an anonymous frame. The element writer is `0x00695400` and emits two **named** ints, `pid` and
`trn`. `tools/serializers.py` reports "no serializer" for the class because it is a POD with no RTTI
class of its own — it is reached only through a specialised `StreamableHelper` thunk, one of the 38
such types §3 of `serializer-struct-recovery.md` describes. So the shape is typed from the writer
directly and is **not bound** in `test_wire_schema.cpp`: there is nothing in the table to bind it to.
## 6. `Game::AISystem::Write` never reads its object
`0x006bda30` takes only the stream. It constructs an empty `AISituation` on the stack — two
16-byte vectors, both zeroed — writes it as `AISysSit`, and destroys it. `this` is never touched.
So **every `AISys` body on disk is two empty weight maps regardless of game state**, which is what
the saves show: 28 identical `AISys` frames per block, in every block, in every save. The
`AINumSys`/`AISysID` pairing still carries real information (which systems the AI tracks); the
situation payload does not. Worth knowing before anyone tries to reconstruct AI behaviour from it.
The recovery graded `AISystem` "partial — only container elements / computed values recovered". That
grade is right, and this is why.
## 7. What is still a hypothesis, and what would settle it
Every container in the block has **count 0 in all eleven `AIAgent` blocks of all four saves**, apart
from `AIDNG`'s `dnnc`, `NPrv` and `NTecS`. So the *frames* are exercised and most *element layouts*
are not. Labelling it honestly:
| item | element layout | exercised? |
|---|---|---|
| `AIDNG` / `dnnc` | `(dnid: ShipSectionID, dnnm: string)` | **yes** — 0, 1 and 2 entries observed |
| `NPrv` | `(NPrvId: int, NPrvVa: float)` | **yes** — 1 and 2 entries observed |
| `NTecS` | `TecS: int` | **yes** — 2 and 3 entries observed |
| `AINumSys` | `(AISysID: int, AISys: frame)` | **yes** — 28 entries in every block |
| `AIAttr` entries | `(string, string)` | no — map empty everywhere |
| `AITurnPris`, `AIPlyHat`, `AISitSecs`, `AISitWeapFams` entries | `(key frame, float)` | no — count 0 everywhere |
| `prs2` | `(pid: int, trn: int)` | no |
| `dsh` | `(pid: int, trns: int[])` | no |
| `NBStab`, `NMBlst` | two ints | no |
| `nalat` | one-int frame | no |
| `NCmbR` / `CmbR` | `Game::AICombatReport` | no |
| `NumCL` | three ints | no |
| `apr` | `Game::AIAutoPeaceRun` | no |
The unexercised rows are typed from the writers, not guessed from bytes, and the conformance check
agrees with the recovery on every one. They are still hypotheses in the behavioural sense and
`tests/mars_stream/test_stream.cpp::test_aiagent_block` is the only thing that exercises them: it
populates every container, round-trips, and asserts the bytes are stable. It also asserts the trap
in §4 directly — that a two-element `nalat` produces two **complex** nodes, which a bare-int typing
would get wrong.
**What would settle them**: a save from a longer game with several AI players in contact. `NBStab`
and `NMBlst` need the AI to have border-stability and menace-blacklist entries, `NCmbR` needs the AI
to have fought a battle it kept a report of, `prs2` and `dsh` need multi-player AI diplomacy and
design sharing, and `AIAttr` needs whatever sets AI attributes (possibly a scenario). None of that
needs anything the shim cannot produce; it needs turns played, which is lane O's machine.
Two things stay carried on purpose:
* **`Game::TacReport`** (`CombatPlayerStats::TacReports`) has a computed count `TRnc` and three
trailing scalar runs (`TRships`, `TRsats`, `TRshipsL`) whose relationship the linear recovery
cannot resolve, and no save we hold contains one. Its body is carried as `Node`s rather than typed
on a guess. It sits two levels under `NCmbR`, which is 0 everywhere.
* **`TurnCommands_v5`**, unchanged from lane W §4a — a proven blocker, not a suspected one.
## 8. Artifacts
* `sots-engine` branch `wip/aiagent` (off `f4f3a7a`, never committed to main):
`src/mars/stream/shapes.h` (`StreamEnum`, `AttribMap`, `AIWeightMap<K>` + two aliases,
`AISituation`, `AISystem`, `DesignNameGen`, `AIAutoPeaceRun`, `CombatPlayerStats`,
`AICombatReport`, `AIRequestStamp`, `StrategyAIAgent`, `CustomDataBlock`; `Sim::Attrib` retyped;
`SysMem`/`mts` element type corrected; `SaveGame` now selects each `CD` body by the id at the same
ordinal in `CDT`), `tests/mars_stream/test_wire_schema.cpp` (+12 bindings),
`tests/mars_stream/test_stream.cpp` (+2 unit tests, no saves needed),
`tests/mars_stream/test_save.cpp` (ratchet 97.5 → 99.8).
* `sots-re`: `ghidra/addresses.d/lane-a.json` (29 entries, merges to 669 with no duplicate name),
this note.
* `include/generated/sots_stream_schema.h` was **not** regenerated: `tools/streams.py` and
`tools/gen_stream_schema.py` were re-run and the output is identical to the committed header apart
from the provenance line, which carries the notes repo's HEAD and so changes on every commit.
`objects/streams.json` is unchanged.
* `ghidra/generated/sots_addresses.h` was **not** regenerated either — same reasoning as lane W: the
fragment carries this lane's addresses, and regenerating in place would sweep other lanes'
in-flight fragments into this branch. The engine consumes none of these addresses.
* `verify/save-reader/save_reader.py` was **not** changed. Its `CD` handling is an *absence* (the
blocks are `any`), not a defect, so there was nothing to correct mid-campaign; `state_checksum.py`
is byte-identical and still `PROVED`. Mirroring the `AIAgent` shape into the Python oracle is
available as follow-up and would change the `/CD` subsystem digests (and only those).
## 9. Open items for the next lane
1. A save with issued turn commands, for `TurnCommands_v5` — still the one blocker of size.
2. A longer multi-AI save would exercise ten of the thirteen element layouts in §7 at once.
3. `Game::TacReport`'s `TRnc` / `TRships` / `TRsats` / `TRshipsL` structure needs a combat report on
disk before it is worth resolving.
4. `objects/streams.json`'s `[element]` flags are unreliable inside
`Game::StrategyAIAgent::Streamable` (§2b) and in `Game::SVSOCrowDefenders` (lane W's item 4). The
flag is not consumed by the conformance check, so this is a cleanliness item, not a correctness
one — but anyone reading the table as documentation should know.

View file

@ -0,0 +1,236 @@
{
"entries": [
{
"name": "Game_StrategyAIAgent_Streamable_Write",
"addr": "0x006c6f00",
"convention": "thiscall",
"prototype": "void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player.<id>.AIAgent` CD block. 36 wire items, NO conditionals: the only `if` the decompiler shows around `lnat` is an inlined std::vector destructor whose operator delete is marked noreturn, and both paths converge at 0x006c72e8. The agent object is *(this+4)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_StrategyAIAgent_Streamable_Read",
"addr": "0x006c8fd0",
"convention": "thiscall",
"prototype": "bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_StrategyAIAgent_DesignNameGen_Write",
"addr": "0x006c77a0",
"convention": "thiscall",
"prototype": "void (Game::StrategyAIAgent::DesignNameGen* this, Mars::Stream* s) // `dnnc` = (this+8 - this+4)/0x24, then dnnc x { dnid: StreamableHelper<ShipSectionID> at elem+0, dnnm: WriteString at elem+8 }. Element stride 0x24 = ShipSectionID(8) + Mars::String(0x1c)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_StrategyAIAgent_DesignNameGen_Read",
"addr": "0x006ccc70",
"convention": "thiscall",
"prototype": "bool (Game::StrategyAIAgent::DesignNameGen* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AISituation_Write",
"addr": "0x0069a220",
"convention": "thiscall",
"prototype": "void (Game::AISituation* this, Mars::Stream* s) // AISitSecs = AIWeightMap<ShipSectionID> at this+0, AISitWeapFams = AIWeightMap<StreamableEnum<WeaponFamilyID>> at this+0x10. sizeof >= 0x20",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AISituation_Read",
"addr": "0x0069a160",
"convention": "thiscall",
"prototype": "bool (Game::AISituation* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AISystem_Write",
"addr": "0x006bda30",
"convention": "thiscall",
"prototype": "void (Game::AISystem* this, Mars::Stream* s) // writes ONE `AISysSit` frame and nothing else, and the AISituation it writes is constructed empty on the stack -- `this` is never read. Every AISys body on disk is therefore two empty weight maps regardless of game state",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AISystem_Read",
"addr": "0x006bd960",
"convention": "thiscall",
"prototype": "bool (Game::AISystem* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AttribMap_Write",
"addr": "0x0059ee60",
"convention": "thiscall",
"prototype": "void (Game::AttribMap* this, Mars::Stream* s) // count = *(this+8) (the tree's _Mysize; _Myhead is at this+4), then a red-black walk writing two NULL-named strings per node -- key at node+0x0c, value at node+0x28, _Isnil at node+0x45. So AttribMap is a std::map<Mars::String, Mars::String>",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AttribMap_Read",
"addr": "0x005a8a60",
"convention": "thiscall",
"prototype": "bool (Game::AttribMap* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_ShipSectionID_Write",
"addr": "0x00696b40",
"convention": "thiscall",
"prototype": "void (Game::AIWeightMap<Game::ShipSectionID>* this, Mars::Stream* s) // NULL-named count = span/0xc, then n x { NULL-named StreamableHelper<ShipSectionID> frame at elem+0, NULL-named float at elem+8 }",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_ShipSectionID_Read",
"addr": "0x006bff00",
"convention": "thiscall",
"prototype": "bool (Game::AIWeightMap<Game::ShipSectionID>* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_StreamableEnumUInt_Write",
"addr": "0x00696c50",
"convention": "thiscall",
"prototype": "void (Game::AIWeightMap<Mars::StreamableEnum<unsigned int>>* this, Mars::Stream* s) // NULL-named count = span>>3, then n x { NULL-named StreamableHelper<StreamableEnum<uint>> frame at elem+0, NULL-named float at elem+4 }",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_StreamableEnumUInt_Read",
"addr": "0x006b57c0",
"convention": "thiscall",
"prototype": "bool (Game::AIWeightMap<Mars::StreamableEnum<unsigned int>>* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_AIPurposeID_Write",
"addr": "0x0069b590",
"convention": "thiscall",
"prototype": "void (Game::AIWeightMap<Mars::StreamableEnum<Game::AIPurposeID>>* this, Mars::Stream* s) // same wire shape as the uint instantiation; `AITurnPris`",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_AIPurposeID_Read",
"addr": "0x006c01a0",
"convention": "thiscall",
"prototype": "bool (Game::AIWeightMap<Mars::StreamableEnum<Game::AIPurposeID>>* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_WeaponFamilyID_Write",
"addr": "0x0069b4c0",
"convention": "thiscall",
"prototype": "void (Game::AIWeightMap<Mars::StreamableEnum<Game::WeaponFamilyID>>* this, Mars::Stream* s) // same wire shape as the uint instantiation; `AISitWeapFams`",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIWeightMap_WeaponFamilyID_Read",
"addr": "0x006bffe0",
"convention": "thiscall",
"prototype": "bool (Game::AIWeightMap<Mars::StreamableEnum<Game::WeaponFamilyID>>* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_ShipSectionID_Write",
"addr": "0x0056ed50",
"convention": "thiscall",
"prototype": "void (Game::ShipSectionID* this, Mars::Stream* s) // two NULL-named ints, at this+0 and this+4",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_ShipSectionID_Read",
"addr": "0x0056ed10",
"convention": "thiscall",
"prototype": "bool (Game::ShipSectionID* this, Mars::Stream* s)",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_StreamableHelper_StreamableEnumUInt_vftable",
"addr": "0x009e24b8",
"convention": "data",
"prototype": "void* [3] // { 0x004f7230 scalar-deleting dtor, 0x00697ae0 Read, 0x0054b6e0 Write }",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_StreamableEnum_Write",
"addr": "0x0054b6e0",
"convention": "thiscall",
"prototype": "void (Mars::StreamableHelper<Mars::StreamableEnum<T>>* this, Mars::Stream* s) // writes exactly ONE NULL-named int. So a StreamableEnum member is a FRAME on the wire whose body is a single int -- not a bare int. This is what makes a VectorHelper<StreamableEnum<T>> a framed array of one-int frames",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_StreamableEnum_Read",
"addr": "0x00697ae0",
"convention": "thiscall",
"prototype": "void (Mars::StreamableHelper<Mars::StreamableEnum<T>>* this, Mars::Stream* s) // ReadInt through stream slot +0x10; stores only when the read succeeds",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_VectorHelper_StreamableEnumUInt_vftable",
"addr": "0x009e2438",
"convention": "data",
"prototype": "void* [3] // { 0x004f7230 dtor, 0x00431610 Read, 0x0042f7f0 Write }",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_VectorHelper_StreamableEnumUInt_Write",
"addr": "0x0042f7f0",
"convention": "thiscall",
"prototype": "void (Mars::VectorHelper<Mars::StreamableEnum<unsigned int>>* this, Mars::Stream* s) // NULL-named count = span>>2, then n NULL-named FRAMES each written by Mars_StreamableEnum_Write. Backs `nalat`, `SysMem` and `mts`, all of which have count 0 in every save we hold",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_VectorHelper_AIPlayerRequestStamp_vftable",
"addr": "0x00a1a700",
"convention": "data",
"prototype": "void* [3] // { 0x004f7230 dtor, 0x006b5600 Read, 0x0069b310 Write }; backs `prs2`",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_VectorHelper_AIPlayerRequestStamp_Write",
"addr": "0x0069b310",
"convention": "thiscall",
"prototype": "void (Mars::VectorHelper<Game::AIPlayerRequestStamp>* this, Mars::Stream* s) // NULL-named count = span>>3, then n NULL-named frames written by Game_AIPlayerRequestStamp_Write. Element stride 8",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Mars_StreamableHelper_AIPlayerRequestStamp_vftable",
"addr": "0x00a1a730",
"convention": "data",
"prototype": "void* [3] // { 0x004f7230 dtor, 0x006953b0 Read, 0x00695400 Write }",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
},
{
"name": "Game_AIPlayerRequestStamp_Write",
"addr": "0x00695400",
"convention": "thiscall",
"prototype": "void (Mars::StreamableHelper<Game::AIPlayerRequestStamp>* this, Mars::Stream* s) // two named ints: `pid` at +0 and `trn` at +4. Game::AIPlayerRequestStamp is a POD with no RTTI class of its own, reached only through this specialised helper, so tools/serializers.py reports 'no serializer' for it and it has no entry in the generated wire table",
"status": "verified",
"source": "findings/objects/aiagent-block.md (lane A 2026-09-08)"
}
]
}