Commit graph

107 commits

Author SHA1 Message Date
alex
c662f73b69 lane Q: type TurnCommands_v5 -- named coverage to 100% on all 11 saves
The last opaque block of the save format. Reconciles lane W's provable negative
against lane O's issued-order saves: the two disagreed because three different
things were being compared as one.

Read out of Game::TurnCommands::Write (0x00842540, 764 bytes, no loops of its
own). Two halves:

  * a PROLOGUE of six flag-gated groups. Each is a WriteBool on a member,
    followed only when set by that command's payload. Write order is NOT offset
    order -- each gate sits after its payload in the struct -- so the class is
    one of the 89 whose offset-sorted layout view cannot be aligned to the wire.
    That, not a branch, is why the sorted view showed an i32 where the save has
    a bool at item 4.
  * TWENTY-SEVEN std::list<T> members (0x70..0x1a8, stride 0xc, allocator-last),
    each written by its own helper as WriteInt(size) then size element records.
    All 27 are always written, so an empty list still costs one zero int.

So the recovered 44 items are 17 member writes plus ONE ITEM PER CONTAINER CALL
SITE: the linear recovery keeps the call site, guesses its kind from an element
field it could resolve, and drops the count word. 44 - 17 = 27 = the number of
lists. The "27 trailing ints with only 22 i32 slots" objection is that same 27
seen from both sides -- a kind mismatch in the table, not a structural
impossibility. And 8 prologue items + 27 zero counts = the 35-item block every
no-orders save carries bit-identically.

Item arithmetic closes to the unit on all five distinct workloads (35/38/41/61/
123). One correction to the provisional layout: the fleet-move element ends in a
COUNTED route vector {fleetId, nHops, nHops x systemId}, not a fixed quadruple --
with a four-item element the writer would need 26 lists on one save and 28 on
another, and it has 27.

Conformance: the generic check cannot be used here. Every tag is "." so the LCS
degenerates to a strict positional compare in which any primitive disagreement is
fatal, and the table's tail describes elements where the wire has counts. A
dedicated check states what is checkable instead: the 17-item prologue item for
item (17/17, and SchemaProbe takes every branch, so this is real evidence that
the conditional structure read from the instruction stream is the one the
recovery flattened) and the tail count (27 lists vs 27 table items). The tail is
reported as wire-only, never claimed as matched.
  86 shapes / 838 items -> 87 / 856, still 0 MISMATCH.

Twenty-two of the 27 lists are HYPOTHESES: the scalar sequence comes straight off
the helper, but no save exercises them. Nested element bodies not otherwise
modelled here (ShipDesignDef, FleetLayout, WeaponGroups, DefenceLayout,
RaidTargets) are carried as opaque Nodes rather than guessed at, so a wrong body
cannot desynchronise a reader. select() matches the exact ".TurnCommands_v5"
suffix, so a future _v6 falls back to the carried Node.

SECOND DEFECT, present at main and unrelated to this block: zuul-turn23-fleet23
is the first save with a non-empty NVs list, and its element's leading id was
typed positionally as "." where the real tag is PID. The typed round trip on that
save differed at 0x89c14. Rule 6 exactly -- a path no save exercised was a
hypothesis flying as a fact. Fixed, and independently corroborated: the recovered
table for Game::ServerSystem names that item PID, and the Sys row moves from
102 matched / 1 wire-only / 3 shape-only to 103 / 0 / 2.

Coverage (CoverageArchive typed-vs-carried, not round-trip success): all eleven
saves 99.7-99.9% -> 100.0%; opaque items 37/43/63/132 -> 2 everywhere, and those
two are the deliberately-carried MT19937 block. Nothing else in any save we hold
is untyped. Ratchet 99.8 -> 99.99. Round trip byte-identical on all eleven
(newly so on zuul-turn23-fleet23).

test_save now asserts, on every real save, that each TurnCommands_v5 block is
consumed by the prologue plus the 27 lists with nothing left over -- the
item-granular statement a wrong list count or element width breaks first.

