30 KiB
B4 — the colony turn and the fleet movement pass, old vs new
Result (2026-09-08): verified on the live game. 36 compared, 0 divergences.
| pass | build | records | verdict |
|---|---|---|---|
scout (b4scout) |
b4-fix1-…0600Z |
28 colony + 1 movement | tracecmp.py exit 0, 0 invalid |
trace (b4trace) |
b4-fix1-…0600Z |
28 colony + 7 MoveFleet + 1 pass |
exit 0, 0 invalid |
compare (b4compare) |
b4-fix2-…0615Z |
36 calls, all 36 compared | 0 diverged, 0 errors, exit 0 |
The headline is the scout's, and it is what the milestone was for: the generator did not move
on a single one of the 28 systems — left delta 0 and the mt[624] block hash identical
before and after, on every record — which is exactly what the static sweep predicted (only
ProcessRebellion draws, and no colony rebelled this turn). fpu_cw = 0x127f (53-bit) on all
28, confirming the two earlier lanes rather than re-investigating.
The one fleet that actually moved reproduced bit for bit: fleet 34 went
(-11.9286022, 4.71900415, 2.31785131) -> (-10.5634995, 5.97172451, 1.56473362) and our
float32 position is identical in all three components; its ship's range went 20 -> 18, i.e.
exactly speed 2 x dt 1.0. sim::PlanFleetMovement's predicted call schedule matched the
observed one exactly (all seven fleets, dt = 1.0, in fleet-vector order — no pursuits in this
save).
Three bugs in my own hooks were caught by the runs, all of which would have produced a clean
compare that checked nothing (see "Three hook bugs the live runs caught"). Host suite 30/30,
tools/clean_room_check.sh OK. Ghidra was used to write the verified prototypes back into the
shared project (reva-server stopped for the run and restarted afterwards). The VM is released
at the main menu with hooks=trace and build b4-fix2-20260908T0615Z deployed.
What was hooked
hook name (record hook) |
RVA | prototype (all now [verified]) |
|---|---|---|
Game::ServerSystem::ProcessTurn |
0x003598e0 | void (ServerSystem*) — no stack arguments |
Game::StrategyServer::MoveFleet |
0x003d9ee0 | bool (StrategyServer*, StarFleet*, float dt) |
Game::StrategyServer::ProcessFleetMovement |
0x003da9a0 | void (StrategyServer*) |
All three are __thiscall and go through Hook<> with CallConv::Thiscall. Sources:
src/shim/hooks/colony_turn.{h,cpp} and src/shim/hooks/fleet_movement.{h,cpp}, installed
from src/shim/main.cpp after the B1/B2/B3 hooks. The pure halves are
src/shim/hooks/colony_inputs.{h,cpp} (lib shim_colony, ctest shim_colony_unit) and
src/shim/hooks/movement_inputs.{h,cpp} (lib shim_movement, ctest shim_movement_unit) —
the B1 split, so the mapping is exhaustively testable without the VM.
How the prototypes were verified
Every one was read off the instruction stream (objdump -d over the shipped exe) before it was
hooked, because M0's lesson is that a wrong thiscall prototype crashes the game:
ServerSystem::ProcessTurnends in a plainretand nothing in the body reads[ebp+8]. The existing decompile shows a second parametervoid* stream; that is a Ghidra guess and it is wrong. Hooking it as a one-argument function would have corrupted the stack.MoveFleetends inret 8;[ebp+8]goes into ESI as the fleet and[ebp+0xc]is read withfld DWORD— a 4-byte float. Each of the five call sites pushes itsdtwithpush ecx; fstp DWORD PTR [esp]. It returnsAL, and the caller tests it.ProcessFleetMovementends in a plainretwithmov esi,ecxand no stack reads.
Three hook bugs the live runs caught
None would have shown up as a divergence. Each would have made a compare that reported "0 diverged" while checking less than it claimed — the same class of failure the harness audit lane is chasing, found the same way B3 found its own: by looking at the trace rather than at the verdict.
describe_argsruns beforeregions(). The template calls them in that order and both descriptors took their snapshot inregions(), so every argument sourced from it was one call stale — the first record showed zeros, the rest showed the previous system's state. Theinputsregion was always right (its "before" is captured afterregions()returns), so the compare would have been sound while the trace's own argument record lied. Fixed: capture indescribe_args, reuse inregions().- The StrategyServer has two bases four bytes apart. A
ServerSystem'sownerword (+0x10) points at a base four bytes above the one the class's own methods receive in ECX (the generator accessor at 0x007437f0 doesowner - 4). EveryStrategyServer_off_*in the contract is relative to the raw/owner base, so the colony hook — which starts fromsys->owner— was right, and the two movement hooks, which applied the same offsets tothis, were not.ProcessFleetMovementread an empty player vector, declared zero gate-traffic regions, and reportedplayers=0 fleets=0; tracecmp dutifully printed "1 call, 0 diverged" for a hook that compared nothing. Fixed with an explicitraw_server()rebase. StrategyServer_off_Fleetswas the Ghidra-base number written down as a raw-base one. With0x64instead of0x60the hook enumerated the fleet vector's spare capacity rather than its elements: the trace pass showedProcessFleetMovementreporting 1 fleet whileMoveFleetwas called 7 times in the same turn. The gate-traffic totals came out all-zero and matched ours — correct by luck, from a garbage fleet list. Fixed to0x60; the compare pass then reported 7 fleets in exactly the orderMoveFleetwas called for them.
The contract entry for StrategyServer_off_Players now spells the two-base hazard out, and the
three corrected offsets carry the reason in their prototype text.
The declared input boundary — say it out loud
Both colony and movement targets are mostly dispatchers. Being honest about that is what makes the compare mean anything.
ServerSystem::ProcessTurn. The function body writes: the unowned infrastructure decay,
both pending bonus pools (through ApplyInfraBonus / ApplyPopBonus), ntdev, the
long-stability accrual, TRes = 0, haltv[0..2] = false, the two per-player countdown
sweeps, and the addiction morale events. Everything else is a callee. So the declared regions
are exactly
infra,ibon,pbon,ntdev,tres,haltv,bats2,bats_mask,rcex,rcex_mask,rng
and nothing else about the system is declared — population, morale, resources, plague,
slaves and rebellion state are simply not regions, so the harness never compares them. They
are recorded in the inputs region for the trace instead. Two live values our side asks the
game for rather than re-deriving: ServerSystem::IsStable (the stability verdict) and
ServerSystem::MaxPop (the imperial capacity the accrual reads) — both read-only, the same
delegation B3 makes to TechTree::Cost.
MoveFleet. Declared: pos, prev_pos, one region per ship's range, the generator,
and the return value. Not declared: the departure hook, the route revalidation, every arrival
handler, the waypoint list, the tanker top-up. A call that arrives is expected to differ in
all of that, and none of it is compared.
ProcessFleetMovement. Declared: each player's gate-traffic word. That is the one thing
our side can honestly reproduce, because the original computes it at the very end of the
pass from the post-move fleet state — which is exactly the state ours reads. The pass
schedule itself is recorded in the arguments (and sim::PlanFleetMovement predicts the call
order so a trace can be checked against it) but is not compared, because reproducing it would
mean running MoveFleet.
Neither MoveFleet nor ProcessTurn offers replace mode. Both say so in shim.log and
fall back to the original: our side models a slice, and feeding that slice to the game would
strand every arriving fleet or skip a colony's whole turn.
The RNG region, and why it has teeth here
A colony turn's RNG consumption was swept function by function to call depth one:
ProcessPlague, the civilian growth pass and ProcessSlaves make no draws at all, and
neither does ProcessTurn itself. ProcessRebellion is the only consumer, and its count
is data-dependent — one RandChance per iteration of a 64-bit rebel counter, plus a
short-circuiting per-species roll loop, plus one outcome roll.
So the generator is a declared region whose expected post-state on a system with no rebellion
is bit-identical, and any movement of it names the system whose rebellion fired. ours
seeds a mars::rng::MT19937 from the pre-call snapshot, consumes the draws its own pass makes
(currently none) and writes the state back, exactly as B3 does. next is rebuilt against the
live generator address so the describer's index arithmetic reads the same on both sides.
MoveFleet declares the generator too, because a type-5 waypoint draws from it: one word on
a successful jump, two on a miss (the second seeds the random scatter direction).
Corrections found by reading the binary
Twenty-two, none of them tuned to make anything match. The ones that change behaviour:
Movement
- The range grace margin is added, not subtracted. The notes said
range = MinRange(fleet) − 0.05. The constant at0x00a1d2c0is0x3d4ccccd—+0.05f, sign bit clear — andMinRangeadds its float argument (fadd DWORD PTR [ebp+8]). The correct expression isrange = float32(MinRange(fleet) + 0.05f): a grace fudge so a fleet exactly at its range limit can still reach the target. Our old code stopped fleets 0.1 short. - The out-of-range case zeroes the range, not the step. The original asks for the
minimum range a second time with no margin and, when that is exactly zero, does
fstp DWORD PTR [ebp+0x8]— the range slot.stepis untouched, andstepis later the divisor of the pass fraction, so zeroing it instead changes (or NaNs) the recursion. movehas no floor at zero.move = min(min(range, step), distance)in that order; a negative minimum range moves the fleet backwards. The floor at zero exists only on each ship's range in the fuel loop.MinRangeseeds its accumulator withFLT_MAX, so an empty fleet is unconstrained rather than stranded, and it skips no ship — a range-exempt tanker still clamps the fleet.- The probabilistic jump does not stop part-way along the vector. On a miss the fleet is
placed at
dest + randomUnitVector x vwherev = float32(roll x CstE)— scattered around the destination by exactlyv, at the cost of a second RNG draw. The jump succeeds iff!(v > CstT), so equality arrives, and the pass fraction is 1.0 either way, so a type-5 waypoint never recurses. - Only waypoint type 3 reports a partial pass fraction.
IsNodeWaypointis a 7-entry jump table that is true for 3 alone — and the node-line case of the movement switch is type 2, which it does not accept. Everything else reports a full pass unless the move was blocked, in which case the fraction isclamp01(move / step). - The recursion threshold is a widened float literal and the test is strict. The constant
at
0x00a261f0is an 8-byte double whose value is exactly(double)0.9999f= 0.9998999834060669;fraction == thresholddoes not recurse, and the recursivedtisfloat32((1 − fraction) x dt). - The stutter overlap rule is not a midpoint. When
seg[i].end > seg[i+1].startthe original sets both boundaries tofloat32(seg[i].end + 0.5 x (seg[i].end − seg[i+1].start))— the mirror of the midpoint aboutseg[i].end, pushing the boundary forward past both chords by half the overlap. Verified down to the ModRM byte, because the FSUB/FADD operand order is the whole claim. Nothing is dropped and nothing is clipped back, so a chord swallowed by its predecessor comes out inverted (start > end) and the step loop skips it. We reproduce it, bug and all. - Smaller ones in the same pass: the chord parameters are clamped to
[0, length]before the drop test (without which the intersect routine's±FLT_MAXsentinels would poison the list); the drop threshold isfabs(start − end) <= 0.01f(a float32 literal, and inclusive); the sort is a realstd::sortkeyed onstartalone, so ties are unordered. - The pass schedule is a pursuit model, not a "departing / in-transit / other" split. Every fleet whose current waypoint targets another fleet is classified by the relation between the two owners: relation 0 (no treaty) makes it a pursuer, anything else a follower. Prey move half a turn, then pursuers move half a turn and a pursuer that arrives retires itself and its prey from the rest of the schedule, then the surviving prey take their second half, then everything unscheduled takes a full turn (an uncaught pursuer gets a second half instead), then the followers take a full turn. A fleet that is only a follower's target is not prey and takes a normal full turn.
- Gate traffic is the sum of a signed int16 at
fleet+0xc0over fleets whose front waypoint type is 4 or 5, indexed by the owner's own index word, and it is assigned to each player rather than accumulated. Two things worth recording: the original accumulates byplayer->indexbut writes back by the player's position in the server's vector, which agree only whileplayers[j]->index == j; and the accumulator is a fixed 32 ints with no bounds check. - The field the notes called
FPogn2atfleet+0xecisFPdpos. By the FlightPlan layoutFPogn2is at+0xe0;+0xecis the destination position, which is what the pass writes. STUTTER_MIN_SPEED == STUTTER_MAX_SPEED == 0.33in the shipped data (radius 2), so the linear ramp collapses to a constant 0.33× inside any influence sphere and 1.0× outside. The ramp is still implemented because the constants are data-file tunable, but a run against shipped data cannot distinguish it from a binary in/out multiplier — worth knowing before reading a clean compare as evidence for the ramp.
Colony
- The population growth curve has no
pop / capacityterm. The notes hadg = clamp01((1 − clamp01(pop/cap))^EXP). The capacity is never passed into the growth chain at all; the base of the power is a suitability term:base = 1 − clamp01(min(|ideal − clamp(suit, 0, 20)|, SuitTol) / SuitTol), and the exponent is clamped into[0.01f, 1000]before a realpow(). Every modifier that follows is gated on a strict> 0and stored back to a float32. The 50,000,000 cap is not here either — it lives in the apply. - The over-cap shrink runs only when the colony was already over the cap, is computed from the old population, and a colony that merely grows past the cap simply lands on it.
- The second
SYSTEMBONUS_MINTURNSgate isntdev, notrbtn. Read off the twocmps at the head ofAccrueSystemBonus. OurSystemBonusInputsfield was namedturnsSinceRebellion; it isturnsDeveloping. - Both bonus-apply helpers reset
ntdevto zero on a colony that is not the owner's home system. That is a real feedback loop: a colony still absorbing a bonus never accumulates the developing turns the accrual gate wants, so it fails the gate on the same turn.ApplyInfraBonusalso snapsInfrato exactly 1.0f when the pool covers the whole remainder, and an unowned system drops its whole population pool. - The output-rate normaliser pins the trade slider. It takes a "pinned channel" argument
that every call site leaves null, which selects trade. Only trade is clamped into
[0, 1]; the other three are summed in float32 (trade excluded) and rescaled to1 − trade; the all-zero fallback is an equal split over three channels, not four. Our version rescaled all four symmetrically. - The engine's "round" is
fistp/fild— round to nearest, ties to EVEN. Not round-half-away-from-zero. 302.5 becomes 302. Andout[0],out[3]and the construction points go through the truncating helper, not the rounding one. - Every carrying capacity is rounded down to a multiple of ten by the shared helper, and
Size x 1e8is an exact 64-bit integer product (the 1e8 is the immediate0x05f5e100); only the modifier chain is floating point. The arcology bonus is 0 for slaves. - The terraforming modifier is inside the point count, not only the yield — a better
modifier needs proportionally fewer points, which is what keeps need and yield consistent —
the sign is
-1only forsuit > idealstrictly, and the apply clamps at the ideal from whichever side it approached.TerraformPointsNeededis aceil, not a truncation. - Smaller ones: the slave death rate folds the hazard term into the base before the output
term (order matters when every step narrows to float32), an unowned system reports 1.0
rather than 0, the worst plague at the system contributes an additive rate term, and
both
SLAVES_MIN/MAX_DEATHSare disabled by any negative value with the result clamped into[0, slaves].BuildQueue::ProcessTurnreturns the leftover points by value, a money refusal skips the order rather than stopping the pass, and removal is a separate sweep that unlinks every order at or below zero.
Float discipline
Every constant was checked bit by bit, because B2 and B3 were both bitten here.
| what | address | bits | value | kind |
|---|---|---|---|---|
| unowned infra decay | 0x009e9170 |
3f947ae140000000 |
0.019999999552965164 | widened 0.02f, not the decimal |
| range grace | 0x00a1d2c0 |
3d4ccccd |
+0.05000000074505806 | float32 0.05f, positive |
| recursion threshold | 0x00a261f0 |
3fefff2e40000000 |
0.9998999834060669 | double whose value is (double)0.9999f |
| stutter chord drop | 0x009e3e14 |
3c23d70a |
0.009999999776482582 | float32 0.01f |
| stutter overlap half | 0x009e20a0 |
3fe0000000000000 |
0.5 | true double |
hazard band +0.1 |
0x00a1a438 |
3fb999999999999a |
0.1 | true double, not (double)0.1f |
| output-rate threshold | 0x009e22c8 |
3f1a36e2e0000000 |
9.999999747378752e-05 | widened 1e-4f |
| infra divisor | 0x00a1f930 |
3f014d2f5dbb9cfa |
3.3e-05 | true double |
| infra chain | — | — | /500, x0.01, x1.65 |
0.01 and 1.65 are true doubles; the three steps are not folded |
| terraform yield | 0x009e62b8 |
3ff3333340000000 |
1.2000000476837158 | widened 1.2f |
| terraform need | 0x00a1f928 |
3ffccccce0000000 |
1.8000000715255737 | 1.5 x (double)1.2f |
| growth exponent floor | 0x009e3e14 |
3c23d70a |
0.01f | shared with the chord drop |
| slave mod base / step | 0x009e3030 / 0x009e20d8 |
3f4ccccd / 3fc99999a0000000 |
0.800000011920929 / 0.20000000298023224 | float32 0.8f / widened 0.2f |
numeric.h gained F32() (narrow-and-widen) and RoundHalfEven(); rng.h's IRandom gained
NextUInt32() for the raw word the jump scatter takes.
What our side runs
sots::sim::ProcessColonyTurn(new,game/sim/colony.{h,cpp}) — the dispatcher's own writes, in the original's order, plusColonyCountdowns/AddictionPhaseOfand the correctedApplyPopulationBonus/ApplyInfrastructureBonus/AccrueSystemBonus.sots::sim::StepFleet(the shim adapter) overResolveMoveStep,AdvanceAlongDirection,ConsumeShipRange,PassFractionandRollProbabilisticJump.sots::sim::GateTrafficTotalsandsots::sim::PlanFleetMovement.sots::sim::NodeLineStepandBuildStutterSegmentsare corrected and unit-tested but are not wired into the hook: the node-line step needs the node graph walked from the live server, which this milestone does not do. A node-line waypoint therefore records its type in the trace and is left to the original — a declared gap, not a silent one.
Host tests
ctest 30/30 (was 28). New coverage:
game_sim_colony— 200 checks. Rewritten around the corrected growth curve (the suitability base, the[0, 20]clamp, the exponent clamp), the ties-to-even rounding, the pinned-trade normaliser, the terraform modifier inside the point count, the infra/suit apply clamps, the additive plague rate and the both-ends slave clamp.game_sim_movement— 144 checks. The+0.05grace, the range-zeroing stranded case, the absence of a floor onmove, the empty-fleetFLT_MAX, the scatter semantics of a failed jump and its two draws, the type-3-only pass fraction, the strict recursion threshold, the forward-pushed overlap boundary (including the inverted segment it produces), the pass schedule with and without a catch, and the gate-traffic sum.shim_colony_unit— 62 checks. The snapshot round trip, the input mapping, a reference-save shaped colony turn, the bonus/accrual interaction with the non-homentdevreset, the addiction sweep across all four phases plus temperance, the unowned colony, and the countdown edges (index 15, the clamp, the skip-when-zero).shim_movement_unit— 56 checks. Straight steps, the exempt-tanker clamp, the arrival snap, the node-waypoint fraction, the gate teleport, both jump outcomes, a held fleet, the gate traffic and the schedule.
Gotchas
ServerSystem::ProcessTurntakes no arguments. The decompile in the RE handoff shows a second parameter. Trusting it would have pushed a garbage word and corrupted the stack — exactly M0's failure mode.MoveFleetrecurses into itself for a multi-waypoint leg.Hook<>handles the nesting (depth, call ids) and the inner call gets its own record, but the outer record's "after" snapshot includes everything the recursion did — read a record'sdepthbefore comparing two of them.- Per-call state lives in statics between
regions()→rebind()→ours()(M1's concession). Safe here because the turn pass is single-threaded, butMoveFleetdoes nest — the compare path is still correct because the template runsregions/rebind/oursfor one call before the original's recursion can start, but do not add state that has to survive the original's execution. Region::nameis aconst char*held for the whole call, so the per-ship and per-player name strings arereserved once up front; a reallocation would dangle every name already pushed.ProcessFleetMovement'soursre-reads the fleet list. The gate-traffic total is computed by the original after the passes, so comparing against a pre-call snapshot would diverge for the wrong reason.- A compare record for
ProcessTurnis small (twelve regions), but the generator's 2496-byte block is hashed rather than inlined — keeptrace.inline_maxat 256. MoveFleetincompareis the busiest of the three; on the reference save that is a handful of records, but on a large map it is every fleet times up to five passes times the recursion depth. Theb4scoutconfig exists for exactly that reason: it leavesMoveFleetoff.
What the runs actually exercised — and what they did not
A clean compare over 36 calls is worth exactly as much as the coverage behind it, so here is the coverage, region by region, from the compare log itself.
ServerSystem::ProcessTurn, 28 calls. Only 3 systems are owned (indices 4 and 15, the
two players' home worlds; index 16, an independent/NPC colony owned by player 7 and the only
non-home one). The other 25 are unowned.
| region | carried a value? | what that means |
|---|---|---|
ntdev |
yes, moved on 3 | the stable-increment path is verified on all three owned colonies |
rcex |
yes, moved on 6 | the recon countdown sweep is genuinely exercised, including a nibble reaching zero |
rcex_mask |
one system held a stale bit with a zero counter | the "skip the sweep when the word is zero" branch is verified |
infra |
matched on all 28, but never changed | owned colonies sit at 1.0 (the apply is a no-op) and unowned ones at 0.0, so the decay is only exercised in its clamp branch |
ibon / pbon |
matched, never changed | both home colonies are at their cap, so ApplyPopBonus short-circuits and ApplyInfraBonus returns at Infra >= 1; the accrual gate fails on ntdev |
tres / haltv |
matched at 0 / false on all 28 | zero branch only |
bats2 / bats_mask |
zero on all 28 | the battle countdown is completely untested |
rng |
matched on all 28, bit for bit | the real result: no colony drew a word |
MoveFleet, 7 calls. Six are fleets with no waypoints at all (wpt_type = -1), which
exercise only the early-out. One call does real work: fleet 34, waypoint type 1 (a straight
run) to a system, speed 2 x dt 1.0, and it matched on pos, prev_pos and its ship's range.
So the step arithmetic is verified for exactly one straight-line move. Not exercised at all:
the range clamp against a real limit (the ship had 20 range for a 2-unit move), the stranded
case, node-line travel (type 2), a node route (type 3), a gate teleport (type 4), a
probabilistic jump (type 5, and with it every RNG draw in the movement path), the multi-waypoint
recursion, and an arrival.
ProcessFleetMovement, 1 call. All eight gate_traffic words matched — but every one of
them is 0 before and 0 after. Three fleets carry non-zero traffic words (10, 12, 12…) yet
none has a waypoint, so nothing is summed. The gate-traffic total is verified in its zero
branch only. The pass schedule was checked separately against sim::PlanFleetMovement and
matched, but this save has no pursuits, so only the "everything else, dt 1.0" pass is covered.
Writes of the original that the declared regions do NOT cover
Stated explicitly, because a clean compare says nothing about any of it. This is the content of
the coverage block once HookPolicy carries the field (the harness audit lane owns that
struct change); it lives as a comment at the top of each descriptor header until then.
ServerSystem::ProcessTurn — state: partial
- the addiction sweep's morale events:
ourscomputes the{species, event id, delta}list, but nothing applies it, so the system'sMorale int[7]and its morale-event vector append are never written or compared. Risk: high — this is precisely B3's failure mode, a list append outside every declared region. Not exercised by this save either (no addiction). - the whole plague pass, imperial and civilian population growth,
AdjustResources, the in-orbit refuel,ProcessSlavesandProcessRebellion. Risk: high; no guard. - the independent-colony imperial↔civilian population drift (0x007514f0). Risk: medium.
TnsOH, the build queue's own list, and every ship the queue creates. Risk: medium.
MoveFleet — state: partial
- every arrival handler:
FleetArrives, theSEFleetArrivedevent object and its dispatch, the next-leg pathability check, and the insert into the server's in-motion set at +0x210. Risk: high — an event post and twostd::setinserts, none declared. - the departure block: the script hook at StrategyServer+0x1b4, per-ship action cancellation,
and
ServerSystem::FleetDeparts(which rewrites a system's fleet vector and its per-player presence bitmasks). Risk: high. - the waypoint vector itself — a completed leg pops its waypoint and a failed probabilistic jump rewrites the whole route. Risk: high.
- the tanker top-up and the fleet flag word at +0x10c. Risk: medium.
- the node-line step: our side computes
speed x dtfor every waypoint type, so a type-2 waypoint's step is wrong by construction. Risk: high; mitigated only bywpt_typebeing on the record — treat type 2 as not compared.
ProcessFleetMovement — state: partial
- the entire pass schedule and therefore every
MoveFleetcall it makes. Mitigation:sim::PlanFleetMovementpredicts the order and the trace was checked against it. - the per-fleet destination position at +0xec and the flag-0x2 / flag-0x100 clears. Medium.
OnFleetArrived: it takes the set difference of +0x200 and +0x210, clears both, and dispatches a per-player arrival event. Risk: high.
Runs (/srv/re-lab/shim/traces/)
| file | mode | build | calls | result |
|---|---|---|---|---|
b4-scout.jsonl |
trace | b4-fix1-…0600Z |
29 | exit 0, 0 invalid; RNG delta 0 on all 28 systems |
b4-trace-golden.jsonl |
trace | b4-fix1-…0600Z |
36 | exit 0, 0 invalid; schedule matched the prediction |
b4-compare.jsonl |
compare | b4-fix2-…0615Z |
36 | 36 compared, 0 diverged, 0 errors |
b4-compare-turn2/3.png |
savings 289,688 -> 532,369, the reference oracle state |
Workload each time: main menu -> Load Game -> Single Player -> ref-turn2.sav -> Launch ->
turn 2 (savings 289,688) -> End Turn -> turn 3 (savings 532,369).
Still owed
- Replace mode is not offered for any of the three hooks, so there is no End-Turn oracle result for this milestone. That is a consequence of the input boundary above, not an omission: our side models a slice of each function, and feeding that slice to the game would strand every arriving fleet or skip a colony's whole turn.
- A richer save. The reference save cannot exercise plague, rebellion (and with it every colony RNG draw), slaves, terraforming, the addiction sweep, the battle countdown, a bonus-absorbing colony, gate traffic, node-line travel, a probabilistic jump, an arrival, or a stranded fleet. A save with a plague, a rebelling colony, a Hiver gate network and a Zuul node bore would cover most of them and is the natural next workload.
- The node-line step is not wired into the hook — it needs the node graph walked from the live server. Until then a type-2 waypoint is compared with the wrong step.
ComputeOutputFromRatesis read but not hooked: it repairs damaged ships in orbit as a side effect, so it cannot run on a scratch copy of the system. Its own milestone.- The three descriptors are
unstatedto the harness; the text above is ready to drop into thecoveragefield the momentHookPolicycarries it.