Two processes, same workload. Phase 18 tries three producers; only ONE of the
three AI players reaches the candidate walk at all, which is why the other two
are stable across every run of both lanes -- different code path, not better luck.
For that player the candidate stream is ONE entry, {2, 12}, in BOTH runs -- a
category, not a tech. So arrival order in the candidate vector is not the
mechanism (a vector of one has no order), and the three-arm fallback never ran
(both probes zero, both runs). The variation is inside the resolver that turns a
category into a tech, and it produced XNC_TrnsMorr2 (techId 288) in one process
and XNC_TrnsHum2 (techId 282) in the next.
k is nameable from the shipped tech data: XNC_ROOT allows six tier-1 techs at an
identical 2000 RP, one per species, each allowing exactly one tier-2 successor --
the six XNC_Trns<Species>2. Four of the six have been observed across six runs
between lanes L4 and L5, and their costs differ (13000-30000), so the resolver is
not ranking by cost; it takes whichever member of the available set it reaches
first.
Bonus rule-19 result: run R2's autosave is byte-identical to the hooks=off control
of the same workload. Twenty detours installed, same tiebreak, same bytes -- the
only free variable in the whole turn is the tiebreak itself.
Still open and flagged: BIO_GnMod, the one observed value outside that family.
Seven more addresses in ghidra/addresses.d/lane-l4.json; the eighth collided with
lane AI4's cl_SetResearchTarget and was dropped with the agreement recorded.
133 lines
No EOL
13 KiB
JSON
133 lines
No EOL
13 KiB
JSON
{
|
|
"_note": "Lane L4 2026-09-08. Nine entry-probe targets for the live AI order capture on VM145. Eight are IAITask::Execute bodies (vtable slot 5) named from the RTTI class that owns the vtable; three of them are SHARED between a task and its `Goal` variant (lane AI3 \u00a75: the paired classes have an identical Execute and differ only in their two GetTarget slots), so the name records the pair rather than pretending there are two bodies. The ninth is the StrategyClient order method AITRaid reaches at depth 1, which is the list-16 producer and the subject of AI3 \u00a72.4. All nine were checked against dumps/functions.json as real function starts and disassembled far enough to confirm a >=5-byte patchable prologue with no branch target inside it, because every one of them is a MinHook target in this lane's shim module. Addresses already carried elsewhere (RunTaskList, IsClaimedByAnotherTask, AcquireFleetsForTask, RequestBuildForTask, AssignFleetsAndIssueOrders, IssueRouteForFleets, BuildTurnCommands, ApplyAllTurnCommands, ApplyTurnCommandBatch) are deliberately NOT re-declared here -- lanes AI1/AI2/AI3/A2 own those names and the generator rejects a same-address-different-name collision. COLLISION RECORDED (rule 14): lane L4 independently reached 0x00578f60 and would have filed it as StrategyClient_cl_SetResearchTarget_Facade; lane-ai4.json already carries it as cl_SetResearchTarget, which is the better name, so this fragment drops the entry. The one fact L4 adds and lane AI4 did not have: that function takes the tech's NAME (a const char*), not an id -- phase 18 at 0x006cafd0-0x006cafdf resolves the chosen object's std::string at +0x4 through the short-string union before pushing it. That belongs on the AI4 entry's prototype whenever the integrator folds the fragments in.",
|
|
"entries": [
|
|
{
|
|
"name": "AITColonize_Execute",
|
|
"addr": "0x0068b400",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITColonize::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITColonizeGoal::Execute is the same address. 48 bytes; a forwarder to the parameterised worker 0x0068b280 called as worker(ecx = agent, this, pass, this->+0x8, &this->+0x20, &this->+0x10) with `edi = this->+0xc` passed as an IMPLICIT REGISTER ARGUMENT (whole-program-optimised custom convention; a reimplementation that ports only the stack arguments passes garbage). Reaches the colonize order (list 7) at depth 4 via 0x0068b280 -> 0x006930f0 -> 0x00578ff0 -> 0x00769640",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#5 -- lane AI3's unlimited-depth direct closure; address re-checked by lane L4 against dumps/functions.json and the prologue disassembled before hooking"
|
|
},
|
|
{
|
|
"name": "AITEscortGateInvade_Execute",
|
|
"addr": "0x0068c7c0",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITEscortGateInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITEscortGateInvadeGoal::Execute is the same address. 80 bytes; forwards to 0x0068c5d0 as __fastcall(ecx = this->+0xc, edx = agent) plus 8 stack arguments. Reaches list 7 at depth 4",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#5 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "AITInvade_Execute",
|
|
"addr": "0x0068d7a0",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITInvadeGoal::Execute is the same address. 160 bytes; forwards to 0x0068d460 with 11 arguments, then maintains this->+0x38 and this->+0x39. +0x39 is computed at 0x0068d80f-0x0068d82a as (0x006a6380(agent, this->+0xc) < 2 * 0x006a6260(agent, this->+0xc)) and is what IAITask slot 11 returns for this class. Reaches list 14 at depth 5",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#4.3,#5 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "AITNodeBore_Execute",
|
|
"addr": "0x0068e590",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITNodeBore::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Does its setup (0x00685810) and its finaliser (0x0068e090) only when pass == 0, and forwards `pass` to 0x0068a520 in both passes. Reaches list 14 at depth 5 via 0x0068a520 -> 0x006ceef0 -> 0x006c16c0 -> 0x006987e0 -> 0x007634d0. Zuul-flavoured task (the node-bore is a Zuul mechanic), so on a non-Zuul board it may be absent from the task list entirely",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#2.3,#5 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "AITRaid_Execute",
|
|
"addr": "0x0068e670",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITRaid::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. THE ONE TASK WHOSE PASS-0 BEHAVIOUR IS NOT SETTLED: at 0x0068e89e it calls 0x006b76a0 and then at 0x0068e8b8 loops the list-16 order method 0x007635f0(client, fleetId, 1) over the returned fleets, and NEITHER the call nor the loop carries a `pass` guard of its own. AI3 \u00a72.3's argument says the fleet vector at [ebp-0x28] is empty on pass 0 because it is downstream of the same hub, but [ebp-0x28] was never traced to closure. This address exists as an address so that a zero at 0x007635f0 can be read: entered-and-silent and never-entered are opposite answers (method rule 20)",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#2.4,#8.3 -- lane AI3 named this probe; lane L4 built it"
|
|
},
|
|
{
|
|
"name": "AITAdvanceIdleShips_Execute",
|
|
"addr": "0x0068f230",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITAdvanceIdleShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Whole body is pass-1 only: `if (pass != 1) return` at 0x0068f25a. Table priority 0, so it is always the LAST task in the descending-priority sweep -- the AI sweeps up whatever is still idle only after every other task has taken both its minimum and its desired force, which is what the two-tier quota model predicts",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#2.3 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "AITBuildDeepScanShips_Execute",
|
|
"addr": "0x006901a0",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITBuildDeepScanShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6 via 0x006ce460 -> 0x006ce360 -> 0x006ce190 -> 0x006bd790 -> 0x006b3bc0 -> 0x00762fd0, the same chain as AITBuildPoliceShips",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#5 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "AITBuildPoliceShips_Execute",
|
|
"addr": "0x00690380",
|
|
"convention": "thiscall",
|
|
"prototype": "void __thiscall Game::AITBuildPoliceShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6, identical chain to AITBuildDeepScanShips. One of the two candidate producers of the single list-3 element the reference AI emits on turn 1",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#5 -- lane AI3; address re-checked by lane L4"
|
|
},
|
|
{
|
|
"name": "StrategyClient_OrderList16",
|
|
"addr": "0x007635f0",
|
|
"convention": "thiscall",
|
|
"prototype": "bool __thiscall Game::StrategyClient::OrderList16(int objectId, bool flag) -- RET 8. THE LIST-16 PRODUCER. Opens with `cmp byte [this+0x15c], 0` -- the End-Turn latch StrategyClient::EndTurn 0x00783be0 sets at 0x00783c59 before BuildTurnCommands runs -- so like every other order method it refuses once the turn has been submitted. Element record for list 16 is {i32, bool} (lane Q), matching the (objectId, flag) pair; list 16 is in the PAYING half of the ModCount table (bump at 0x00821e23 in its applier 0x00821e20). Reached from AITRaid::Execute at depth 1. The method is named for the list it fills, NOT for what the flag means: no save has ever carried a list-16 element and nothing has been observed",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-stepping-and-passes.md#2.4 + findings/objects/turncommands-block.md#3 -- lane AI3 named the probe, lane Q typed the element; lane L4 read the prologue and checked the +0x15c latch"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_SelectResearchTarget",
|
|
"addr": "0x006c8890",
|
|
"convention": "thiscall",
|
|
"prototype": "void* __thiscall Game::StrategyAIAgent::SelectResearchTarget(void** outSlot) -- RET 4, the THIRD and last research producer tried by Process Turn phase 18 (0x006caf70), reached only when 0x006a84f0 and 0x006c27c0 both answer null. Body: (1) `if (agent->+0x10->+0x150 /*ClientPlayer*/ ->+0x294 != 0) return 0` -- a player that already has a target picks nothing; (2) build a std::vector of 0x0c-stride candidates with 0x006c2490(agent, &vec); (3) walk it FRONT TO BACK calling 0x006c8580(ecx = outSlot, edx = cand[1], stack: agent, cand[0]) and return the FIRST non-null answer; (4) if none answered, fall through to the three-arm rotation seeded by 0x006b36e0(agent) & 7 over the .data tables at 0x00a1a544 / 0x00a1a564. THERE IS NO SORT AND NO SCORE IN THE WALK: the vector's arrival order IS the priority, which is what makes a per-process ordering difference change the outcome",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08, instruction-stream read of 0x006caf70 and 0x006c8890 swept to the next function start"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_BuildResearchCandidates",
|
|
"addr": "0x006c2490",
|
|
"convention": "cdecl",
|
|
"prototype": "void __cdecl Game::StrategyAIAgent::BuildResearchCandidates(StrategyAIAgent* agent, std::vector<Candidate>* out) -- constructs a working object on the stack (0x006bcca0(agent)), emits the candidate vector into `out` (0x006bc500), destroys it (0x006bcef0). Element stride 0x0c. The emitter is a nested walk over groups with NO std::sort at that level, so the output order is the walk order",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_TryResearchCandidate",
|
|
"addr": "0x006c8580",
|
|
"convention": "fastcall",
|
|
"prototype": "void* __fastcall Game::StrategyAIAgent::TryResearchCandidate(void** outSlot /*ecx*/, int candWord1 /*edx*/, StrategyAIAgent* agent, int candWord0) -- called once per candidate in arrival order by SelectResearchTarget; the FIRST call that returns non-null ends the walk and that return value is the chosen tech object (its +0x4 is the std::string the order method is given). An entry probe on this address therefore records the whole candidate stream in the order the selector sees it",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_ResearchProducerA",
|
|
"addr": "0x006a84f0",
|
|
"convention": "cdecl",
|
|
"prototype": "void* __cdecl (StrategyAIAgent* agent) -- the FIRST research producer tried by phase 18. Body unread; probed for reachability only, because a non-null answer here means the candidate walk never runs",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08, from the call site at 0x006caf8f"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_ResearchProducerB",
|
|
"addr": "0x006c27c0",
|
|
"convention": "thiscall",
|
|
"prototype": "void* __thiscall (StrategyAIAgent* this) -- the SECOND research producer tried by phase 18, entered with `mov eax,esi; call` (the receiver is in EAX at the call site, so the real convention is not confirmed). Body unread; probed for reachability only",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08, from the call site at 0x006caf9d"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_ResearchFallbackIndex",
|
|
"addr": "0x006b36e0",
|
|
"convention": "thiscall",
|
|
"prototype": "int __thiscall (StrategyAIAgent* this) -- SelectResearchTarget's fallback index source, masked with 0x80000007 and used to index the eight-entry .data tables at 0x00a1a544 (2 0 2 0 2 1 2 0) and 0x00a1a564 (0 2 0 2 1 0 2 0). It is NOT an RNG draw: it reads ClientPlayer+0xf4 and calls 0x0080da80. A hit here means the research pick came from the three-arm rotation, not from the candidate list",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08"
|
|
},
|
|
{
|
|
"name": "StrategyAIAgent_ResearchFallbackArm",
|
|
"addr": "0x006c8670",
|
|
"convention": "fastcall",
|
|
"prototype": "void* __fastcall (void** outSlot /*ecx*/, StrategyAIAgent* agent) -- one arm of SelectResearchTarget's three-arm fallback rotation; the arm index is (i + table[seed]) % 3 for i = 0,1,2 and the first non-null answer wins. Its outcome space is therefore at most three",
|
|
"status": "unverified",
|
|
"source": "findings/subsystems/ai-order-capture.md -- lane L4 2026-09-08"
|
|
}
|
|
]
|
|
} |