24 KiB
The tail does draw under active contents — the trade-raid gate, decoded and fired
- Type: control-flow (static decode + live measurement)
- Status: verified for the trade half; the spy half is decoded but not reached and is labelled a hypothesis (rule 6)
- Confidence: high on the trade half — the callees were entered live, the RNG word delta moved, and the state that moved it was reached by playing a stock game with no file edited
- Owner / date: lane AC · 2026-09-08 · guest VM141 (
sots-re-win10-b, 192.168.10.143) - Predicts / corrects: lane W3's open risk (not retired — realised), lane L3's
trade-and-spy-workload.md§3.5, lane V2's "closure 185 functions, draw-free" reading of the trade slots
0. Headline
Yes. The strategic-turn tail draws RNG once its containers are active.
StrategyServer::OnAllCombatDone_Tailconsumes 1 word per turn in the state this lane built, against 0 on the same guest, same build and same config on the state lane L3 left.TradeManager::Slot13RngCalleeA— entered 0 times in every measurement any lane had ever taken — was entered 1×/turn on both instrumented turns, andSlot13RngCalleeBon one of them.Lane W3's open risk is realised, not retired. The RNG ledger and the standalone's generator model need extending by one draw site.
And the condition that does it is not the one lane L3 named. Freighters in service do not do it and a deployed spy would not do it either. The gate is:
a player-owned fleet parked exactly on a trade-sector node, whose owner's bit is set in that sector's
tscrmask — which is set by researchingCCC_ComRaid("Commerce Raiding").
That is the trade-raid roll. It had never fired in this campaign because no lane had ever
researched Commerce Raiding, and without it the game refuses the order in as many words:
Alpha Fleet cannot raid trade sector.
1. What was measured
Instrument: shim.cfg.l3probe verbatim (lane L3's, = lane H's hp11 + watch.mode=cont), shim
build ac-7e726b9-20260909T0017Z, cross-built on the dev box. All eleven probes reported
create=MH_OK enable=MH_OK; no COVERAGE: line was emitted; the watchpoint canary printed
SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS on every End Turn. These are measured
numbers, not MinHook failures.
1.1 Entry counts, active state (turns 21→22 and 22→23 of the AC lineage)
The baseline column is this lane's own control, not a quotation: the same shim build, the same
shim.cfg.l3probe, the same guest, one End Turn on lane L3's human-turn15-spyprogram.sav, run
immediately after the active measurement. It reproduces lane H's and lane L3's published counts
line for line.
| probe | active, turn 1 | active, turn 2 | AC control (L3's turn 15) |
|---|---|---|---|
ServerSpyManager::vslot13 |
1 | 1 | 1 |
ServerSpyManager::vslot14 |
1 | 1 | 1 |
ServerTradeManagerImpl::vslot13 |
1 | 1 | 1 |
ServerTradeManagerImpl::vslot15 |
1 | 1 | 1 |
SpyManager::Slot13RngCallee |
0 | 0 | 0 |
TradeManager::Slot13RngCalleeA |
1 | 1 | 0 |
TradeManager::Slot13RngCalleeB |
1 | 0 | 0 |
ServerTradeManager::CreateRaidEncounter |
0 | 1 | 0 |
ServerTradeManager::GenerateTradeRaidEncounters |
1 | 1 | 1 |
ServerSpyManager::vslot15 [control] |
1 | 1 | 1 |
ServerTradeManagerImpl::vslot14 [control] |
1 | 1 | 1 |
EncounterDetect::Run [control] |
NOT INSTALLED | NOT INSTALLED | — |
Slot13RngCalleeB firing on turn 1 and not on turn 2 is exactly what the decoded control flow
predicts: B is called only when A returns true, i.e. only when the raid roll succeeds. Two
turns, one success — the roll is doing what a roll does.
1.2 The RNG ledger
side.rng word deltas. Control first (L3's turn 15, no raider, same instrument):
ProcessTurn 353 -> 355 delta 2
OnAllCombatDone_Tail 355 -> 355 delta 0
Active (raider parked on sector 6's node), both turns:
ProcessTurn 448 -> 468 delta 20
OnAllCombatDone_Tail 468 -> 469 delta 1
ProcessTurn 469 -> 487 delta 18
OnAllCombatDone_Tail 487 -> 488 delta 1
One word per turn inside the tail, on a turn where the tail had never drawn a word before. The
draw is FUN_0047d830(StrategyServer+0x16c + 4) inside Slot13RngCalleeA — one uniform compared
against the raid odds. Slot13RngCalleeB and CreateRaidEncounter each cost 0 words on the turns
they ran, so the whole of the new consumption is A's single roll.
2. The gates, decoded from the instruction stream
Read off sots.exe (image base 0x00400000), disassembling to the next function start rather than
to Ghidra's reported size (rule 17).
2.1 Slot13RngCalleeA / B — called from ServerTradeManagerImpl::vslot13
Both calls are direct E8 rel32, not vtable slots:
| callee | called from | at |
|---|---|---|
TradeManager::Slot13RngCalleeA 0x00820ca0 |
ServerTradeManagerImpl::vslot13 0x0088ef80 |
0x0088f036 |
TradeManager::Slot13RngCalleeB 0x0088b440 |
same function | 0x0088f042 |
SpyManager::Slot13RngCallee 0x008408e0 |
ServerSpyManager::vslot13 0x008877b0 |
0x00887af2 |
ServerTradeManagerImpl::vslot15 (0x0082cca0) and ServerSpyManager::vslot14 (0x0088db80) do
not call any of the three. The only data references to the four outer callees are their vtable
slots (0xa30770, 0xa30774, 0xa31ba8, 0xa31bb0).
vslot13 does not iterate trade routes or trade sectors. It iterates
StrategyServer+0x64..+0x68 — the fleet vector — and per fleet requires, in order:
0088ef95 test ecx,0xfffffffc ; G0 no fleets -> return
0088efb1 je 0x88f047 ; G1 null slot
0088efb9 call 0x6fe320 ; StarFleet::GetLocationAsTradeSector()
0088efc0 je 0x88f047 ; G2 location is not a trade-sector node
0088efc7 call 0x80ed50 ; the raid predicate
0088efd1 je 0x88f047 ; G3
0088efeb..0088f016 ; G4 fleet.Pos == sector.Pos, all three floats
0088f036 call 0x820ca0 ; <-- A
0088f03d je 0x88f047 ; G6 raid roll failed
0088f042 call 0x88b440 ; <-- B
G2 is the one nobody had satisfied. 0x006fe320 reads StarFleet+0xa0 (LocID, resolved to a
pointer) and returns it only if node+0x14 == 2. That kind word is written by
StarMapNode::StarMapNode(int) 0x00730900, and enumerating every call to it gives exactly
three: StarSystem ctor 0x00752ea0 passes 0, StarFleet ctor 0x00706140 passes 1,
TradeSector ctor 0x00858020 passes 2. So vslot13 only ever acts on a fleet whose location
is a trade-sector node, never a star. Every fleet in every corpus save before this lane was
parked at a star, so G2 rejected all of them — which is why the "23-fleet Zuul save" experiment in
tail-probes.md §3.1 could not have worked no matter how many fleets it had.
G3 is FUN_0080ed50(fleet):
if (!fleet->GetLocationAsTradeSector()) return false;
if (FUN_00813ab0(fleet, 0x80000, 0)) return false; // runtime flag pair +0xb8/+0xbc
ServerPlayer *p = fleet->[0x58]; if (!p) return false;
if (!(sector->[0x90] & (1u << p->[0x28]))) return false; // ** tscr **
if (FUN_0080e9c0(sector, p)) // ** tsct ** bit, or p->[0x5c]==4
if (!FUN_006fe1d0(fleet, 0x800)) return false; // FtFlg & 0x800
return true;
FUN_0080eac0 masks six consecutive words with a low-N-players mask, which is what identifies them
as per-player bitmasks and pins the tags:
0080ead7 and [ecx+0x88],edx tssec 0080eae3 and [ecx+0x90],edx tscr
0080eadd and [ecx+0x8c],edx tsct 0080eae9 and [ecx+0x94],edx ptssec …
G4 requires the fleet's Pos (+0x18) to be bit-equal to the sector's — arrived, not in
transit. The test ah,0x44 / jp after fucompp is the MSVC float-not-equal idiom.
2.2 SpyManager::Slot13RngCallee — decoded, and much further away than "deployed"
0088781d je 0x887e92 ; SpyCraft.deat (+0x10) == 0 -> next spy
00887856 je 0x887c5b ; SpyCraft.sdet (+0x40) == -1 -> DETECTION-ROLL branch, not P
00887867 jl 0x887e92 ; (turn - sdet) < 3 -> next spy
0088787a je 0x887c4f ; system unowned -> sdet = -1
00887891 je 0x887c4f ; system owner == spy's owner -> sdet = -1
00887af2 call 0x8408e0 ; <-- P
P itself is the false-flag draw: 0.25 (0x009e5ac0) for an ordinary owner, or 0.75
(0x009e5ac4) plus a uniform pick over the other players when owner->[0x5c] == 6; its result is
the name that goes into EVENT_SPY_DESTROYED, posted to the system owner.
So deploying a spy is necessary but nowhere near sufficient for P: the spy must be deployed,
then detected on an earlier turn, then survive three more turns at a system still owned by
someone else. Lane L3's "the next condition is spyon = 0" understates the distance by three turns
and a detection roll.
But the sdet == -1 arm at 0x00887c5b is itself an RNG draw —
FUN_008e6dd0(StrategyServer+0x16c, spy->sdo), gated on FUN_00743f80(system) & 1 — and it sits
inline in vslot13, not inside P. That is a second, independent way for the tail to draw that
no entry probe on P can see. It is unreachable today only because deat == 0 rejects the
docked spy two gates earlier. Rule 20, pointing the other way: instrumenting the entry was necessary
and was not sufficient — a probe on P reading 0 does not mean the spy half is draw-free.
This is a hypothesis, not a result (rule 6): no save in the corpus has a deployed spy, and this lane did not manage to build one (§5).
3. tscr is the raid mask and CCC_ComRaid sets it — measured, before and after
This is the part that turns the decode into a recipe, and it was not predicted: this lane's
committed prediction P6 said a player fleet on a sector node would not fire the callees, on the
reasoning that tscr = 252 = 0b11111100 is exactly the six NPC player slots (2–7) and excludes both
real players (0 = re, 1 = The Eternal Empire). That reading of the value was right and the
inference from it was wrong, because the mask is not fixed.
| save | tscr, all six sectors |
player re CnRad |
|---|---|---|
human-turn15-spyprogram.sav (L3) |
252 = 0b11111100 |
absent/false |
AC turn 19, after CCC_ComRaid completes |
253 = 0b11111101 |
true |
Researching Commerce Raiding sets bit 0 — the researching player's index — in every trade
sector's tscr, and sets the ServerPlayer flag serialised as CnRad. CnRad is to
CCC_ComRaid exactly what CnTrd (ServerPlayer+0xff) is to CCC_FTLEcon. The AI, which never
researched it, stays out of the mask: CnRad false, bit 1 clear, in the same save.
So tscr reads: the set of players permitted to raid this sector — the six NPC slots by default
(which is why TRADE_RAID_ODDS_NPC and TRADE_RAID_ODDS_REFUGEE exist), plus any empire that has
bought its way in with the tech.
Two further tags fall out of the same pair of saves: tsct is the set of players trading through
the sector (bit 1 on sectors 1, 2 and 5 — the AI's; 0 on sector 6, which is mine), and it is tsct
that switches on the extra FtFlg & 0x800 requirement. Sector 6 has tsct = 0, so my raider needed
no flag.
4. The recipe, end to end
Stock game, stock data, no file edited, starting from lane L3's human-turn15-spyprogram.sav.
About 45 minutes of clicking.
- Research
CCC_ComRaid.MasterTechList.tech:CCC_FTLEcon -> CCC_ComRaid RP:12000 Human:100. At this game's settings it is one turn. In the tech tree it is the node directly above FTL Economics in the C3 branch, and at the zoom the screen opens at it is drawn off the top of the viewport: zoom out two notches with the wheel, then zoom back in one notch with the cursor low on the screen (wheel 365 560 120 1), which shifts the camera down far enough to expose it. Vertical right-drag does not pan the tech tree at all — only horizontal, and only in ~600 px steps, and even then the response is not monotonic in distance. - Toggle Trade View. Bottom bar circle (300,714) opens a six-icon vertical strip; the fifth
icon (301,626) is
Toggle Trade View. Trade sectors then draw as large translucent wireframe cubes with a padlock/$marker at the sector node itself. Nothing else in the UI shows a trade sector, and there is no bar button for trade. - Order a fleet to the sector node. Select the system, select the fleet row, click
Move(155,464), hover the sector marker — the readout readsSector 6 / Range 5.0 / ETA 2 Turns— then plainclickit. This must be onecmd.txt; and note the jiggle-click variant (jclick) silently failed to register the destination where a plainclickworked. Confirm from the fleet panel: it readsDest: Sector 6, 2 Turns. Confirm again from the save — do not trust the panel: on the first attempt the panel showed nothing and the save showedLocID 48(unmoved). - Two End Turns. The fleet arrives:
LocID 832=TradeID 832= sector 6,Pos [0, 0, 8.0]bit-identical to the sector'sPos. - End Turn with the instrument on.
Slot13RngCalleeAfires.
The refusal is the oracle. Before the tech, clicking the sector marker with a fleet selected pops
Alpha Fleet cannot raid trade sector. (MOVEWARNING_NO_RAID; the server-side twin is
OnCommand: Fleet not able to raid.). After the tech, the same click is accepted silently. That
single dialog is the cheapest possible test of the whole gate chain.
5. What this lane could not reach: a deployed spy
Reported as an absence, not inferred around.
ARes2 on ServerSystem is the asteroid-belt field — its non-zero values in this save (1703,
1948, 2061, 2172, 2531) sit inside SYSTEM_MIN/MAX_ASTEROID_RESOURCES 1500–3000, and it is 0
elsewhere. Deploying a spy needs a system that is not mine, has no enemy fleet present, has
no spy already, and has a belt. In human-turn15-spyprogram.sav only five of 28 systems have
one:
| idx | id | name | ARes2 |
owner |
|---|---|---|---|---|
| 1 | 64 | Ka'Palum | 2061 | unowned |
| 4 | 112 | Regulus | 1703 | unowned |
| 10 | 208 | Kam'Tara | 1948 | unowned, AI fleet parked on it |
| 12 | 240 | Downbelow | 2172 | mine |
| 27 | 480 | Hitomi | 2531 | player index 7, Independent Colony |
No system owned by the AI has an asteroid belt. The textbook target — an enemy colony with a
belt — does not exist in this game. The only non-own, player-owned belt system is Hitomi, held by
the NPC Independent Colony; it is 10.1 ly from the tender's berth against a hull range of 9.0, so
it needs a staging hop, and this lane ran out of session before establishing whether it is even
discovered. The Special menu at the tender's own system offers only
Repair / Scuttle / Auto-repair / Flag As Guard — no Deploy Spy, which is consistent with the
gate but does not locate it.
So: the spy half of "active contents" remains unreached, and §2.2's prediction that a deployed
spy makes the tail draw through the inline detection roll is a hypothesis for the next lane. The
save to start from is verify/results/saves/ac-turn22-raider.sav or L3's turn-15.
spies2 is still 0 in all 28 systems in every save this lane produced, including the turn-22
one. That is unchanged from L3 and is expected: no spy was ever deployed, so the experiment that
distinguishes "spies2 is not the spy list" from "spies2 fills only for a deployed spy" was not
run. The rule-6 flag on spies2 / SysMem / mts stays up. What this lane can add is that the
question is now cheap for whoever reaches a deploy, because deat (SpyCraft+0x10) is the field
vslot13 uses as the system handle, and spies2 living on ServerSystem makes the deployed-spy
reading the natural one.
6. Corrections to earlier findings (rule 11)
Lane L3, trade-and-spy-workload.md §3.5 — "every trade sector has tsnumflt = 0, no freighters
in service". Not true of the save L3 shipped. human-turn15-spyprogram.sav, sector TradeID 816
(tsgridID 5): tsnumflt 1, tsflt 2736 — and fleet 2736 is Freighters, PID 32, the AI's,
with all six member systems AI-owned. The single serialised route record is the AI's too. So across
L3's seven instrumented turns the game already had a freighter in service on an active route, and
the trade callees were entered 0 times anyway. "Freighters in service" was already falsified as the
gate by L3's own data, before this lane started, and §2.1 now says why: freighters are not in the
predicate at all.
By turn 20 of this lane's lineage the AI had built two more (sector 5 tsnumflt 2, sector 2
tsnumflt 1) with no effect on any probe.
Lane V2 — ServerTradeManagerImpl vftable slot 13 recorded as "closure 185 functions,
draw-free". It is not draw-free. It reaches a NextFloat at 0x00820ca0 on any turn a permitted
raider is parked on a sector node. The closure was right; "draw-free" was a statement about the
states that occurred (rule 23).
Lane W3's standing risk — "if either container ever fills, the ledger needs extending". The risk was correctly identified and the trigger was wrong: filling the containers changes nothing, and the ledger needs extending anyway, for a reason that has nothing to do with either container's contents.
tsflt is not a freighter list. At turn 20, sector 6 reads tsnumflt 1, tsflt 3744 — and 3744
is my raiding fleet's id, not a freighter. Whatever the tag is named, it counts fleets present in
the sector.
7. Controls and coverage (rules 15, 19, 26)
Guest adoption / instrument-removed control. hooks=off watch=off on ref-turn2.sav, one End
Turn, on VM141 with this lane's own shim build:
(Autosave EndTurn).sav BB4FD9AC89F41E3BC0DB2AF08B18CE83417521AC4BCEE695FC9FA6CE16E30948
(Autosave).sav 978041ACD168B56ED8EB3F5E42E78D5E70EAE6E6517D75E659A5EB7CA3D60921
Both identical to the published three-run oracle in
verify/results/saves/determinism-hashes.txt. That is agreement with a control already reproduced
in three fresh processes by other lanes on another guest, which is the rule-26 bar; this lane ran it
once here rather than twice, and says so.
Instrument armed. All eleven probes create=MH_OK enable=MH_OK, no COVERAGE: line; watchpoint
canary PASS on every End Turn; container watch read trade routes count=46 -> 47,
spy programs count=1 across the two measured turns.
Thin coverage, stated plainly.
- Two instrumented turns, one raiding fleet, one sector, one ship in the fleet. The word cost is 1 on both, but 1 fleet × 1 sector is the smallest possible workload and the per-fleet loop means the cost should scale with permitted raiders parked on sector nodes, which this lane did not vary. Do not fit a constant to two observations (rule 20).
Slot13RngCalleeA's own body has a short-circuit that skips the draw entirely: any ship whosedesign+0x12c > 1returns true without rolling. My fleet is oneCR Repair and Salvage, so the short-circuit was not exercised, and a raid by a different hull could cost 0 words while still firingB. Untested.- The odds arithmetic (
0.20vs0.05base,+0.01·nShips, doubled when a freighter shares the sector, scaled by a per-ship float) was read statically and not validated against measured success rates: two turns, one success. probes=11is not determinism-neutral (lane H bisected the detour on probe index 8). These entry counts and word deltas are comparable line-for-line with lane H's and L3's tables, which is what they are for; they are not a byte-identity claim.probes=8still installs indices 0–7, i.e. all four outer and all three inner callees, and is the configuration to use if a future lane needs both at once.EncounterDetect::Run [control]is index 11 andprobes=11installs 0–10, so it reports NOT INSTALLED, never 0.
8. Artifacts
| what | where |
|---|---|
| turn-22 save, raider parked on a trade sector, both callees having fired | verify/results/saves/ac-turn22-raider.sav |
| turn-20 save, the moment the fleet arrives on the node (pre-measurement) | verify/results/saves/ac-turn20-raider.sav |
| probe + ledger trace, two instrumented End Turns | verify/traces/ac-probe-turn21-turn23.jsonl |
| shim log and watch hits | verify/results/shim/ac/ac-probe-run-{shim.log,watch.txt} |
| predictions, committed before the workload and again after the decode | sots-engine docs/AC-predictions.md |
| instrument | sots-engine src/shim/shim.cfg.l3probe, unchanged |
New content these saves carry (rule 27 — expect the coverage ratchet to break, and type the
content rather than moving the bar): a ServerPlayer with CnRad true; trade sectors with
tscr = 253 rather than the corpus-universal 252; a trdmgr with three rt route records
rather than one; sectors with tsnumflt up to 2 and a tsflt entry that is a warship fleet id
rather than a freighter; and a StarFleet whose LocID is a TradeSector id (832) rather than a
star system id — the first fleet in the corpus located anywhere but a star, which is precisely the
state that makes the tail draw.
9. VM141 as left, and two things that cost this lane time
Guest state. Game stopped. C:\SOTS\shim.cfg is a copy of shim.cfg.l3cont (hooks=off,
watch=on, watch.mode=cont — the cheap config); the lane's dist is C:\SOTS\shimdist-ac
(BUILD_ID ac-7e726b9-20260909T0017Z) and C:\SOTS\binkw32.dll is that build's proxy, with the
original still at binkw32_real.dll. SavedGames has been restored from
C:\SOTS\SavedGames-acstash and additionally holds ac-turn20-raider.sav and L3's three saves,
so every Load-dialog row position recorded by an earlier lane has moved on this guest — screenshot
the dialog, do not reuse a remembered row. Also left, all harmless: C:\SOTS\acdata\ (config text
extracted from sots.gob) and C:\SOTS\ui\ac-*.ps1.
click_helper.ps1 gained six verbs, purely additive, every existing verb byte-for-byte
unchanged; pre-lane backup at click_helper.ps1.preAC:
drag x1 y1 x2 y2 left-button drag (ported from VM144, lane L3)
rdrag x1 y1 x2 y2 RIGHT-button drag, 30 steps (the only thing that pans the tech tree)
sdrag x1 y1 x2 y2 slow left drag, 40 steps
wheel x y delta [n] n wheel notches
jmove x y move with a real relative mouse_event jiggle (new; ported from lane L2's ui.ps1)
jclick x y click with the same jiggle (new)
The jiggle is needed to read the star map and harmful when committing to it. jmove is what
makes the map's hover readout update — without it the Sector 6 / Range 5.0 / ETA 2 Turns line never
appears and you are clicking blind. But jclick on the sector marker did not register the
destination, twice, silently; a plain click at the same coordinate did, first time. So: jmove to
read, plain click to commit. Lane L2's note that the map "needs real mouse motion" is right about
motion and wrong if read as "use the jiggle for everything".
Verify a fleet order from the save, never from the panel. The first move order left the fleet
panel showing no destination and the save showing LocID 48 — unmoved — through two End Turns,
with no error anywhere. The second attempt showed Dest: Sector 6, 2 Turns in the panel and moved.
There is no failure signal; the only reliable check is Flt.LocID in the autosave.
Dev-box build note (not a repo change). cmake/toolchain-mingw-i686.cmake links libgcc and
libstdc++ statically but not libwinpthread. On the WSL dev box the i686 mingw is the posix
threads model, so binkw32.dll comes out importing libwinpthread-1.dll and the game dies at load
with 0xC0000135 STATUS_DLL_NOT_FOUND — no shim log, no banner, just an exit code in
dxvklog\run.txt. Adding -DCMAKE_SHARED_LINKER_FLAGS=-static fixes it. CT111 does not have this
problem (its mingw uses the win32 model), which is why no lane has hit it before; the toolchain file
was deliberately not changed, because that is a shared-infrastructure decision for the integrator
rather than a lane's.