sots-re/ghidra/addresses.d/lane-c3.json
alex 3b99e255c7 lane C3: ComputeOutput on the turn path -- the money ComputeBudget actually sums
`ServerSystem::ComputeOutput` 0x00751fb0 and the whole of `ComputeOutputFromRates`
0x00751bb0, read from the instruction stream to the next function start. This is the
function lane E1 correctly identified as the real blocker on P01/P02/P03/P05/P06 after
its own chain came out 25/25 on BnkEl and did not unblock them.

The shape, and the fact everything turns on: construction points the build queue and
the ship-repair pass do not spend are redistributed over trade / terraform /
infrastructure, and the TRADE share is added to the money channel. Unspent
infrastructure points cascade into the terraform pool and unspent terraform points
cascade into money -- two hops, not one. So a colony with an empty build queue earns
the same money whichever way its sliders point, which is why the turn path's money
sits within one trade point of the projected path on this corpus.

Eleven addresses, five of them dropped as agreeing duplicates. New: the repair pass
0x00751590 (the B1 double-run side effect, and its round robin is provably a `min`),
the construction-point helper 0x00746830, the queue-demand walker 0x008251e0, the
ideal-suitability resolver 0x00745d60, and two helpers with non-standard conventions
that would be silently wrong read as thiscall -- 0x0074c6f0 takes `this` in ESI and
0x007460b0 takes the system in EBX.

Corrections in place: `income-term.md`'s "leftover science points" are leftover SHIP
CONSTRUCTION points (there is no science channel in this function), its
science-cascade bullet moves from inferred to instruction-verified, and
`output-term.md`'s 0x00751fa8 boundary is padding -- the `ret 8` is at 0x00751fa0.

Measured, with the engine change (sots-engine wip/output2 d3ee453):

    turn1-state -> turn2-state    209 -> 157   closed 52  regressed 0   (was 51 / 0)
    turn2-state -> turn3-state    108 ->  86   closed 22  regressed 0   (was 21 / 0)

Closed 1 per pair, regressed 0. Every prediction in the doc's section 4 held; the one
that was off is named and explained. The strongest check is not the leaf count but
the eleven-save self-check in section 5.3, where every difference between the turn
path and the projected path decomposes to the unit into the build queue's points.
2026-09-08 14:50:50 -04:00

93 lines
9.2 KiB
JSON

