From e1735eea7ec0412a60b56e01f6e426756cc6d82b Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 8 Sep 2026 10:41:54 -0400 Subject: [PATCH] Z: per-call-site ledger -- every word of a turn attributed, nothing left over Seven entry points detoured, each call keyed by __builtin_return_address(0) with the word cost from left before/after. Three consecutive turns on ref-turn2: site sums 19/18/20 against independently measured ProcessTurn totals of 19/18/20, residual 0 every time. The 18-20 spread is now explained rather than reported -- it is the two gated research draws. The dominant consumer is FUN_00893290: two Chance calls per player across all eight player-vector entries, 16 of every turn's 18-20 words, and it is NOT one of lane I's 22 sites. Lane I said its closure covered direct edges only and that indirect reachability was unsettled; this is that gap, measured. The function is unidentified and is the highest-value target left. Two bookkeeping corrections are in the report tool, not the shim: helper- internal rows (Chance's own NextFloat) double-count, and 8 calls per turn are on the StrategyClient's generator, not the strategic one. The first build did not distinguish generators and reported 44 words against a bracket of 18 -- which is what caught it. A per-site ledger that cannot say which generator a draw came from is not a ledger. --- findings/control-flow/tail-rng-ledger.md | 75 ++++++++++++++++++++++ tools/rng_site_report.py | 78 +++++++++++++++++++++++ verify/traces/tailrng-drawsites.jsonl.gz | Bin 0 -> 12210 bytes 3 files changed, 153 insertions(+) create mode 100755 tools/rng_site_report.py create mode 100644 verify/traces/tailrng-drawsites.jsonl.gz diff --git a/findings/control-flow/tail-rng-ledger.md b/findings/control-flow/tail-rng-ledger.md index 95879be..02d8417 100644 --- a/findings/control-flow/tail-rng-ledger.md +++ b/findings/control-flow/tail-rng-ledger.md @@ -488,3 +488,78 @@ and unknown". occurred. It is still one observation. * **No `EVENT_*` or state-side check was made.** These hooks declare the generator and nothing else, so this says the battle was RNG-free and says nothing about whether it was *computed* correctly. + +--- + +## 11. The per-call-site ledger — every word of a turn, attributed + +§2 said *where* a turn's words are spent (all inside `ProcessTurn`); this says *which call site* spends +them. The seven generator entry points are detoured and each call records +`__builtin_return_address(0)` — the game instruction after its own `call` — with the word cost taken from +`left` before and after (`sots-engine/src/shim/hooks/draw_sites.{h,cpp}`; report tool +`tools/rng_site_report.py`). This is attribution, not discovery: lane I closed the search space at seven +entry points and 22 sites in `ProcessTurn`'s closure. + +Three consecutive End Turns on `ref-turn2`, build `z-sites2-20260908T1432Z`: + +| call site | owner | entry point | calls/turn | words/turn | +|---|---|---|---|---| +| 0x0050329d | `FUN_00503200`+0x9d ← `DetectEncounters` (lane I, depth 4) | `NextFloat` | 1 | 1 | +| 0x007929a4 | `FUN_00792750`+0x254 ← `DetectEncounters` (lane I, depth 3) | `NextInt` | 1 | 1 | +| 0x00587888 | `TechTree::ProcessResearch`+0x1c8 | `NextFloat` | 0–1 | 0–1 | +| 0x0088df4f | `ServerPlayer::RollResearchEvent`+0x2f (lane T) | `NextFloat` | 0–1 | 0–1 | +| **0x00893426** | **`FUN_00893290`+0x196** | **`Chance`** | **8** | **8** | +| **0x00893513** | **`FUN_00893290`+0x283** | **`Chance`** | **8** | **8** | + +| turn | site sum | `ProcessTurn`, measured independently | residual | +|---|---|---|---| +| 3 | **19** | 19 | **0** | +| 4 | **18** | 18 | **0** | +| 5 | **20** | 20 | **0** | + +**Nothing is unattributed, on any of the three turns.** The two instruments share no code path — one reads +`left` around a boundary and reconstructs an absolute position, the other reads `left` around a single call +and keys on a return address — and they agree word for word. The 18–20 spread that §2 could only report is +now *explained*: it is the two optional research draws, both of which are gated. + +### 11.1 The dominant consumer of a turn is not in the static inventory + +**`FUN_00893290` spends 16 of every turn's 18–20 words** — two `Chance` calls per player across all eight +entries of the server's player vector — and it is **not one of lane I's 22 sites**. Lane I stated plainly +that its closure covers **direct** call edges only and that indirect-call reachability was unsettled. This +is that gap, measured: the single largest RNG consumer in a strategic turn is invisible to a direct-call +sweep from `StrategyServer::ProcessTurn`. + +That is not a criticism of lane I's inventory — its recall claim is about the *tempering-immediate scan*, +which is complete, and its 22-site list is explicitly a direct-edge closure. It is the demonstration that +the caveat mattered. **`FUN_00893290` is unidentified as of this writing** and is the highest-value target +left: it decides something twice per player per turn, on a coin the game bothers to flip. + +### 11.2 Two bookkeeping corrections the raw numbers need + +The shim's raw totals are 35 / 34 / 36, not 19 / 18 / 20, and both differences are accounting rather than +measurement — `tools/rng_site_report.py` applies them and shows its working: + +* **Helper-internal rows double-count.** `Chance` calls `NextFloat` internally, so its 16 words appear + twice: once on the `Chance` rows and once on a row whose return address (0x008e6e04) is *inside* + `Chance`'s own body. The report subtracts any row landing inside another entry point's body. +* **Other generators are not this generator.** 8 calls per turn come from `FUN_00578cf0`, `FUN_005798e0` + and `FUN_0069dbb0` drawing on a **different `Mars::RNG` instance** — the `StrategyClient`'s at + `client+0x134` (§7). They are real draws and they are correctly excluded: they never touch the strategic + generator the save serialises. The first version of this instrument did not distinguish them and reported + 44 words against a bracket of 18, which is what caught it. + +The second point is worth keeping: **a per-site RNG ledger that does not identify which generator each draw +came from is not a ledger.** The boundary instrument was immune to this by construction because it watches +one object; the site instrument had to be told. + +### 11.3 What the site ledger did not see + +* **`EncounterDetect_AssignContacts` never ran** on these three turns — its gate in + `EncounterDetect_ProcessTeamRecord` (some team-record member must have `+0xfc != 0`) was not satisfied. + Its hook recorded no call, so lane I's one inlined site in `ProcessTurn`'s closure contributed **0**, + which is consistent with the sums reconciling exactly. **The inlined-draw path is therefore still + unexercised**, and if it fires on some other save the site sum will fall short of the bracket by exactly + its cost — which is how it will announce itself. +* Three turns of one save. The two `Chance` sites fired 8/8 every turn with no variation, so nothing here + says what makes them fire *fewer* times, and `FUN_00893290`'s own gating is unmeasured. diff --git a/tools/rng_site_report.py b/tools/rng_site_report.py new file mode 100755 index 0000000..d18798d --- /dev/null +++ b/tools/rng_site_report.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Per-call-site RNG ledger from a lane-Z trace with the draw-site detours enabled. + +The boundary ledger (tools/rng_ledger_report.py) says how many words a turn spent and in which +phase. This says which CALL SITE spent them: the seven generator entry points are detoured and each +call records __builtin_return_address(0) -- the game instruction after its own `call` -- with the +word cost taken from `left` before and after. + +Two corrections are applied here rather than in the shim, because both are bookkeeping: + + * rows whose return address falls INSIDE another entry point's body are the helper's own internal + draw (Chance calls NextFloat; IntRangeBell calls NextInt twice). Those words are already counted + against the helper's row, so including them double-counts. + * rows tagged `strategic: false` came from a different Mars::RNG instance (the StrategyClient's at + +0x134, the tactical CombatSim's, a map-generation temporary). They do not touch the save's + strategic generator and must not be added to the turn's total. + +The sum of the remaining rows must equal the bracket total the boundary ledger measured +independently. A shortfall is an unattributed word. + + uv run python3 tools/rng_site_report.py +""" +import json, sys, bisect, os + +# entry-point bodies (RVAs), for the helper-internal test +BODIES = {"Chance": (0x4E6DD0, 0x4E6E2E), "FloatRange": (0x7D8A0, 0x7D8C9), + "IntRangeBell": (0x4E6D80, 0x4E6DC6), "GaussianRange": (0x4E6E30, 0x4E6FCA)} + +def internal_of(rva): + for name, (lo, hi) in BODIES.items(): + if lo <= rva <= hi: + return name + return None + +fns, addrs = {}, [] +fp = os.path.expanduser("~/sots-re/dumps/functions.json") +if os.path.exists(fp): + fns = json.load(open(fp)) + addrs = sorted(int(k, 16) for k in fns) + +def owner(va): + if not addrs: + return "" + i = bisect.bisect_right(addrs, va) - 1 + if i < 0: + return "" + a = addrs[i] + name, _ = fns[f"0x{a:08x}"] + return f"{name}+0x{va - a:x}" + +for line in open(sys.argv[1]): + d = json.loads(line) + if "hook" not in d or not d["hook"].endswith("Autosave"): + continue + a = {x.get("n"): x for x in d.get("args", []) if x.get("n")} + if a.get("end_turn", {}).get("v") is not False: + continue + rows = [r["v"] for r in a.get("draw_sites", {}).get("v", [])] + print(f"turn: strategic {a.get('draw_site_words',{}).get('v')} words / " + f"{a.get('draw_site_calls',{}).get('v')} calls; " + f"other generators {a.get('draw_site_words_other_rng',{}).get('v')} words / " + f"{a.get('draw_site_calls_other_rng',{}).get('v')} calls; " + f"overflow {a.get('draw_site_overflow',{}).get('v')}") + net = 0 + for v in sorted(rows, key=lambda r: (not r["strategic"]["v"], r["ret_rva"]["v"])): + rva = v["ret_rva"]["v"] + ins = internal_of(rva) + strat = v["strategic"]["v"] + counted = strat and not ins + if counted: + net += v["words"]["v"] + call = rva - 5 # every entry point here is reached by a 5-byte call rel32 + tag = "STRAT" if strat else "other" + note = f" [internal to {ins}: already counted on that row]" if ins else "" + print(f" {'*' if counted else ' '} {tag} {v['entry']['v']:<13} " + f"call 0x{call+0x400000:08x} {owner(call + 0x400000):<34} " + f"calls={v['calls']['v']:>3} words={v['words']['v']:>3}{note}") + print(f" => attributed strategic words: {net}") diff --git a/verify/traces/tailrng-drawsites.jsonl.gz b/verify/traces/tailrng-drawsites.jsonl.gz new file mode 100644 index 0000000000000000000000000000000000000000..0b89937231f138ed759cf7b4d889d900695efdc8 GIT binary patch literal 12210 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 AbpQYW literal 0 HcmV?d00001