{ "entries": [ { "name": "StrategyServer_StampTreatyTurns", "addr": "0x007898c0", "convention": "thiscall", "prototype": "void (StrategyServer* S, vector* allianceBroken, vector* napBroken, vector* cfBroken) // 700 B, ret 0xc. THE DIPLOMACY LEDGER'S PER-TURN STAMP, and the only writer of DiplomacyStats on a turn with no combat and no diplomatic command. Pass A (0x00789920..0x007899c4): over every ORDERED pair (A,B) of the S-frame player vector at S+0x54/+0x58, skipping A==B by POINTER, rel = A->GetRelation(B) 0x0080e050; rel 1 -> slot 8, rel 2 -> slot 0, rel 3 -> slot 4, else skip; then ctor a DiplomacyStats on the stack, GetDipStat(&local,B) 0x008180e0, store (int16)S->Frame(+0xc) at local+8+slot*2, SetDipStat(&local,B) 0x00863950. Field mapping: rel 3 -> lastally(+0x10), rel 2 -> lastnap(+8), rel 1 -> lastcf(+0x18). Pass B (0x007899ca..) is the betrayal counter and indexes the three broken-mask arguments by the inner loop index, substituting a zero local when a vector's length != nPlayers -- with no command stream it is a no-op. SOLE CALLER: ApplyTurnCommands 0x007b18b0 at 0x007b2461, so this runs BEFORE both turn drivers and AFTER BeginProcessTurn's frame bump", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08); closes 26 leaves on the reference pair and 14 on pair 2 in sots-engine, 0 regressed, and reproduces the ledger of 10 of 11 corpus saves exactly" }, { "name": "PlayerAlliances_Relation", "addr": "0x006d2050", "convention": "cdecl", "prototype": "int (int myPlyrIdx, PlayerAlliances* a, int otherPlyrIdx) // 58 B. if (myPlyrIdx == otherPlyrIdx) return 3; bit = 1 << otherPlyrIdx (shl by cl, so masked to 5 bits); if (a->AL(+4) & bit) return 3; if (a->NA(+8) & bit) return 2; return (a->CF(+0xc) & bit) ? 1 : 0. THE RELATION CODES ARE 3 = ALLIED (and self), 2 = NON-AGGRESSION, 1 = CEASE-FIRE, 0 = WAR -- strategic-turn-internals.md section 5.2 had 1 and 3 the other way round. The bit is the INDEX FIELD, not the position in the player vector (the opposite of the shared-vision mask), and AL is tested with NO alliance-id guard", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08); agrees with combat-done-tail.md section 2A, which reads Node::ResupplyAlliedFleets as gated on GetRelation == 3 and UpdateDiplomacyStatsFromCombat as gated on GetRelation < 1" }, { "name": "ServerPlayer_GetRelation", "addr": "0x0080e050", "convention": "thiscall", "prototype": "int (ServerPlayer* this, ServerPlayer* other) // 33 B, ret 4. A thin forwarder: tail-calls the cdecl PlayerAlliances_Relation 0x006d2050 with (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28)). 60+ call sites across the image", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)" }, { "name": "DiplomacyStats_ctor", "addr": "0x0080e7b0", "convention": "thiscall", "prototype": "DiplomacyStats* (DiplomacyStats* this) // 55 B. vptr = 0x00a21430; every field zeroed; then lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1. So a fresh entry's three 'last in force' fields are -1, NOT 0, and every counter (lastnapbty/bkn*/bty*/deadhome) is 0. This is what distinguishes 'never' from 'on turn 0' in the ledger", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)" }, { "name": "ServerPlayer_GetDipStat", "addr": "0x008180e0", "convention": "thiscall", "prototype": "void (ServerPlayer* this, DiplomacyStats* out, ServerPlayer* other) // 294 B, ret 8. if (!out) return; re-initialise *out to the ctor's defaults IN PLACE (the vptr is not touched); out->other(+4) = other->+0x4 (the handle id, i.e. the wire's PlayerID); if (!other) return; then a LINEAR FIRST-MATCH scan of the 0x24-stride vector at this->dipstats(+0x230/+0x234) for entry.other == GetId(other) 0x0042bfb0, copying the entry's thirteen int16 fields (out+8..out+0x21) on a hit. Stride read as 0x38e38e39 / sar 3", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)" }, { "name": "ServerPlayer_SetDipStat", "addr": "0x00863950", "convention": "thiscall", "prototype": "void (ServerPlayer* this, const DiplomacyStats* src, ServerPlayer* other) // 328 B, ret 8. if (!src || !other) return; the same linear first-match scan; ON A MISS default-construct a DiplomacyStats on the stack and push_back it (0x0085bc40) so a NEW ENTRY IS APPENDED AT THE END, then back().other = other->+0x4; finally copy src's thirteen int16 fields into the entry and re-write other. The append order is therefore the order in which pairs are first stamped, which is player-vector order", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)" }, { "name": "StrategyServer_MarkPlayerTurnEnded", "addr": "0x00821a40", "convention": "thiscall", "prototype": "bool (StrategyServer* this, int playerId) // 60 B, ret 4. p = HandleMap::Resolve(this + 0x80, playerId) 0x008b9240; if (!p) { Log(2, <0x00a2fb30>, playerId); return false; } p->Status(+0x164) = 4; return true. THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE. Three callers, all End Turn SUBMISSION paths that run before the turn is processed: EndTurn 0x00783be0 (+0x70, passes the client's own id at client+0x148), EndTurnForced 0x00783d30 (+0x7b), OnPlayerEndTurn 0x007d9af0 (+0x35). The other two immediate stores to +0x164 in the image are ProcessTurn +0x5ca (value 1, inside the 0x44-stride encounter-member loop) and ResumePlaying +0xb1 (value 0, the load-path normalisation determinism-oracle.md recorded as 'Status resets 4 -> 0 on load'). There is NO writer between tail phase 31 and the autosave; backlog.md item 6 looks in the wrong place", "status": "mapped", "source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08) section 3" } ] }