clean-room OK; host ctest 36/36; test_save 11 saves 0 failures. src/shim/ not
touched, so no cross-build was needed.
2026-09-08 09:03:11 -04:00
alex
c883a325ad lane T merge fixups: name the roll-succeeded branch instead of its FUN_ id (clean-room); mark describe_i32 maybe_unused so the shim cross-builds 2026-09-08 08:13:39 -04:00
alex
6835cf4055 merge lane T: turn driver map (StrategyServer is the spine, not ServerPlayer); ResearchRollPending settled; player_turn hook prepared 2026-09-08 08:09:38 -04:00
alex
a7ca208b63 T: hook for Game::ServerPlayer::ProcessTurn, with the prediction committed first
Descriptor + pure adapter + host tests for the per-player turn driver. Not
deployed; the WIN32 half is unbuilt here (no cross-compiler on this host).

The declared boundary is narrower than the function on purpose. Phases 2, 3
and 6 -- the savings apply, the aid records and the research refund -- are pure
functions of ComputeBudget's 22 slots and ProcessResearch's overBudget, and
both live in the original's own stack frame. Reaching them would mean calling
ComputeBudget ourselves (it repairs ships in orbit, audit #6), reading the
nested B1/B3 hooks (audit #5, the self-fulfilling compare), or inferring them
from the Sav delta. So they are guarded, not checked, and the three formulas
are written and unit-tested but not wired into the verdict.

Declared: the phase-7 clear, the RebAI decay, the descending timed-bonus
sweep, plus roll_flags and rng as observations ours never writes. Guards over
the whole ServerPlayer and the TechTree header.

docs/T-turn-driver.md states, before any run: which regions must not diverge,
which checks are weak by construction on the reference save, what falsifies
the ResearchRollPending reading, and the save that would finally fire the
branch nobody has seen.

host ctest 36/36 (was 35/35); clean_room_check OK.
2026-09-08 08:08:47 -04:00
alex
c75931f592 sync generated headers (669 addresses) after aiagent merge 2026-09-08 07:36:15 -04:00
alex
1df6977808 merge lane A: StrategyAIAgent blocks typed; named coverage 98.0 -> 99.9%, opaque items 748 -> 37 2026-09-08 07:34:22 -04:00
alex
8e45b43638 A: type the AIAgent custom-data blocks; named coverage 98.0% -> 99.9%
Game::StrategyAIAgent::Streamable and the ten shapes under it. The whole
writer is unconditional -- the branch the decompiler shows around lnat is an
inlined vector destructor whose operator delete is marked noreturn, and both
paths converge -- so the recovered sequence and a single record are the same
sequence, and all 36 items match with 0 wire-only and 0 shape-only.

CD blocks are now selected by the CDT id at the same ordinal, in both
directions; the one .TurnCommands_v5 block per save still falls to a Node.

Also: Sim's Attrib was not an empty frame, it was an AttribMap holding a count
of 0, and typing it closes those two items too. And StreamableEnum<T> writes a
frame containing one int, not a bare int, so SysMem/mts/nalat are arrays of
one-int frames -- byte-neutral, since all three have count 0 in every save,
but the previous typing was wrong.

Conformance 74 shapes/769 items -> 86/838, still 0 MISMATCH. Round trip
byte-identical on all four saves; ratchet 97.5 -> 99.8. Every container that
is empty in all four saves is named as such in the notes; the new unit test
populates each one, since nothing else exercises them.
2026-09-08 07:33:06 -04:00
alex
f4f3a7aae7 sync generated headers (640 addresses) after wire merge 2026-09-08 07:08:39 -04:00
alex
872e214d8e merge lane W: SvSctOb/DOpts/spies2 typed; named coverage 97.1 -> 98.0%; conformance 74 shapes 0 mismatch 2026-09-08 07:06:53 -04:00
lane-w
2170782a81 W: close SvSctOb, DOpts and spies2; named coverage 97.1% -> 98.0%
SvSctOb is a StreamableHelper<SVScriptObject> -- a polymorphic pointer holding a
Game::SVSOSots, which writes two variant lists each dispatched by the key item
before it (xscn -> xsc, EncID -> EncObj). Neither map is on the wire; both were
read out of the game's factories (see the notes repo). The shapes apply the key
in both directions, so a body goes back out as whatever it came in as, and an
unmodelled key still round-trips as a Node. 18 new shapes: SVSOSots, the four
scenario bodies (traps / crowdefs / indsys / gmtrigger -- indsys really does
serialize nothing, its Read and Write are both the shared `ret 4` stub) and the
eight encounter bodies the saves exercise. The four factory ids no save carries
(7 SystemKiller, 8 PuppetMaster, 14 Locust, 21 Ortgay) are deliberately NOT
typed: their serializers are recovered but nothing could check a shape for them.

DOpts and SVSOVonNeumann::trev are VectorHelper<Mars::String>, so read_elem /
write_elem / SchemaBuilder::carr grew the std::string branch lane G listed as
missing. spies2 is VectorHelper<int>: the TYPE is certain from the helper's own
decorated name, but the count is 0 in all 28 systems of all four saves, so no
element value has ever been observed -- the shape is a hypothesis about
behaviour even though it is a fact about type. Same for SysMem and mts.

Conformance 56 shapes / 657 items -> 74 / 769, still 0 MISMATCH, and every new
binding is 0 wire-only and 0 shape-only. Coverage 97.1/97.2/97.2/97.6 ->
98.0/98.0/98.0/98.4 with the byte-identical round trip preserved; ratchet
95.0 -> 97.5. CD is now the only remaining region of size, and it stays opaque:
the recovered 44-item Game::TurnCommands sequence cannot be aligned to the
save's 35 items even as a subsequence (item 4 is 8 bytes, so a bool where the
recovery says i32; and the 27 trailing ints have only 22 i32 slots to come
from), which proves the no-orders diagnosis rather than assuming it.

Two unit tests added that need no saves: the string-array element branch
(including the empty string, which is four zero bytes and so looks like int 0)
and the SvSctOb variant dispatch round trip.

ctest 34/34, clean_room_check OK, test_save skips cleanly with SOTS_SAVES_DIR
unset. sots_stream_schema.h unchanged: streams.py and gen_stream_schema.py were
re-run and the output is byte-identical apart from the provenance line.
2026-09-08 07:04:45 -04:00
alex
4a4c7bc249 merge lane U: tech unlock cascade (header regenerated from addresses.json + addresses.d/lane-u.json, not hand-resolved) 2026-09-08 06:58:53 -04:00
Alex
4e729212ba U: live verification - 0 divergences on 35 calls, three workloads
First End Turn 3/3/0, five-turn continuation 15/15/0, Zuul 20/20/0, tracecmp
exit 0 on all three. The End-Turn oracle hashes are unchanged, so the cascade
does not perturb the game.

All 22 divergent fields lane V recorded are gone. The prediction in section 4
held field for field on the deterministic half; call 9 turned out to be a
different completion from lane V's (the AI picked another target from turn 5),
which the model reproduced anyway - three unlock costs that appear in no earlier
report.

Honest limit: roll_draws was 0 on all 35 calls. ResearchRollPending is normally
consumed by ProcessTurn before ProcessResearch runs, so the RollResearchEvent
draw is modelled and inside the compare but has never been seen to fire.
2026-09-08 06:57:15 -04:00
alex
241e7fabc4 sync generated headers (627 addresses, 386-class wire schema) 2026-09-08 06:36:25 -04:00
alex
4b19961c07 merge lane G: generated wire schema channel + SchemaProbe conformance check; named coverage 38% -> 97% 2026-09-08 06:34:43 -04:00
alex
5bae56a397 G: wire schema channel + widen the save codec to 97% typed coverage
The serializer recovery reaches this repo as a generated wire schema
(include/generated/sots_stream_schema.h, 386 classes / 2042 items): for each
class, the ordered sequence of items its Write puts on the stream. Facts only —
no field offsets, no sizeof, no strides. This engine reads and writes the on-disk
format; it does not inherit the original's memory layout.

The table is a specification, not a program: the recovery is a linear pass over
Write, so it cannot see Write's branches (StarShip's BQ2 is gated on hbq but
listed unconditionally) and it flattens container loops. A codec driven off it
would desynchronise. The hand-written io() shapes stay the codec; SchemaProbe
(probe.h) walks them with every branch taken and test_wire_schema LCS-aligns
that against the table — 56 shapes bound, 657 items matched, 0 mismatches.

