sots-re/ghidra/addresses.d/lane-w3.json
alex 471d6cb973 W3: NVO.TShn's writer trapped live, its gate named, and 158/158 on the corpus
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.
2026-09-08 16:41:09 -04:00

256 lines
16 KiB
JSON

{
"entries": [
{
"name": "ServerSystem_off_AFlags",
"offset": "0xd4",
"convention": "offset",
"status": "verified",
"prototype": "int AFlags -- the per-player 'has something here' mask, bit = PlyrIdx. Confirmed from ServerSystem::Write 0x00749630 at 0x00749aec (`mov ecx,[edi+0xcc]` with the \"AFlags\" tag at 0xa1f3b0; Write runs on the IStreamable subobject at +0x8, so 0xcc + 8 = 0xd4). Lane E3's +0xd4 is right"
},
{
"name": "ServerSystem_off_SnLv",
"offset": "0x24",
"convention": "offset",
"status": "verified",
"prototype": "int SnLv -- 2 bits per player (level = 0 none / 1 partial / 3 full), NOT SERIALISED. Written once per turn in the sensor phase at 0x0086a789 as `s->SnLv = (s->SnLv & ~(3<<2i)) | (level<<2i)`; read by ServerSystem::IsKnownTo 0x00746390 and RefreshVisibleOwnerIfKnown 0x0075bd70, and shipped to clients by SynchronizePlayer at 0x007c76a4. Because it is absent from every save, any NVO/TShn claim that depends on it is a hypothesis on the corpus alone (rule 6)"
},
{
"name": "ServerSystem_off_NVO_Myhead",
"offset": "0x274",
"convention": "offset",
"status": "verified",
"prototype": "std::map<int PlyrIdx, OwnRec>::_Myhead for NVO (the visible-owner record). The map object starts at +0x270 and is 0x10 bytes: allocator +0x0, _Myhead +0x4, _Mysize +0x8, comparator +0xc. From ServerSystem::Write 0x0074a097/0x0074a195 (`[edi+0x26c]`, +8 for the IStreamable subobject) and from the live arming probe, which validated +0x274 on 9 of 28 systems and +0x26c on 0"
},
{
"name": "ServerSystem_off_NVO_Mysize",
"offset": "0x278",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVO. Write 0x0074a07d (`[edi+0x270]`) carries the \"NVO\" tag 0xa1f2c0, so the wire's NVO count is this word. CORRECTS objects/layouts.md, which lists NVO@0x278 as an int field and PID@0x274 as a handle: +0x274 is _Myhead and the \"PID\" tag belongs to the node KEY, not to the object"
},
{
"name": "ServerSystem_off_NVE_Myhead",
"offset": "0x284",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Myhead for NVE (the sighting record). Write 0x0074a1bb. Confirms lane E3's +0x284/+0x288"
},
{
"name": "ServerSystem_off_NVE_Mysize",
"offset": "0x288",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVE. Write 0x0074a1a1, tag \"NVE\""
},
{
"name": "ServerSystem_off_NVs_Myhead",
"offset": "0x294",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Myhead for NVs (the per-player PlayerView record). Write 0x0074a27d"
},
{
"name": "ServerSystem_off_NVs_Mysize",
"offset": "0x298",
"convention": "offset",
"status": "verified",
"prototype": "std::map _Mysize for NVs. Write 0x0074a263, tag \"NVs\""
},
{
"name": "ServerSystem_NVO_node_off_TShn",
"offset": "0x12",
"convention": "offset",
"status": "verified",
"prototype": "int16 TShn, offset within the NVO map NODE (not the value). Node is _Left +0x0, _Parent +0x4, _Right +0x8, key(PlyrIdx) +0xc, value +0x10 = {int16 touched +0x10, int16 TShn +0x12, int32 OID +0x14, bool isind +0x18, IndependenceInfo indi +0x1c}, _Color +0x8c, _Isnil +0x8d -- so an NVO node is ~0x8e bytes, unlike NVE's 0x20. From Write 0x0074a0ca (`lea ecx,[ebx+0x12]`, tag \"TShn\") and confirmed live: a 4-byte watchpoint on value+0x0 trapped both halves separately"
},
{
"name": "ServerSystem_NVO_operator_index",
"offset": "0x0075a890",
"convention": "thiscall",
"status": "verified",
"prototype": "OwnRec* (NVO_map* this /* ServerSystem+0x270 */, const int* plyrIdx) // std::map::operator[]; returns node+0x10. Exactly ONE caller in the image: ServerSystem::SetVisibleOwner 0x0075b94b (NVE's operator[] 0x00752730 has three). That single-caller fact is what makes the writer inventory below complete"
},
{
"name": "ServerSystem_SetVisibleOwner",
"offset": "0x0075b880",
"convention": "thiscall",
"status": "verified",
"prototype": "void (ServerSystem* this, ServerPlayer* p, const OwnRec* src) // ret 8. Stamps value.touched = (int16)this->owner(+0x10)->Frame(+0x8) and copies src's TShn/OID/isind/indi verbatim into NVO[p->PlyrIdx]. THE ONLY WRITER OF NVO.TShn IN THE IMAGE. Local gate (converge point 0x0075b983, the inlined IndependenceInfo dtor, so it is a real branch and not a rule-4 artifact): skip the whole write when src->OID == p->id but this->PID(+0x100)->id != p->id, logging \"VisibleOwner: %s sees their self as owner of %s, but they are not.\""
},
{
"name": "ServerSystem_SetVisibleOwner_TShnStore",
"offset": "0x0075b961",
"convention": "offset",
"status": "verified",
"prototype": "site `mov WORD PTR [eax+0x2],dx` -- the TShn store. Trapped live by a DR watchpoint on Spica's NVO root node: EIP 0x0075b965 (the trap is taken after the store retires), twice per End Turn. The touched-word store is the instruction before it at 0x0075b957 (trap EIP 0x0075b95a)"
},
{
"name": "ServerSystem_RecordVisibleOwner",
"offset": "0x0075bca0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (ServerSystem* this, ServerPlayer* p) // ret 4. Builds an OwnRec with TShn = (int16)server->Frame, OID = this->PID(+0x100)->id, isind/indi from this->indi(+0x1c8), then calls SetVisibleOwner at 0x0075bd45. THIS is where TShn = Frame is decided. Confirmed live: the trapped frame's [ebp+4] was 0x0075bd4a"
},
{
"name": "ServerSystem_RecordVisibleOwnerNone",
"offset": "0x0075bc00",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* p) // TShn = Frame, OID = 0 -- 'you now see this system as unowned'. Frame read at 0x0075bc5f"
},
{
"name": "ServerSystem_ShareVisibleOwner",
"offset": "0x0075b9b0",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* from, ServerPlayer* to) // the NVO analogue of ShareObservation 0x00754d90: passes the source player's record verbatim, so TShn is NOT refreshed. No corpus save has an alliance, so this path has never executed (rule 6)"
},
{
"name": "ServerSystem_FindVisibleOwner",
"offset": "0x0074d2e0",
"convention": "thiscall",
"status": "verified",
"prototype": "OwnRec* (ServerSystem* this, ServerPlayer* p) // map find on NVO (0x0074d2f2 `lea ecx,[esi+0x270]`, 0x0074d300 `cmp edx,[esi+0x274]`); returns node+0x10 or NULL. Every caller only READS the record"
},
{
"name": "ServerSystem_GetVisibleOwnerId",
"offset": "0x0074f710",
"convention": "thiscall",
"status": "mapped",
"prototype": "int (ServerSystem* this, ServerPlayer* p) // FindVisibleOwner then rec->OID"
},
{
"name": "ServerSystem_IsKnownTo",
"offset": "0x00746390",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // THE NVO GATE, and it is looser than IsVisibleTo 0x00743fb0. Whole body: `p && ( (this->AFlags(+0xd4) >> p->PlyrIdx(+0x28)) & 1 || ( p->PlyrIdx < 15 && ((this->SnLv(+0x24) >> 2*p->PlyrIdx) & 3) != 0 && TechTree::HasResearched(p->TechTree(+0xf4), 10000 /* CCC_AdvSens */) ) )`. Byte-decoded at 0x007463ae / 0x007463c0 / 0x007463cb / 0x007463da"
},
{
"name": "ServerSystem_RefreshVisibleOwnerIfKnown",
"offset": "0x0075bd70",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (ServerSystem* this, ServerPlayer* p) // ret 4. IsKnownTo inlined byte-for-byte (0x0075bd88 AFlags, 0x0075bd93 idx<15, 0x0075bd9e SnLv, 0x0075bdb2 HasResearched(0x2710)), then RecordVisibleOwner at 0x0075bdbe. Confirmed live: the trapped frame's [[ebp]+4] was 0x0075bdc3, the return of that call"
},
{
"name": "SensorPhase_RefreshAllVisibleOwners",
"offset": "0x00813700",
"convention": "fastcall",
"status": "verified",
"prototype": "void (StrategyServer_S4* server) // for p in players(+0x50..+0x54) for s in systems(+0x40..+0x44): s->RefreshVisibleOwnerIfKnown(p) at 0x00813737. The player loop is UNFILTERED -- no NPC/species test anywhere on this path. Called from StrategyServer::UpdateSensors 0x0086a8d0 at 0x0086a91d"
},
{
"name": "SensorPhase_ComputeContactLevel",
"offset": "0x00850cf0",
"convention": "thiscall",
"status": "mapped",
"prototype": "int (Sensors* this, ServerPlayer* p, Entity* e) // 3 if IsVisibleTo (AFlags), else 1 if inside p's sensor envelope, else 0 (0x00850d18 / 0x00850e1b). Its result is packed into ServerSystem::SnLv(+0x24) at 0x0086a789"
},
{
"name": "ServerSystem_ChangeOwner",
"offset": "0x0075bde0",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (ServerSystem* this, ServerPlayer* oldOwner, ServerPlayer* newOwner) // Game::ServerSystem vftable 0x00a2044c slot 6; records a visible-owner entry for both parties at 0x0075be77 / 0x0075bf2b"
},
{
"name": "StrategyServer_OnCommand_SetResearchRate",
"offset": "0x00821a80",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const float* rate) // ret 8. Bumps ModCount at 0x00821a84 then sets player->ResRate(+0xbc) = clamp(*rate, 0.0f, 1.0f), logging \"OnCommand: Research rate %f constrained to %f for %s.\" when it clamps. NAMES lane W2's `StrategySim_ModCountBump_unresolved_00821a84`, the handler that fires 4x per End Turn on ref-turn2 -- once per player that has a client"
},
{
"name": "StrategyServer_OnCommand_SetSystemRates",
"offset": "0x0086c3e0",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const SysRatesCmd* cmd) // ret 8. Bumps ModCount at 0x0086c3e6. Strings \"OnCommand: Unable to set system rates for %d(id). System not found.\" (0xa32448) and \"OnCommand: Player setting system rates for system he does not own.\" (0xa32400). NAMES lane W2's `..._unresolved_0086c3e6`"
},
{
"name": "StrategyServer_OnCommand_TransferShips",
"offset": "0x0088bed0",
"convention": "thiscall",
"status": "verified",
"prototype": "int (StrategyServer_S4* this, int playerId, const TransferCmd* cmd, ...) // ret 0xc. Bumps ModCount at 0x0088befe. Strings \"OnCommand: Cannot transfer ships. Player %d(id) not found.\" (0xa33248) and \"TransferShips: Fleet flagged as empty, but still has ships.\". NAMES lane W2's `..._unresolved_0088befe`"
},
{
"name": "StrategyServer_OnCommand_AddBuildOrder",
"offset": "0x00849460",
"convention": "thiscall",
"status": "mapped",
"prototype": "bool (StrategyServer_S4* this, int requesterId, const AddOrderCmd* cmd, int* outReason) // ret 0xc. Bumps ModCount at 0x0084946b. cmd = {int which, id target, id design, int arg}; picks target->+0xa4 (which==0) or +0x98 (which==1) as the BuildQueue, validates with 0x0083ce50, then BuildQueue::AddOrder 0x00845260 push_backs onto the list at queue+0x10. Reached from the Build Screen 0x00656540 via 0x00762fd0, and from the command replay at 0x0089009f. NAMES lane W2's `..._unresolved_0084946b`"
},
{
"name": "StrategyServer_OnCommand_SetResearchProject",
"offset": "0x0088fe0a",
"convention": "offset",
"status": "verified",
"prototype": "site `inc DWORD PTR [ebx+0x4]` -- a SECOND ModCount bump INLINED into ApplyTurnCommandBatch 0x0088f9b0 (at +0x45a), distinct from the one lane W2 found at 0x0089046c. String \"OnCommand: Cannot set research project for player %d(id). Player not found.\" (0xa30ed0). Fires 3x on turn1-state and 0x on turn2-state -- once per AI player that picks a research target on turn 1; the human player set a rate but no project, which is why the reference game's map banner reads \"No Research Project Assigned\""
},
{
"name": "StrategyServer_OnCommand_CreateDesign",
"offset": "0x00882910",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyServer_S4* this, int playerId, const DesignCmd* cmd) // ret 8. Bumps ModCount at 0x0088291a. Strings \"OnCommand: Cannot create design for player %d(id), player does not exist.\" (0xa32c40) and \"OnCommand: Unable to create design. ID not specified.\" (0xa32c04). Fires 1x on turn1-state, 0x on turn2-state. ANSWERS LANE AI4'S FALSIFIER: command list 1 emits a design COMMAND, not merely a design object that the build order names"
},
{
"name": "BuildQueue_AddOrder",
"offset": "0x00845260",
"convention": "thiscall",
"status": "mapped",
"prototype": "void (BuildQueue* this, int arg, ShipDesign* design) // builds an order record with five fields pre-set to -1 and push_backs it onto the std::list at this+0x10/+0x14 (\"list<T> too long\" guard at 0x008452e4)"
},
{
"name": "StrategyServer_CanAddBuildOrder",
"offset": "0x0083ce50",
"convention": "cdecl",
"status": "mapped",
"prototype": "bool (ServerPlayer* who, BuildQueue* q, ShipDesign* d, int* outReason) // the 'can this be built' validator; writes a numbered reason (0,1,2,8,...) through arg 4 and calls BankruptcyLevel 0x0080db10"
},
{
"name": "ServerPlayer_off_ReqCL",
"offset": "0xfd",
"convention": "offset",
"status": "verified",
"prototype": "bool ReqCL -- 'this player requires a client'. THE PREDICATE BEHIND Player.Status: a player with ReqCL == 0 never reaches SetupAIPlayers' AI list (gate at 0x007bc25c), so never gets a StrategyClient, so is never the subject of MarkPlayerTurnEnded and keeps the 0 that ResumePlaying wrote. The script binding that creates the four '_NPC' pseudo-players clears it explicitly at 0x00797bcb"
},
{
"name": "StrategyNetworkServer_AllPlayersTurnEnded",
"offset": "0x007cfd00",
"convention": "thiscall",
"status": "verified",
"prototype": "bool (StrategyNetworkServer* this) // vftable slot 3. The game's OWN statement of which players must carry Status == 4: `for p in players: if (!p->ReqCL(+0xfd)) continue; if (p->Elim(+0xf8)) continue; if (p->Status(+0x164) != 4) return false;` -- loop 0x007cffb7..0x007d0002 with edi = 4 loaded at 0x007cffa3"
},
{
"name": "StrategyNetworkServer_SetupAIPlayers",
"offset": "0x007bc1c0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (StrategyNetworkServer* this) // the ReqCL gate at 0x007bc25c decides which players get an AI client at all; AddAIPlayer 0x007bc333 -> 0x007b8ab0"
},
{
"name": "StrategyServer_OnPlayerEndTurn",
"offset": "0x007d9af0",
"convention": "thiscall",
"status": "verified",
"prototype": "void (StrategyServer* this, const SNMPlayerEndTurn* msg) // ONE call per network message, not a loop: handle = msg->+0x4 straight off the wire, MarkPlayerTurnEnded(this+4, handle) at 0x007d9b25 with NO gate before the store. Reached from StrategyNetworkClient::OnMessage 0x00784816. The field tests at 0x007d9b5d/0x007d9ba2/0x007d9bb0 all follow the store and only decide whether to raise a 'waiting on X' event"
},
{
"name": "StrategyServer_ProcessTurn_StatusOneStore",
"offset": "0x007dcc8a",
"convention": "offset",
"status": "verified",
"prototype": "site `mov DWORD PTR [eax+0x164],1` -- the phase-31 Status write lane T2 located. Its gate, decoded at 0x007dcc78, is `if (p->IsAI(+0xf9) == 0 || p->+0xfa != 0)`, over the encounter records at S+0x1e8 (0x74-byte outer, 0x44-byte inner with the ServerPlayer* first). It reads NO species field"
},
{
"name": "Game_SNMSetPlayerStatus_vftable",
"offset": "0x00a2d718",
"convention": "offset",
"status": "verified",
"prototype": "vftable for Game::SNMSetPlayerStatus (factory 0x0080f990, sizeof 0xc: vptr / +4 player INDEX / +8 status). Its handler is the OnMessage+0xa15 store lane W2 found: no gate, no field test -- the value comes verbatim off the wire. The sender is StrategyNetworkServer::Update 0x007cda40 at 0x007cdcf0, which computes `2*(flag!=0) + 2`, i.e. 4 when the player has nothing pending and 2 when it does"
}
]
}