Commit graph

8 commits

Author SHA1 Message Date
alex
bb0f990de9 lane K: field offsets belong in the offset schema, and off_RNG was already right
gen_addresses.py distinguishes "addr" (VA -> RVA) from "offset" (a member
offset). Three lane-K entries used "addr" for offsets and generated negative
RVAs, which broke the engine build. Two are re-expressed as offsets; the third,
StrategyServer_off_RNGPtr_S_frame, is withdrawn - addresses.json already has
StrategyServer_off_RNG = 0x16c for the S frame alongside off_RNGPtr = 0x168 for
S+4, which is precisely lane T's rule. The second call site found this lane
confirms it rather than correcting it.
2026-09-08 08:48:25 -04:00
alex
bb1f8b4692 lane K: StrategyServer::OnAllCombatDone_Tail mapped, 36 phases from the instruction stream
The second turn driver, 0x007d92a0, read byte for byte. Corrects turn-driver.md
section 5: of the four subsystems it said live here, only bankruptcy does.

- 36-phase map with strides enumerated (EncounterResults 0x178, Encounter 0x74,
  member 0x44) and the arity check that logs but does not return.
- Phase 7 is encounters.clear(), not a filter: the erase pair is the same
  four-argument shape vector<Encounter>::operator= uses, and both arms converge
  three instructions later.
- Bankruptcy: ProcessBankruptcy at phase 15, UpdateBankruptcyLimits at phase 31.
  Three corrections to formula-gaps Q1 - the divisor is the double
  -0.15000000596046448 not -0.15, the per-system income term is clamped at 0
  before summing, and the 3.3 factor lives in .bss and is DB-loaded.
- Turn results are FILLED here (phases 6, 11, 18 write S+0x2f4[PlyrIdx]) but
  rotated by ApplyEncounterResults and dispatched by SynchronizePlayer as event
  0x25 afterwards. sizeof(SETurnResults) = 0x11c, enumerated five ways.
- BuildTurnEvents is misnamed: it is the setup/load/rejoin resync push, gated on
  a pending descriptor, and references no EVENT_ string at all.
- TurnEvents_Write and TurnEvents_Read are swapped in Ghidra (layouts.json is
  right). sizeof(TurnEvents) = 0x18, enumerated four ways.
- The autosave: StrategyHost::Autosave 0x00895210, its four localized paths, the
  rotation that fires only on the post-turn call, the connection detach around
  the write, and why the payload carries nothing time-, name- or machine-derived.
- Two RNG sources in the tail that nothing models: one NextFloat per expired node
  line, plus draws inside the combat resolver. Both run before the autosave.
- S+0x8 advances twice per turn, not once.

Repo-wide correction: the research-event roll costs one or two RNG words, not
one. Fixed in unlock-cascade.md, addresses.json and lane-u.json; the captured
compare artefacts under verify/results are left alone as run records.
2026-09-08 08:47:08 -04:00
alex
9de77f7e5e lane O: 7 purpose-built saves - issued turn commands, node routes, research-roll-pending
Produces the game states several board rows were blocked on, and reads each
one back out of the save before claiming it.

- CD/TurnCommands_v5 decoded: three saves with issued orders (research target,
  research boost, build orders, system rates, colonize, fleet move). Layout in
  SAVE_FORMAT.md section 11; ids cross-checked against the same file's Sys/Flt.
- Waypoint type 3 (NodeRoute) now has 16 instances in one save and the player's
  own fleet is a mover in two. Type 2 is shown NOT to be produced by a Move
  order for either node-drive race (Zuul and Human both emit type 3).
- research_roll_pending true at load in two saves; the ProcessTurn gate constant
  at 0x00a2c788 is a float 0.5, so the flag survives into ProcessResearch only
  while progress/cost <= 0.5 at the start of the turn.
- Budget tail: construction and a large maintenance are live; Nexp is empty in
  all 11 saves and the 1.8 UI has no expense slider, so slot 12 looks dead.

VM140 restored to the original 8-file SavedGames set (autosaves byte-identical)
and left at the main menu; VM140 row set FREE.
2026-09-08 08:35:13 -04:00
alex
834eb09a19 T: read ServerPlayer::ProcessTurn and StrategyServer::ProcessTurn from the instruction stream
Phase map for both turn drivers, byte-for-byte from objdump rather than the
decompiler. Corrections that matter:

- the end-of-turn tail is UNCONDITIONAL. turn-spine.md 2.4 step 11 said it is
  deferred when encounters are pending; that came from reading an inlined
  std::vector destructor as a branch (both arms converge at 0x007dcb38). There
  is no branch on the encounter snapshot in the function, and 0x00794ad0 does
  not build an encounter list -- it builds the 15 ship-action type ids.
- ServerPlayer::ProcessTurn takes one float argument and never reads it.
- the ResearchRollPending site runs AFTER ProcessResearch, not before, and
  clears the flag only when the roll actually fires. Threshold is a strict
  0.5f < progress/Cost. Both consumers located by a full displacement scan.
- EVENT_NO_RESEARCH's gate is 'completed nothing this turn', not 'nothing
  available' -- 0x00584e50 collects RESEARCHED techs, not available ones.
- a fired research roll costs one or two RNG words, not one: the plague branch
  draws a second NextInt and posts EVENT_PLAGUE_OUTBREAK.
- two StrategyServer bases four bytes apart, and two per-turn counters.
- new: the research refund at 0x008914aa, previously unmodelled.

45 addresses in ghidra/addresses.d/lane-t.json (669 -> 722 entries, no dupes).
2026-09-08 08:08:36 -04:00
alex
805409ec11 A: the AIAgent CD blocks - derivation, addresses, and what stays a hypothesis
Game::StrategyAIAgent::Streamable typed end to end. The writer is entirely
unconditional; the branch the decompiler shows is an inlined vector destructor
whose operator delete is marked noreturn. dsh is a std::map's _Mysize, so it is
the (pid, trns) count and not a scalar; lnat and lat are scalars, not elements.
AISystem::Write never reads its object, so every AISys body on disk is empty
regardless of game state. StreamableEnum<T> is a frame holding one int, which
makes SysMem/mts/nalat arrays of frames rather than of ints.

Named coverage 98.0% -> 99.9% on all four saves; state_checksum.py unchanged
and still PROVED. Section 7 lists the ten element layouts no save exercises and
says what workload would settle each.

29 addresses in the per-lane fragment; merges to 669 with no duplicate name.
2026-09-08 07:33:33 -04:00
lane-w
2308b6edb9 lane W: SvSctOb variant factories; fix the four save_reader.py defects openly
Part 1 (notes side): findings/objects/svsctob-variants.md records the two maps
that are nowhere on the wire and were read out of the game -- EncID -> class
from a 23-entry dword jump table at 0x0052bf60 indexed by EncID-1 (0x0052bf00),
and xscn -> class from an exhaustive four-way _stricmp chain at 0x005a7050.
Twelve live EncIDs, four scenario names, and "indsys" =
Game::SVSOIndependentSystems whose Read and Write are both the shared `ret 4`
stub at 0x005f8ac0, so its empty frame is correct output rather than a
truncation. Also: SVSOSots::Read accepts NPCPlr and hastraps, which Write never
emits -- read-only backward compatibility, not a hole in the recovery. And a
correction to the recovery itself: SVSOCrowDefenders writes `dsys` INSIDE the
ndsys loop; layouts.json calls it a plain member and no save can settle it
because both counts are 0 everywhere. 13 addresses in ghidra/addresses.d/
lane-w.json; gen_addresses.py merges to 640 with no duplicate name.

Part 2: lane G found four defects in both readers and deliberately did not
patch the oracle mid-campaign. Fixed now, with tests, and byte-neutral.
  1. Game::SystemParams field 1 is a string, not an int (empty string == four
     zero bytes == int 0, so it round-tripped by luck).
  2. ObservedTech/ObservedWeapon odet is a bool, not an int (byte-safe only
     because a 4-char tag makes both items 12 bytes; 3 chars would not).
  3. SpeciesRatios nv is a count, not a field.
  4. ShipRecords srbd is a count, not a field -- and this one is behaviourally
     confirmed, not inferred: srbd takes 0, 1, 3 and 4 across the players and
     every non-zero count is followed by exactly srbd x 5 scalars.
Note that 3 and 4 were an ABSENCE in save_reader.py, not an error: ShipRecs and
civr were both A(..., "any"), so the fix had to add the shapes rather than
retype a field.

Byte-neutrality: every item's inflated offset is unchanged on all four saves
(38,933 / 39,843 / 40,300 / 35,771 offsets, sequences identical), so no item
boundary moved. state_checksum.py still reports coverage: PROVED on all four
with the same rebuilt byte counts. The /CreateParams and /Sim/players digests do
change, because they hash typed VALUES and two fixes change what a value is --
and the value-byte deltas balance exactly: odet items x 3 plus p1 items x 4.
--strict exit 0 on all four saves; tests 36 -> 48.

findings/objects/wire-schema-closeout.md carries the whole account, including
the proof that CD/TurnCommands_v5 cannot be typed without a save that has
issued orders.
2026-09-08 07:05:09 -04:00
Alex
5a3f986f38 lane U: the unlock cascade, implemented and live-verified
Reads: TechTree::PrereqsMet 0x0057d8e0 (AND of ORs; zero groups TRUE, an empty
group FALSE), the prerequisite layout at TechDef+0x88/+0x98, the tail collector
0x00587cc3, and the head of OnTechResearched (RecordObservedTech unconditional;
the research-event roll gated on ResT == def && ResearchRollPending).

Live: 35 compared calls over three workloads, 0 divergences, tracecmp exit 0,
End-Turn oracle hashes unchanged. The EVENT_TECHS_UNLOCKED residual lane P
predicted and lane V measured is closed by running the cascade, not by posting
on completion.

12 new addresses in ghidra/addresses.d/lane-u.json (header 615 -> 627).
2026-09-08 06:57:37 -04:00
alex
9834b3d175 gen_addresses: per-lane fragment dir; duplicate names are a hard error 2026-09-08 05:55:03 -04:00