findings/subsystems/treaty-turn-stamp.md. Briefed on the post-combat tail, ranked its
phases by leaves-closed-per-effort against the standalone's 158-leaf residual, and found
that every tail phase which moves a leaf in this corpus is blocked on another lane
(ship construction, the budget) or on the tail's vtable blind spot. Section 5 lists that
ranking with the numbers. The two items below are what a lane holding no VM could close.
1. StrategyServer_StampTreatyTurns 0x007898c0, read as instructions. The diplomacy
ledger's per-turn stamp over every ordered pair of players holding a treaty, creating
the entry on demand. Sole caller is ApplyTurnCommands, so it is a HOST step and not a
tail phase. Three corrections to strategic-turn-internals.md 5.2, which is flagged in
place: the relation codes are 3=ally / 2=NAP / 1=cease-fire and not the reverse (which
combat-done-tail.md 2A already implied independently); the stamped value is Frame, not
ModCount; and the bit is the PlyrIdx field, not the player's vector position -- the
opposite convention from the shared-vision mask.
Modelled in sots-engine as host phase H02. Predicted 26 leaves on the reference pair
and 14 on pair 2 before building; measured exactly 26 and 14, 0 regressed, and +12 on
each of three further pairs from a different game at turns 2, 15 and 16 that the model
was never fitted to. 76 closed / 0 regressed over five pairs, 0 RNG words. The rule
also reproduces the ledger of ten of the eleven corpus saves entry for entry.
2. Player.Status. backlog.md item 6 has it blocked on "the writer between tail 31 and the
autosave -- watchpoint". There is no such writer. A whole-image scan for immediate
stores to ServerPlayer+0x164 finds exactly three: ProcessTurn's encounter loop writes
1, ResumePlaying writes 0 on load (the 4 -> 0 the determinism note recorded), and
StrategyServer_MarkPlayerTurnEnded 0x00821a40 writes 4 from the three End Turn
SUBMISSION paths, which run before the turn is processed. Item 6 needs one predicate,
not a watchpoint -- and an entry probe on that function names the set directly.
NOT written. The only two saves in the corpus with a non-zero Status agree with
Species != 4 on all eight players, and that is eight observations on a 1-bit predicate
that nine of the eleven saves do not exercise at all. Evaluated and reported.
Seven addresses in ghidra/addresses.d/lane-t2.json, validated to a scratch path against
the merged set (1,095 entries, no duplicate). verify/results/standalone/{report,status}
deliberately untouched: lane C3 published into them minutes before this run.
60 lines
6 KiB
JSON
60 lines
6 KiB
JSON
{
|
|
"entries": [
|
|
{
|
|
"name": "StrategyServer_StampTreatyTurns",
|
|
"addr": "0x007898c0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (StrategyServer* S, vector<uint32>* allianceBroken, vector<uint32>* napBroken, vector<uint32>* 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"
|
|
}
|
|
]
|
|
}
|