b4 live artifacts; two-base StrategyServer hazard in the contract
This commit is contained in:
parent
b878c4c48f
commit
ff67ec0d9a
4 changed files with 36 additions and 14 deletions
|
|
@ -63,7 +63,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail |
|
||||
| hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants |
|
||||
| section-loader compare crash | verify | backlog | — | 0% | 2026-09-08 | SectionDictionary compare crashes the engine while the identical weapon path succeeds -> fault is in re-running LoadSection, not the manifest reader. Next boundary: hook LoadSection itself. docs/M2.md has 3 ranked leads |
|
||||
| P2-B4 colony + movement (behavioral) | phase2 | code-complete | med | 70% | 2026-09-08 | CODE-COMPLETE (engine 89c0013), dist-b4 staged, ctest 30/30. Verified 3 signatures (ProcessTurn takes NO stack arg - Ghidra's 2nd param was fiction, would have crashed). 22 corrections incl: population growth has NO pop/capacity term (base is a suitability ratio through pow()) - our model was wrong in SHAPE; range margin is +0.05f not -0.05 (fleets stopped 0.1 short); failed jump SCATTERS around the destination (2nd draw); AccrueSystemBonus gate is ntdev not rbtn; rounding is fistp ties-to-even; only ProcessRebellion draws RNG in a colony turn. NO replace/oracle this milestone (input boundary, deliberate). UNTESTED sub-paths: plague, rebellion, slaves, terraform, addiction, unowned decay, jumps, gate teleports, node-line travel, stranded fleets | AUDIT: `ours` re-reads LIVE post-original fleet state (breaks the compare invariant, verdict partly self-fulfilling); morale events computed then DROPPED (DescribeMoraleEvents never called); PassSchedule/DescribeGateTraffic/DescribeFleetStep are DEAD CODE so the 'predicts call order' claim is unimplemented. Fix before trusting its compare |
|
||||
| P2-B4 colony + movement (behavioral) | phase2 | verified | high | 100% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z |
|
||||
| harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md |
|
||||
| RollResearchEvent draw (behavioural) | verify | backlog | — | 0% | 2026-09-08 | ServerPlayer::RollResearchEvent draws one NextFloat when the completing def is the current target and +0x3b4 is set. Region declared, ours reproduces it, but the branch never fired on ref-turn2 - needs a workload that sets research_roll_pending |
|
||||
| golden-trace recapture (post-guards) | verify | backlog | — | 0% | 2026-09-08 | region sets changed, so goldens must be recaptured: ProcessResearch (expect NEW divergences - the event defect becoming visible), ComputeBudget (verdict should stay 0), and first captures for OnTechResearched / ServerSystem::ProcessTurn / MoveFleet |
|
||||
|
|
|
|||
|
|
@ -330,3 +330,25 @@ into the Ghidra project.
|
|||
**any** negative value, with the result clamped into `[0, adjustedSlaveCount]`.
|
||||
* **`HazardMod`'s 0.1 (0x00a1a438) is a true double**, not `(double)0.1f`, and a zero band at the
|
||||
ideal returns NaN (which the capacity's `max(v,0)` then turns into 0).
|
||||
|
||||
### B4 LIVE (2026-09-08) — what the game confirmed, and three offset traps
|
||||
|
||||
36 calls compared, 0 divergences. Confirmations:
|
||||
* **`ProcessRebellion` really is the only RNG consumer in a colony turn.** Over 28 systems the
|
||||
generator's `left` did not move and the `mt[624]` hash was identical before and after, on
|
||||
every single record. The static sweep was right.
|
||||
* `fpu_cw = 0x127f` (53-bit precision) on every colony record.
|
||||
* `ServerSystem::ProcessTurn` really does take no arguments — hooked as a one-argument
|
||||
`__thiscall` for 28 calls per turn across three full runs with no corruption.
|
||||
* The movement step reproduced bit for bit on the one fleet that moved, and the pass schedule
|
||||
matched `PlanFleetMovement`'s prediction exactly (no pursuits in this save).
|
||||
|
||||
**Offset traps, all three found live and all three now fixed in the contract:**
|
||||
1. **The StrategyServer has two bases four bytes apart.** `ServerSystem+0x10` (`owner`) points
|
||||
at a base four bytes ABOVE the one the class's own methods get in ECX — 0x007437f0 does
|
||||
`owner - 4` before reading the generator. Every `StrategyServer_off_*` entry here is relative
|
||||
to the RAW (owner) base; a hook whose `this` is the method base must add 4 first.
|
||||
2. `StrategyServer_off_Fleets` is **0x60** relative to the raw base (Ghidra `+0x64`). The
|
||||
earlier 0x64 made a hook enumerate the vector's spare capacity: `ProcessFleetMovement`
|
||||
reported 1 fleet in a turn where `MoveFleet` was called 7 times.
|
||||
3. `off_EntityHash` 0x80, `off_ArrivedSet` 0x200, `off_InMotionSet` 0x210 — same −4 correction.
|
||||
|
|
|
|||
|
|
@ -2286,21 +2286,21 @@
|
|||
"name": "StrategyServer_off_Players",
|
||||
"offset": "0x00000050",
|
||||
"convention": "offset",
|
||||
"prototype": "std::vector<ServerPlayer*> (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2",
|
||||
"prototype": "std::vector<ServerPlayer*> (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2 /* TWO BASES: every StrategyServer_off_* here is relative to the RAW base a ServerSystem's owner word (+0x10) points at. The class's own methods receive a base FOUR BYTES LOWER in ECX (0x007437f0 does owner-4), so a hook on MoveFleet or ProcessFleetMovement must add 4 to `this` before applying these */",
|
||||
"status": "verified",
|
||||
"source": "handoff/b4-colony-movement.md"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_off_Fleets",
|
||||
"offset": "0x00000064",
|
||||
"offset": "0x00000060",
|
||||
"convention": "offset",
|
||||
"prototype": "std::vector<StarFleet*> (begin @+0x64, end @+0x68) -- one flat global list, not per player",
|
||||
"prototype": "std::vector<StarFleet*> (begin @+0x60, end @+0x64) -- ALL fleets of ALL players, one flat global list. B4 live: the earlier 0x64 was the Ghidra-base number transcribed as a raw-base one; it made ProcessFleetMovement enumerate the vector's spare capacity instead of its elements",
|
||||
"status": "verified",
|
||||
"source": "handoff/b4-colony-movement.md"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_off_EntityHash",
|
||||
"offset": "0x00000084",
|
||||
"offset": "0x00000080",
|
||||
"convention": "offset",
|
||||
"prototype": "16-bucket hash of entity id -> object, hashed by (key & 0xF); 0x008b9240 is the lookup",
|
||||
"status": "verified",
|
||||
|
|
@ -2316,7 +2316,7 @@
|
|||
},
|
||||
{
|
||||
"name": "StrategyServer_off_ArrivedSet",
|
||||
"offset": "0x00000204",
|
||||
"offset": "0x00000200",
|
||||
"convention": "offset",
|
||||
"prototype": "std::set<pair<FleetId,LocationId>> cleared at the head of ProcessFleetMovement",
|
||||
"status": "verified",
|
||||
|
|
@ -2324,7 +2324,7 @@
|
|||
},
|
||||
{
|
||||
"name": "StrategyServer_off_InMotionSet",
|
||||
"offset": "0x00000214",
|
||||
"offset": "0x00000210",
|
||||
"convention": "offset",
|
||||
"prototype": "std::set<pair<FleetId,LocationId>> that MoveFleet fills for fleets still in motion; OnFleetArrived takes the set difference",
|
||||
"status": "verified",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 5b6f812, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ b878c4c, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -577,18 +577,18 @@ constexpr uint32_t ServerSystem_off_ntdev = 0x000002c4;
|
|||
constexpr uint32_t ServerSystem_off_rbtn = 0x000002cc;
|
||||
// offset int ModCount (the turn counter), relative to the RAW server base a ServerSystem's +0x10 points at [verified]
|
||||
constexpr uint32_t StrategyServer_off_ModCount = 0x00000008;
|
||||
// offset std::vector<ServerPlayer*> (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2 [verified]
|
||||
// offset std::vector<ServerPlayer*> (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2 /* TWO BASES: every StrategyServer_off_* here is relative to the RAW base a ServerSystem's owner word (+0x10) points at. The class's own methods receive a base FOUR BYTES LOWER in ECX (0x007437f0 does owner-4), so a hook on MoveFleet or ProcessFleetMovement must add 4 to `this` before applying these */ [verified]
|
||||
constexpr uint32_t StrategyServer_off_Players = 0x00000050;
|
||||
// offset std::vector<StarFleet*> (begin @+0x64, end @+0x68) -- one flat global list, not per player [verified]
|
||||
constexpr uint32_t StrategyServer_off_Fleets = 0x00000064;
|
||||
// offset std::vector<StarFleet*> (begin @+0x60, end @+0x64) -- ALL fleets of ALL players, one flat global list. B4 live: the earlier 0x64 was the Ghidra-base number transcribed as a raw-base one; it made ProcessFleetMovement enumerate the vector's spare capacity instead of its elements [verified]
|
||||
constexpr uint32_t StrategyServer_off_Fleets = 0x00000060;
|
||||
// offset 16-bucket hash of entity id -> object, hashed by (key & 0xF); 0x008b9240 is the lookup [verified]
|
||||
constexpr uint32_t StrategyServer_off_EntityHash = 0x00000084;
|
||||
constexpr uint32_t StrategyServer_off_EntityHash = 0x00000080;
|
||||
// offset Mars::RNG* relative to the RAW base (== the -4-adjusted base's +0x16c) [verified]
|
||||
constexpr uint32_t StrategyServer_off_RNGPtr = 0x00000168;
|
||||
// offset std::set<pair<FleetId,LocationId>> cleared at the head of ProcessFleetMovement [verified]
|
||||
constexpr uint32_t StrategyServer_off_ArrivedSet = 0x00000204;
|
||||
constexpr uint32_t StrategyServer_off_ArrivedSet = 0x00000200;
|
||||
// offset std::set<pair<FleetId,LocationId>> that MoveFleet fills for fleets still in motion; OnFleetArrived takes the set difference [verified]
|
||||
constexpr uint32_t StrategyServer_off_InMotionSet = 0x00000214;
|
||||
constexpr uint32_t StrategyServer_off_InMotionSet = 0x00000210;
|
||||
// offset int id (also the entity-hash key) [verified]
|
||||
constexpr uint32_t StarFleet_off_Id = 0x00000004;
|
||||
// offset the object whose +0x80 holds the entity hash the waypoint target is resolved in [verified]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue