diff --git a/findings/subsystems/formula-gaps.md b/findings/subsystems/formula-gaps.md index 8c692e3..8bc5d74 100644 --- a/findings/subsystems/formula-gaps.md +++ b/findings/subsystems/formula-gaps.md @@ -113,9 +113,17 @@ Also note the speed is per **segment** (closest approach of the whole chord), no ## Q8. Does `DecayAllResearch` also hit the current target? -Yes. `TechTree::ProcessResearch` 0x005876c0, loop 0x00587c20–0x00587c90: for **every** node with `state == 2 && -progress != 0`: `progress = max(0, progress − ftol(Cost(node) × 0.05))`. The just-funded node is not excluded; it only -escapes by completing (state 4) in the same call. Net gain of the current tech per turn is `spend − 5 %·cost`. +**No — corrected 2026-09-08 by the B3 live trace.** The loop reading is right: `TechTree::ProcessResearch` +0x005876c0, loop 0x00587c20–0x00587c90, for every node with `state == 2 && progress != 0`, +`progress = max(0, progress − ftol(Cost(node) × 0.05f))`. What was wrong is the assumption that the +funded node is in state 2. **The selected research target carries state 3**, so the equality test skips +it and the current tech keeps its whole gain; only *idle* partially-researched techs decay. Observed +directly in `b3-trace-golden.jsonl` / `b3-compare.jsonl`: the funded nodes (144, 142, 9) are state 3 +before and after, and one traced tree's states are 164×0, 7×1, 23×2, 1×3, 22×4. Net gain of the +current tech per turn is therefore `spend`, not `spend − 5 %·cost`. + +(State 3 is presumably "available and selected"; nothing in the traced runs had a state-2 node with +non-zero progress, so the decay branch itself is still unexercised behaviourally.) ## Extras resolved on the way @@ -166,3 +174,24 @@ below is now in `ghidra/addresses.json` (status `verified`) and folded into created without `FPU_PRESERVE` would leave 24-bit precision). It changes only the last bit of a draw (0.094 % of words) and of the odds. The B3 shim records the control word with every call, so the first trace settles it. + + +## B3 live verification (2026-09-08) — three more facts + +From the `ProcessResearch` trace/compare/replace runs (engine repo `docs/B3.md`; artefacts +`/srv/re-lab/shim/traces/b3-*`): + +* **x87 precision control is 53-bit.** `fnstcw` inside the hooked call returns `0x127f` (PC = 10b = + double, RC = nearest; bit 12 is the legacy infinity-control flag). At DLL init it is `0x027f`. + So the FPU is *not* left in single precision by the D3D9 device, and the `NextFloat` product + rounds to double before the caller narrows it to float32. The 24-bit contingency is moot. +* **`ServerPlayer::OnTechResearched` can consume an RNG draw.** Two techs completed during the + compare run; one cascade consumed no word and the other consumed exactly one more than the + research arithmetic accounts for. The draw is inside the owner's tech-effect callback, not in + `SetResearched` itself. Relevant to the B2 lane: at least one strategic effect rolls. +* **`EVENT_RESEARCH_OVERBUDGET` is raised in the same branch that sets `node.flag = 2`**, with + `EvDsc "Research Over Budget"`, `EvMsg "Research for has gone overbudget."`, + `EvImg "EVENT_RESEARCH_OVERBUDGET"`, `EvAct 1`, `EvPos {inf,inf,inf}`, and it bumps the player's + `EvNxID`. A replace-mode run that sets only the flag differs from the oracle by exactly this one + event and nothing else in 40,300 save items. +* Tech-tree size in this game: **293 nodes** per player tree. diff --git a/findings/subsystems/strategic-turn-internals.md b/findings/subsystems/strategic-turn-internals.md index f0700c7..7e851e2 100644 --- a/findings/subsystems/strategic-turn-internals.md +++ b/findings/subsystems/strategic-turn-internals.md @@ -192,7 +192,7 @@ not "bTurnDone". TechTree object: `+0xc` owner `ServerPlayer*`, `+0x10/+0x14` `vector` indexed by tech id, `+0x20` research order counter. Node (0x34 B, ctor in 0x005822d0): `[0] TechDef*`, `+4..+0xc vector children`, -`+0x14 state` (0 hidden, 1 parent researched, 2 available, 4 researched), `+0x18 costRP` (0x7fffffff = none; +`+0x14 state` (0 hidden, 1 parent researched, 2 available, **3 available *and selected* as the current research target**, 4 researched — state 3 confirmed by the B3 live trace and it is why the decay loop, which tests `== 2`, skips the funded node), `+0x18 costRP` (0x7fffffff = none; min over researched parents' edge cost), `+0x1c progress`, `+0x20 turnAvailable`, `+0x24 turnResearched`, `+0x28 order`, `+0x2c flag` (1 default, 0 "completed early", 2 "over-budget notified"). Edge: `+0x1c RP cost`, `+0x20 + 4×species` float availability (0..1), `+0x40 child TechDef*`. @@ -229,6 +229,7 @@ for each {node, points} in alloc: // one entry: the current Re SetResearched(node.def, 2) } for each node with state==2 && progress!=0: progress = max(0, progress - ftol(Cost(node)*0.05f)) // 0x009e5060 = (double)0.05f + // NB the funded node is state 3, so it is NOT in this sweep (B3 live trace; formula-gaps Q8 corrected) for each node with state==2 && turnAvailable == currentTurn && parent researched → EVENT_TECHS_UNLOCKED (names list) ``` Net effect: the current tech gains `spend − 5 %·cost`; every other partially researched tech decays 5 % of its cost per turn. diff --git a/ghidra/addresses.json b/ghidra/addresses.json index d0159c4..29d0a19 100644 --- a/ghidra/addresses.json +++ b/ghidra/addresses.json @@ -990,9 +990,9 @@ "name": "TechNode_off_State", "offset": "0x14", "convention": "offset", - "prototype": "int state (0 hidden, 1 parent researched, 2 available, 4 researched); ctor writes 0", + "prototype": "int state (0 hidden, 1 parent researched, 2 available, 3 available AND selected as the current research target, 4 researched); ctor writes 0. The decay sweep tests == 2, so a state-3 node never decays -- confirmed by the B3 live trace", "status": "verified", - "source": "B3 own disassembly pass 2026-09-08 (objdump/pei-i386): 0x005876c0, 0x0047d830, 0x00426e00, 0x0049fdf0, 0x004271c0, 0x0057da00, 0x008914a5" + "source": "B3 own disassembly pass 2026-09-08 (objdump/pei-i386): 0x005876c0, 0x0047d830, 0x00426e00, 0x0049fdf0, 0x004271c0, 0x0057da00, 0x008914a5 + B3 live trace 2026-09-08" }, { "name": "TechNode_off_CostRP", @@ -1691,4 +1691,4 @@ "source": "B2 own disassembly pass 2026-09-08 (ReVa read-memory + objdump -b binary -m i386): ServerPlayer::OnTechResearched 0x00891790, RebuildSpeciesTechFlags 0x0082bf10, ApplyAITechBonus 0x0080e330, SetAIBenefit 0x008186b0, UpdateNodeBoreParams 0x008182c0, SelectNodeBoreParams 0x006e18e0, GetPlagueCureMask 0x00537240" } ] -} \ No newline at end of file +}