Four defects the check found, all invisible to a round-trip test:
  - SystemParams field 1 is a string, not an int. It is the empty string in
    every save, and an empty string is four zero bytes — byte-identical to the
    int 0, so it round-tripped by luck. A named planet would have desynced.
  - ObservedTech/ObservedWeapon odet is a bool, not an int. Byte-safe only
    because a 4-char tag makes a bool item and an int item both 12 bytes.
  - SpeciesRatios nv and ShipRecords srbd are counts, not fields.

CoverageArchive separates items a field names from items a Node merely carries,
because a byte-identical round trip is not a coverage claim. Typed coverage of a
real save goes 37.9% -> 97.1% (97.2/97.2/97.6 on the others) with the round trip
still byte-identical, by typing TechTree (both NumTechs sections), Events,
ShipRecs, sprjs, civr, comms, spy2, spymgr, aid, Ojvs, AIEnf, FNG, trdmgr and
the Des section/gun-bank tree. Ratchet at 95%.

trdmgr resolves a recorded trap: ServerTradeManager's Read/Write really are the
inherited no-op, but the call is virtual and ServerTradeManagerImpl has the real
serializer. Same shape resolves IServerSpyManager -> ServerSpyManager.

ctest 34/34, clean_room_check OK, test_save skips cleanly with SOTS_SAVES_DIR unset.
2026-09-08 06:30:35 -04:00
Alex
405ba41a1e U: the SetResearched unlock cascade, and the prediction for the run that checks it
Pure module game/sim/techgraph: PrereqsMet (AND of ORs, empty group fails),
SetResearched (stamps, child-cost sweep, sticky turnAvailable, zero-cost
recursion) and the newly-available collector, all read out of 0x00581e10,
0x0057d8e0 and 0x00587cc3.

Wired into the B3 hook in compare mode only, over the scratch node copies:
four more node write-backs, the EVENT_TECHS_UNLOCKED list (still an input,
still nullptr when it could not be computed), the de-duplicating observed-tech
append and the one RNG word RollResearchEvent draws.

docs/U-unlock.md section 4 is the prediction, written before the build was staged.
2026-09-08 06:26:00 -04:00
alex
0f7aba1278 sync header after eventlive merge 2026-09-08 06:02:54 -04:00
alex
517e1f2e52 merge lane V: live event verification - advance prediction held field for field; sizeof(ObservedTech)=44 confirmed behaviourally 2026-09-08 06:01:28 -04:00
alex
410f980832 V: live verification of lane P's event posting; the advance prediction held
Ran the queued VM140 verification of docs/P-events-wiring.md §4 with main dd38117
unchanged, cross-built on CT111 as eventlive-dd38117-20260908T0916Z and staged to
C:\SOTS\shimdist-v.

First End Turn on shim.cfg.recapb3 unchanged: 3 calls, 3 compared, 0 divergent,
tracecmp exit 0. Every field lane P forecast for call 0 read as predicted, including
turn=3, events_turn_bucket_exists=true, events_dedup_risk=0, next_id 3->4 on both
sides and observed_techs.bytes unchanged on all three calls.

Five End Turns: exit 1 with two divergent calls instead of three, both short by
exactly one id (7/6 and 12/11) — the unmodelled EVENT_TECHS_UNLOCKED, at the size
lane P said it would be.

sizeof(Game::ObservedTech) measured live at 44 on both completion calls
(observed_techs.bytes 440->484 and 484->528), confirming a static pin behaviourally.
That confirmation was written into the RE repo's addresses.json and has already
reached the generated header on main, so this branch carries docs only.

One deviation from lane R: rng diverged on call 9 because the completed tech had
research_roll_pending set, so the OnTechResearched callback drew one word ours does
not. That is a declared out-of-scope boundary, and it means lane R's "RNG 15/15" was
workload luck rather than a property of the hook.

Also closed the Zuul double roll behaviourally on a species-5 save made for the
purpose: 8 calls, 0 divergent, and all four species-5 researching calls advance the
generator by two with ours matching bit-for-bit.

clean_room_check.sh OK; host ctest 33/33. Run as separate commands.
2026-09-08 06:00:01 -04:00
alex
9d685cb78a sync generated header (615 entries): serializer-recovered struct layouts, 87 verified classes 2026-09-08 05:54:25 -04:00
alex
dd38117e79 CONTRIBUTING: cmake >=3.25 requirement and the uv workaround 2026-09-08 05:13:31 -04:00
alex
e96294d418 sync generated header (414 entries) after strings merge 2026-09-08 05:13:20 -04:00
alex
4488e2f4e6 merge lane S: std::string is 0x1c confirmed (one layout); ObservedTech fully mapped from serializer enumeration 2026-09-08 05:11:50 -04:00
alex
fb23616a8d merge lane F: x87 control-word sensitivity - 53-bit and 64-bit identical; 24-bit and round-up each move 2 named leaves 2026-09-08 05:10:21 -04:00
alex
d462513f84 shim: force and verify the x87 control word around the turn gate
New fpu_force module (4 register-transparent asm stubs, same pattern as the M0
Initialize hook, so the [unverified] prototypes of the turn-gate functions are
never relied on) plus two shim.cfg keys:

  fpu.force=<cw>|off      fldcw at StrategyClient::EndTurn and
                          StrategyServer::BeginProcessTurn, and nowhere else
  fpu.sample_ticks=on|off per-frame sampler, logs only when the word CHANGES

Forcing is deliberately one write per turn: re-forcing inside the pipeline would
guarantee the value is present without proving it ever held, which is the exact
false negative this experiment has to avoid. Verification is kept separate --
StrategyServer::ProcessTurn is hooked sample-only, and its reading plus the
existing per-hook fpu_cw fields (38 samples per turn across phases 4, 6 and 8)
are what establish that the setting lasted the whole turn.

Six shim.cfg variants, identical apart from the fpu.force line, and docs.

Used to settle STATE_CHECKSUM.md 3.5: 53-bit and 64-bit x87 produce byte-
identical turn results, so an x64/SSE port has no double-rounding budget to
preserve; 24-bit and round-up each move exactly one thing. Findings and evidence
live in the notes repo (findings/subsystems/fpu-precision-sensitivity.md).

clean_room_check.sh OK; host ctest 32/32.
2026-09-08 05:09:16 -04:00
lane S
32d3e36d8b sync generated header (414 entries): std::string is 0x1c, ObservedTech fully mapped
The header's ObservedTech notes carried a std::string size of 0x18. That was a
mis-attribution: the element's +0x24 word is the name string's trailing (empty)
allocator, not a data field. sizeof(std::string) is 0x1c binary-wide, confirmed
against every serializer in the original.

ObservedTech is now fully mapped, so the two research-hook comments that said
sizeof and the append site were unpinned are corrected. The region stays
declared-not-modelled -- appending is a behavioural change this hook does not
make -- but its byte delta now has an exact expected value (44 per completion),
and the note records that the original's append de-duplicates by tech name, so a
naive push_back would diverge on re-observation.

clean_room_check: OK.  host ctest: 33/33.
2026-09-08 05:08:53 -04:00
alex
f0cc2a9d06 sync generated header (404 entries): ObservedTech pinned at 0x2c, RecordObservedTech append site 2026-09-08 04:46:53 -04:00
alex
59590d9774 merge lane P: count-only event posting; next_id reaches 4 host-side; observed_techs declared region 2026-09-08 04:18:57 -04:00
alex
6293ce44ac Merge branch 'main' into wip/post-events
# Conflicts:
#	include/generated/sots_addresses.h
2026-09-08 04:17:35 -04:00
lane P
f3c6d551de lane P: post the research events from ours (count-only), declare ObservedTech
TechTree::ProcessResearch's events region now compares a modelled value instead of
reporting a known defect. ours posts the pass's events into its own
sots::events::EventStorage, seeded from a scan of the owner's list taken BEFORE the
original runs, and writes only the counts into the region's scratch copy. The game's
PostEvent is never called and no live byte moves; replace mode still posts nothing,
because a bumped EvNxID with no record behind it would corrupt the oracle's save.

