sots-engine/docs/L3-predictions.md
alex 6699f543bc watch.mode=cont: both containers, both ends, re-armed every turn; and hooks=off no longer disarms the watchpoints
Lane L3 needed to watch the trade-route and spy-program vectors across a whole game
played forward, not one turn of one save. `modcount` and `tshn` arm once on purpose --
their targets are picked from one turn's state and re-picking them would move the
measurement -- so this is a third mode rather than a change to either.

`cont` puts all four debug slots on the two containers, `_Myfirst` as well as `_Mylast`.
Both vectors are default-constructed with all three pointers zero, so the first element
writes all three: watching only `_Mylast` cannot separate "allocated for the first time"
from "appended to an existing buffer", and those are different events in the model this
lane set out to falsify. It re-arms and re-logs on every End Turn, and the canary
self-test's counter is therefore read as a delta -- on the arm-once modes the delta is
the old value, so their log lines are byte-identical.

`ReportContainer` is factored out of `ArmTshnSlots` so both modes emit the same container
line. Lane W3's published count=0 is the control every later count is compared against,
and a reformatted line would have made that comparison a judgement call.

The defect: `Shim_Init` returned before `install_watchpoints` whenever the trace mode was
`off`, so `hooks=off watch=on` printed `watch=on` in the banner and armed absolutely
nothing -- a config that reports a confident zero, which is the failure method rule 1
exists to catch. The watchpoints are an independent instrument with their own arming
detour and no trace records, and a long play session wants them without paying 30-45 s
per End Turn for template hooks that measure nothing it is asking about. MinHook is now
initialised and the module installed on the `hooks=off` path when `watch=on`.

Configs: shim.cfg.l3cont / .l3control differ in exactly one key for rule 19;
.l3probe is lane H's hp11 verbatim plus the three watch keys, so the entry counts stay
comparable to lane H's empty-container baseline line for line.

Gates: clean_room_check OK; host ctest 54/54; CT111 shim cross-build OK, exports 66
names identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 19:10:10 -04:00

7.8 KiB
Raw Blame History

Lane L3 — predictions, written before the build (method rule 2)

Lane L3 owns VM144 (sots-re-win10-144, re@192.168.10.144, a ZFS clone of VM140). Target: manufacture a workload in which the trade-route vector and/or the spy-program vector is non-empty, then measure whether the four RNG-bearing tail callees draw.

Instrument: watch.mode=cont, a third mode on lane W3's watchpoint module. Same single arming detour (StrategyServer::ApplyAllTurnCommands, RVA 0x0038f6a0), same VEH, same canary, same hit format. Two differences from tshn:

  1. it arms all four debug slots on the two containers — _Myfirst and _Mylast of each — so an allocation (_Myfirst moves from 0) and a push_back (_Mylast moves) are separable;
  2. it re-arms and re-logs every End Turn instead of only the first. Lane W3 printed one count from one turn of one save; this workload is a game played across many turns, and the question "did the container grow on turn N" needs a line per turn.

What the static reading says the workload is (before any run)

