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.
findings/subsystems/output-term.md is the whole reading: the call chain with
real function boundaries, the formula, the closed list of nine values the data
files supply (down from an unbounded fear), the advance prediction with its
falsification table, and the live result.
Corrects strategic-turn-internals.md 3.3 in place. That block had the SHAPE
wrong, not just the detail: a system's output is a sum of three terms, and the
function it named as the population base-output term is the over-harvest
resource demand.
Live on VM140, two builds, two species, both hooks in compare mode:
GroupOutput 13,105 calls / 0 divergences, ComputeTotalOutput 11,252 / 1 (one
ulp), 0 undeclared writes in 24,357 guarded calls. Thirteen distinct system
states, and every unexercised branch is listed rather than counted as covered.
tools/max_income_predict.py is the other half: it computes lane Y's
bankruptcy-limit oracle from colony state and reports 6 of 25 player-records
matching exactly, with the misses all AI-owned and the single-system ones short
by exactly the 1.1 difficulty income multiplier.
Closes B3's oracle gap and B2's known gap. Both milestones write into the
owner's event list; nothing modelled it, so their clean compares bounded the
economy fields only.
Container: EventStorage embedded at ServerPlayer+0x29c (0x1c bytes), verified by
ServerPlayer::GetEventStorage 0x0080db00 whose whole body is 'lea eax,[ecx+0x29c];
ret'. EvNxID at +0x14 == player+0x2b0, exactly the byte run the harness guard
reports. The list is a vector<TurnEvents{int EvTurn; vector<PlayerEvent>}> --
bucketed by turn, not flat, which the save-editor struct note had wrong.
Entry point: int __thiscall EventStorage::PostEvent(this, std::string BY VALUE,
std::string BY VALUE, void* obj, Vector3* pos, int turn, const char* img,
int act), 0x008862b0, RET 0x4c. 161 call sites in 110 functions: this is the
whole simulation's event API, not a research helper. Dedup is per turn bucket on
message/image/location/position/action but NOT summary; act 0 with no subject and
no position is stored as 2; EvNxID starts at 0 and is promoted to 1 on first post;
PruneOldTurns drops buckets older than turn-50 with an off-by-one that always
leaves one stale bucket behind.
Record: 0x74 bytes, EvEID(+4) EvDsc(+8) EvMsg(+0x24) EvLoc(+0x40) EvPos(+0x44)
EvImg(+0x50) EvAct(+0x6c) EvCID(+0x70) -- confirmed field by field against
turn3-state.sav, which contains the EVENT_RESEARCH_OVERBUDGET record itself.
B3's defect is fully explained: 0x00587b97, reached only from the
completion-roll-FAILED branch, under !wasDone && nowDone && owner.
Three note corrections: EvPos defaults to FLT_MAX, not infinity (writing +inf
changes the save bytes); the save array is turn-bucketed, not flat;
EVENT_TECHS_UNLOCKED has no 'parent researched' clause.
56 entries added to ghidra/addresses.json (header regenerated); 11 prototypes,
13 labels, 12 comments and 2 structs written back to the Ghidra project.