- game/events: PostResearchPassEvents (the decision half, pure) + KeylessEventText
- shim/hooks/event_inputs (new lib shim_events, host-tested): the live<->model adapter,
  carrying game pointers as explicit uint32 so a 64-bit host build cannot alias them
- research hook: the wiring, a new observed_techs region for ServerPlayer+0x274, and
  turn / events_next_id_in / events_dedup_risk in the args so the count model's own
  assumption is measured rather than assumed
- EVENT_TECHS_UNLOCKED is NOT posted: its trigger is pinned but needs SetResearched's
  unlock cascade, which ours does not run. The driver takes the unlock list as an input
  and is handed 'no list', so a missing input cannot look like a modelled negative.
  Predicted residual: next_id short by exactly 1 on a completion call.

ctest 33/33 (shim_events_unit is new), clean_room_check OK. The shim TU is
syntax-checked only: no MinGW cross toolchain on this box.

See docs/P-events-wiring.md for the exact prediction for the next VM run.
2026-09-08 04:16:22 -04:00
alex
cef889ecef sync generated header (386 entries) after movefleet merge 2026-09-08 03:53:49 -04:00
alex
41547dee89 merge lane M: MoveFleet position rounding - five float32 narrowings + reciprocal-multiply; live 8 divergences -> 0 2026-09-08 03:52:18 -04:00
alex
2aa8cba614 movefleet: reproduce the original's float32 normalise; 8 of 45 live divergences -> 0
The behavioural compare found 8 of 45 StrategyServer::MoveFleet calls diverging by
one ULP on a position component. Read off the instruction stream, the cause is that
the engine's vector normalise narrows to float32 four separate times and we kept
everything in double:

  delta.c = f32(dest.c - pos.c)     stored back to a float32 slot before normalising
  sumsq   = f32(x*x + y*y + z*z)    products/adds in 53-bit regs, only the SUM stored
  len     = f32(sqrt(sumsq))
  inv     = f32(1.0 / len)          a reciprocal, MULTIPLIED through, not three divides
  dir.c   = f32(delta.c * inv)

and the same call returns the leg distance, so it is never recomputed in a wider
precision either. The position tail was already right, which is why the error was a
constant absolute ~1.2e-7 (half an ULP of the inputs) rather than a formula error.

Adds NormalizeVec3 / StraightLeg / StraightLegDistance / AdvanceAlongUnitDirection
and rebuilds AdvanceAlongDirection on them; the movement hook now takes both the
direction and the distance from one StraightLeg call, as the original does. The
arrival test is an exact float compare, so the distance has to be that same float32.

Tests pin float32 BIT PATTERNS, not tolerances: one case per narrowing plus four
independent legs component by component. A CHECK_NEAR would pass against the old
arithmetic.

sim::Distance is left in double on purpose and flagged at its declaration: it now
serves only the node-line/stutter geometry, which very likely needs the same
treatment but has zero behavioural coverage to correct it against.

Live, same VM/save/workload, run twice by this lane:
  control recap-7584bad-20260908T0615Z  45 calls, 45 compared, 8 diverged, exit 1
  fixed   mf-45bdf7d-dirty-20260908T0721Z 45 calls, 45 compared, 0 diverged, exit 0
with identical arguments, identical pos.before and identical ORIGINAL pos.after on
all 45 calls. The control reproduced the eight divergent call_ids exactly.

Coverage unchanged and still thin: all 15 moving calls are the same straight-run
waypoint type; types 2-5 were attempted and could not be reached (the only player
that would travel a node line has no ships on this save). See docs/M-movefleet.md.