{
"_note": "Lane C3, ComputeOutput on the turn path. Five addresses this lane read are already held elsewhere and AGREE, so they are dropped from this fragment rather than duplicated (rule 14): ServerSystem_ComputeOutput 0x00751fb0 and ServerSystem_TerraformPointsNeeded 0x00746890 (lane-e1.json), ServerSystem_ComputeOutputFromRates 0x00751bb0 and ServerSystem_NormaliseOutputRates 0x00747390 (addresses.json), Ship_TestFlags 0x00814da0 (lane-z.json). Every one was re-disassembled to the next function start this lane and the address, convention and boundary matched. One CORRECTION to a boundary that is recorded here rather than by editing another lane's row: ComputeOutputFromRates ends at its `ret 8` at 0x00751fa0 and 0x00751fa3..0x00751faf is int3 padding, so output-term.md's 0x00751fa8 is padding, not code; income-term.md's 0x00751fa0 is right. See findings/subsystems/output-turn-path.md.",
"entries": [
{
"name": "ServerSystem_RepairShipsInOrbit",
"addr": "0x00751590",
"convention": "thiscall",
"prototype": "int (ServerSystem* sys, int points, bool estimateOnly) // `ret 8`, real end 0x007517b5 (SEH frame, /GS cookie). Returns the points NOT consumed. Walks the system's fleets through the system's own vtable (slot 2 = count, slot 4 = element), keeps those whose +0x58 equals sys->PID and that pass 0x00813ab0(0,8), and inside each keeps the ships that pass 0x00814da0(0,8) with 0x00815180(ship,1) > 0. With estimateOnly it returns max(points - totalCost, 0) and touches nothing; without it, it distributes round-robin -- share = max(points / shipCount, 1) per pass, take = min(cost, share), applied by 0x008151c0 -- until no ship takes anything or the points run out. THE ROUND ROBIN IS EQUIVALENT TO points - min(points, totalCost): share is at least 1, so every ship with a positive cost takes at least one point per pass, and the only early exit needs every remaining cost to be zero. THIS IS THE SIDE EFFECT that makes ComputeOutputFromRates unsafe to call for its value (the B1 replace double-run defect): ComputeOutputFromRates passes estimateOnly = 0",
"status": "verified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "ServerSystem_ConstructionPoints",
"addr": "0x00746830",
"convention": "thiscall",
"prototype": "int (ServerSystem* sys, double constructionShare) // `ret 8`, real end 0x00746883. Turns the ship-construction channel's rounded share into out[7]: with no owner it is ftol(share), else k = StationCount(sys, PID, 1) and b = STATION_BONUS_SHIPCON (slot 0x00af08ec) taken as 0 unless STRICTLY positive, returning ftol( k x (b x share) + share ). TRUNCATING, not rounding, and note the association -- neither is `share x (1 + b x k)`",
"status": "verified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "BuildQueue_TotalConstructionLeft",
"addr": "0x008251e0",
"convention": "thiscall",
"prototype": "int (BuildQueue* q) // 23 bytes, plain `ret`, no frame. Walks the std::list at q+0x10 from its sentinel and sums the dword at +0x18 of each node -- the order's `conleft`. This is the queue demand ComputeOutputFromRates charges against out[7] before anything cascades back to the money channel, and the ONE input of that function that a save can supply in full (Sys/BQ/ords/conleft is on the wire)",
"status": "verified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "ServerSystem_IdealSuitability",
"addr": "0x00745d60",
"convention": "thiscall",
"prototype": "double (ServerSystem* sys) // real end 0x00745dc0. The suitability the terraform channel aims at, and the value NormaliseOutputRates compares sys->Suit against with an exact ==. With no owner it returns sys->Suit itself, so an unowned system is always 'at its ideal'. Otherwise it starts from owner->IdealSuit (+0xb0), replaces that with StrategyServer::IdealSuit(sys->server, sys->indi->indsp) when the system carries an independence record (+0x1c8), and finally overrides both with sys->dsu (+0x118) whenever dsu differs from the float behind 0x00aeca6c. NOT the same source as CalcSuitMod's ideal, which is the server's per-species array unconditionally -- the two agree on every corpus save, so the difference is instruction-verified only. Every corpus system carries dsu = FLT_MAX, which is why the sentinel is READ AS FLT_MAX (inferred from the corpus, not from the data files)",
"status": "verified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "ServerSystem_CivilianConsumption",
"addr": "0x0074c6f0",
"convention": "custom",
"prototype": "int (/* ESI = ServerSystem* sys */) // real end 0x0074c80f. TAKES ITS `this` IN ESI, not ECX -- a compiler-local helper that inherits the register its caller holds; hooking or calling it as a __thiscall reads the wrong object. Returns 0 with no owner and 0 on an independent colony (+0x1c8), else derives a civilian-share ratio from three int64 population helpers (0x0074a870, 0x0074a8c0, 0x0074a920), scales CIVILIAN_RESOURCES_CONSUMED (slot 0x00ae2ea4) by it and floors the result at 1. Feeds only the resource ledger (out[1], out[2]); it is NOT on the path to out[3]",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- the ESI convention and the two early returns are read from the instruction stream; the ratio's exact shape is not"
},
{
"name": "SystemRepairDemandForOwner",
"addr": "0x007460b0",
"convention": "custom",
"prototype": "int (/* EBX = ServerSystem* sys */ ServerPlayer* owner) // real end 0x0074615a. TAKES THE SYSTEM IN EBX and the player on the stack -- the second compiler-local helper in this call graph with an inherited register. Returns 0 when either is null. Walks the system's fleets through the system's vtable (slot 2 = count, slot 3 = element), keeps those whose owner (0x0071e280) is the argument and whose +0x78 byte is set, and sums 0x00829180 over the ship vector at +0xa4..+0xa8. ComputeOutput stores the result in out[6]; nothing downstream of out[3] reads it",
"status": "verified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "TradeManager_SystemRouteIncome",
"addr": "0x0083a5b0",
"convention": "thiscall",
"prototype": "void (TradeManager* mgr, ServerSystem* sys, ServerPlayer* owner, int* a, int* b) // `ret 0x10`. Returns immediately when `sys` is null, else forwards to 0x00833a10(owner, a, b, 0, sys) with the manager still in ECX. ComputeOutput reaches it as `mgr = server->vtbl[2]()` -- a zero-argument getter whose four argument pushes were scheduled BEFORE the call, which reads as a five-argument virtual call and is not one. Fills out[4] and out[5]; nothing downstream of out[3] reads them. The name is INFERRED from the callee's neighbourhood (the trade manager's difficulty multiplier lives at 0x00833938), not from a symbol",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "ServerSystem_StationCount",
"addr": "0x00815c10",
"convention": "cdecl",
"prototype": "int (ServerSystem* sys, ServerPlayer* owner, int kind) // the station count both station bonuses read. ConstructionPoints 0x00746830 passes kind = 1 (shipyards); GroupOutput 0x0074b7a0 passes kind = 0 (the imperial output bonus). Body not read this lane; the argument order and the two kinds are read off the two call sites",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
},
{
"name": "Ship_RepairCost",
"addr": "0x00815180",
"convention": "thiscall",
"prototype": "int (StarShip* ship, int kind) // the per-ship repair demand the orbit repair pass sums and then spends against, always called with kind = 1. Body not read; this is the ONE input of ComputeOutput that no save can currently supply, and until it is read the engine takes the demand as 0 -- which reads a colony with a damaged fleet HIGH, because every point the repair pass would have taken is a point that comes back to the money channel instead",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
},
{
"name": "Ship_ApplyRepair",
"addr": "0x008151c0",
"convention": "thiscall",
"prototype": "void (StarShip* ship, int points) // the write half of the orbit repair pass. Body not read",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
},
{
"name": "Fleet_TestFlags",
"addr": "0x00813ab0",
"convention": "thiscall",
"prototype": "bool (StarFleet* fleet, int a, int b) // the fleet-level gate of the orbit repair pass, called as (0, 8); the ship-level counterpart is 0x00814da0 with the same arguments. Body not read",
"status": "unverified",
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
}
]
}