Read out of sots.exe, TechTree/MasterTechList.tech, Data/Strategy/StrategyVars.txt, Species/*/sections/*.shipsection and the repo's own catalogs. This is the part three previous lanes did not have, and it changes the cost estimate by an order of magnitude.

Trade routes

  • The gate is the tech CCC_FTLEcon (id 10026) — it sets ServerPlayer+0xff (CnTrd) unless the player is RebAI (ghidra/addresses.json:1510, tech-effects.md:64).
  • Cost: CCC_ROOT → CCC_FTLBrdB (RP 4,000) → CCC_FTLEcon (RP 18,000). Zuul cannot research it (Zuul:0 in the availability line) — so the workload must not be Zuul.
  • StrategyServer_RegisterTradeSystems 0x007adc80 registers, for every owned system whose owner has CnTrd, ServerSystem::NumTradeRoutesSupported 0x00819d20 route slots, and that function is ceil(civ/REQ_CIV) + ceil(imp/REQ_IMP) with a floor of 1.
  • Therefore the population thresholds in StrategyVars.txt (TRADE_ROUTE_REQ_CIVPOPULATION 200000000, TRADE_ROUTE_REQ_IMPPOPULATION 500000000) are not a hard gate — they scale the count, and the minimum is 1 per owned system.
  • TRADE_SECTOR_SIZE 10.0 — routes live inside a trade sector, so the endpoints have to be near each other.
  • A trade station (DNStationTrade, station type 4) adds STATION_TRADE_NUMROUTES 2 and STATION_BONUS_TRADE_INCOME 0.25. It is a bonus, not a prerequisite.

Prediction T1. The premise carried in tail-probes.md §3.2 and repeated in the L3 brief — "trade income needs trade-station construction plus its tech" — is wrong about the station. CCC_FTLEcon plus two owned systems inside one trade sector is sufficient to make the trade-route vector non-empty. No trade station is needed.

Falsifier: a game with CCC_FTLEcon researched, ≥2 owned systems within 10 units, and trade routes … count=0 still logged after several End Turns.

Prediction T2. The vector fills during turn processing, not at the moment the tech completes, so the first non-zero count appears on the End Turn after the one that finished CCC_FTLEcon.

Spy programs

Nobody had a click path. The static reading gives one.

  • Species/<race>/sections/_Spy.shipsection — entity_class "SpyShip", section_type mission, nodesign 1, spy 1, cost 20000, and requires CCC_SpyBm + requires IND_SlvgTech.
  • The spy tender — the "ship a spy can dock with" in Can't build spy: Fleet does not have a ship spy can dock with. — is the section carrying spytender. In this image that is exactly one section per race: CRRepairandSalvage (Cruiser Repair & Salvage mission section, requires IND_SlvgTech, cost 60,000). Nothing in the UI says "tender".
  • Tech cost: IND_OrbFound → IND_CruisCon (16,000) → IND_SlvgTech (12,000) for the tender and half the spy; CCC_ROOT → CCC_FTLBrdB (4,000) → CCC_SpyBm (12,000) for the other half. CCC_FTLBrdB is shared with the trade chain.
  • The action path is the Special menu on the fleet panel ((229,464) at 1024×768): SHIPACTION_BUILDSPY / DEPLOYSPY / PICKUPSPY, backed by Game::BuildSpyDialog, Game::DeploySpyDialog, Game::PickupSpyDialog and the Game::CanBuildSpy functor.
  • The engine's own refusal strings enumerate the preconditions exactly: build — own system, player has ability, fleet has a tender, tender not already carrying; deploy — not your own system, no enemy fleet present, and Can't deploy spy: Spy requires an asteroid belt to hide. (SPYSHIP_NEARBY_ASTEROID_RANGE 500.0; STARMAP_ASTEROIDBELT_ODDS 25 %).

Prediction S1. ServerSpyManager's vector is the spy-craft list, and it grows at Build Spy, not at Deploy — the Game::SpyCraft record has atto (attached-to) and tdep (turn deployed) fields, which only make sense if the object exists while still docked. If S1 holds, the spy half of this workload needs no second empire at all.

Falsifier: Build Spy succeeds (the fleet shows a docked spy) and the next End Turn still logs spy programs … count=0. That would move the growth point to Deploy and make an enemy colony with an asteroid belt mandatory.

Prediction S2. The trapped write to the spy vector's _Mylast will have an eip inside ServerSpyManager and an ebpret inside the turn-command applier, not inside a UI class — because the debug registers are armed on the turn thread only (ArmCurrentThread(GetCurrentThread())), and SOTS is lockstep: the UI issues a command and the server applies it. If the write instead happens on the UI thread, no trap will be taken at all and the count line is the only evidence. This is the rule-20 trap in this instrument: a zero trap count with a rising count line means wrong thread, not no write.

Getting there without twenty turns of play

The Custom game-setup screen (GAMESETUPFE_BTN_CUSTOM) exposes GAMESETUP_INITCOLONIES, GAMESETUP_INITSAVINGS, GAMESETUP_INITTECHS, GAMESETUP_MONEY_RESEARCHMOD, GAMESETUP_MONEY_INCOMEMOD, GAMESETUP_SYSTEM_DISTANCE, GAMESETUP_SYSTEM_SIZE, GAMESETUP_SYSTEM_SUITABILITY. All of it is stock: no data file is modified, no save is edited, so the resulting save is a legitimate corpus member.

Prediction W1. With initial colonies, initial savings, initial techs and the research modifier at maximum on a small close-packed map, both tech chains are reachable inside ~10 End Turns rather than the "tens of 30–60 s End Turns" the brief budgets.


Instrument predictions

Prediction I1 (control, rule 19). watch=on watch.mode=cont is byte-neutral: one End Turn from ref-turn2.sav reproduces the determinism oracle — (Autosave EndTurn).sav bb4fd9ac89f41e3b…, (Autosave).sav 978041acd168b56e… — exactly as watch=off does. The two configs differ in one key.

Prediction I2. On ref-turn2.sav the new mode reproduces lane W3's numbers: both managers non-null, all six pointers zero, both counts 0, zero traps. If it does not, the mode is wrong and nothing measured with it later counts.

Prediction I3. The canary self-test passes on every turn, not just the first — the delta form of the counter is new in this mode and is itself a thing that can be wrong.


What would make this lane's result worthless

  • Counting a container from the wrong base. S is the ApplyAllTurnCommands this; every StrategyServer_off_* is in the S+4 frame (lane-t.json:172). A wrong base yields a plausible-looking zero.
  • Reporting "0 traps" when the writer ran on another thread (S2).
  • Producing a save whose non-empty container came from a modified data file. Nothing in this lane modifies sots.gob or any loose data file; if that ever changes it must be recorded in the save's provenance line.