ctest 32/32; tools/clean_room_check.sh OK.
2026-09-08 03:50:04 -04:00
alex
45bdf7d805 merge lane R: golden-trace recapture with guards; ProcessResearch event defect now visible (exit 1) 2026-09-08 03:07:44 -04:00
alex
4e2b7b694d Merge commit '82ef52f' into wip/recapture 2026-09-08 03:04:37 -04:00
alex
e50d5e5900 recapture: golden traces with guards on the live game
Five hooks recaptured; no source change needed. ProcessResearch's over-budget
event now diverges as one line instead of a 609 KB save diff (15 calls, 3
diverged, RNG 15/15). ComputeBudget holds at 0 over 4284 compares but only 20
distinct states and 13 always-zero slots. MoveFleet diverges on 8 of 45 calls by
1 ULP of position - B4's clean verdict was a one-sample verdict. First guarded
captures for OnTechResearched (RollResearchEvent draw finally fired and matched),
the colony turn and MoveFleet. Guards mapped SetResearched live and found an
undeclared observed-tech vector append.
2026-09-08 03:04:34 -04:00
alex
82ef52f55c sync generated header (380 entries) after events merge 2026-09-08 02:50:37 -04:00
alex
5bd6d04bce merge lane E: event-posting API (EventStorage@player+0x29c, PostEvent 0x008862b0) 2026-09-08 02:48:55 -04:00
alex
a7348be72c game/events: the player event log and the five research events
Recovers the game's event-posting API so the engine can post events and the
compare harness can see them. Until now the owner's event list was invisible to
every layer: B3's replace-mode oracle failed by exactly one item across 40,300
(an unposted EVENT_RESEARCH_OVERBUDGET) while its compare read clean, and B2's
clean compare bounds the economy fields only.

New pure module src/game/events:
  * EventStorage / TurnEvents / PlayerEvent -- the list is bucketed by TURN, not
    flat, which the save-editor struct notes had wrong.
  * EventStorage::Post reproducing the original's rules, including the four that
    change save bytes: the FLT_MAX (not infinity) default position; action 0 with
    no subject and no position storing as 2; per-bucket dedup that compares
    message/image/location/position/action but NOT summary; and EvNxID starting
    at 0 and being promoted to 1 on the first post.
  * PruneOldTurns reproduced with its off-by-one: of a leading run of buckets
    older than turn-50 it erases n-1, so one stale bucket always survives. The
    survivor is serialized, so correcting it would diverge.
  * research_events: the five events the research path raises, their EvImg
    identifiers and string-table keys, and the 0.8 completion split evaluated
    against (double)0.8f rather than the decimal 0.8.

Localized text is deliberately absent: only the EVENTSUM_/EVENTMSG_ keys are
here and the text resolves through a caller-supplied lookup, as the game does.

The four event offsets the B3 hook carried as local literals now come from the
generated header; they are read off instructions rather than inferred from the
save schema.

tests/game_events: 112 checks including a replay of the event list
turn3-state.sav actually holds. ctest 31/31 -> 32/32.

docs/E-events.md carries the proposed region and Coverage wording for the next
B3 recapture.
2026-09-08 02:45:57 -04:00
alex
7584badab3 sync header 2026-09-08 02:07:51 -04:00
alex
ebd8297a5d merge b4 live verification (header regenerated) 2026-09-08 02:07:28 -04:00
alex
bf163a187f b4 live: 36 calls 0 divergences, no colony RNG draws as predicted; fix 3 hook bugs (stale args, two-base StrategyServer, raw Ghidra offset) 2026-09-08 02:07:28 -04:00
alex
e1cbb81fdb clean-room: strip decompiler identifier from a coverage note 2026-09-08 01:30:33 -04:00
alex
3f0721f443 merge harness audit (coverage + guards) 2026-09-08 01:28:35 -04:00
alex
c110fb245b harness: compile-time Coverage on every descriptor, guard regions, replace-mode records; audit of 23 undeclared side effects 2026-09-08 01:28:35 -04:00
alex
b925944492 fix: regenerate address header so b2 offsets are present 2026-09-08 01:19:30 -04:00
alex
a5ffe187ea merge b2 live verification (header regenerated) 2026-09-08 01:18:39 -04:00
alex
bbba536d99 b2 live: 3 completions compared 0 divergences; float32 confirmed on the game; RollResearchEvent RNG region added; xenotech block size bug fixed 2026-09-08 01:18:39 -04:00
alex
89c0013496 merge b4; regenerate header 2026-09-08 00:57:55 -04:00
alex
e3b14ed22b merge b4 (colony/movement corrections; VM verification pending) 2026-09-08 00:57:55 -04:00