sots-re/ghidra/addresses.d/lane-ai2.json
alex 232397aa12 lane AI2: the AI's task selection loop, the priority table, and the order-method -> TurnCommands map
The strategic AI does not search or score. Once a turn (Process Turn phase 20, 0x006cf630) it
rebuilds a candidate task list -- which families it builds at all is a switch on the player's
SPECIES, four arms, and the NPC arm builds nothing -- sorts it by a per-task-type priority, and
walks it twice calling each task's Execute(agent, pass) with pass 0 then 1.

Closed from lane AI1's open list:
  * the selection loop (its item 2) and the whole ordering policy: a 33-entry priority table at
    0x00691f00 plus five named overrides;
  * IAITask's unnamed pure virtuals (item 1) -- seven, not eight: GetTypeId, GetTargetA/B,
    Execute, IsFinished, GetTypeName, Describe;
  * the order-method -> list mapping (item 5): all 27 of lane Q's lists and all six prologue
    gates now have a named producer, and list 14 -- lane Q's "observed but not understood" --
    is the AI's fleet order, two elements per fleet;
  * g_CurrentClientIndex (item 6): a stack pointer with exactly two writers, pushed around the
    whole AI turn by StrategyAIAgent::OnEvent;
  * the think-time throttle (item 7): AIProcessMinTime is a trailing Sleep, not a compute
    budget. Every pending AI player's turn runs back-to-back inside one Update. The clean
    "all AI orders in before the human's End Turn" ordering HOLDS, and Rung B is not at risk;
  * Broadcast -> OnAIPacket, read to the call -- AI1's one inferred hop is now verified.

Corrections: 31 concrete task classes, not 34; 26 order methods, not 21; seven pure virtuals,
not eight. lane-ai1.json's Broadcast and g_CurrentClientIndex entries upgraded in place.

Open and said so: slots 11/12/13 unnamed, the nine goal tasks' bodies unread, the two-pass
meaning inferred, the StrategyApp pending-AI enqueue site not found, nothing run under an
instrument. Four predictions in section 9, P1 being a ModCount prediction.

ghidra/addresses.d/lane-ai2.json: 25 entries, 1031 -> 1056, no duplicates.
2026-09-08 14:43:41 -04:00

204 lines
20 KiB
JSON

{
"entries": [
{
"name": "StrategyAIAgent_RebuildAndRunTasks",
"addr": "0x006cf630",
"convention": "thiscall",
"prototype": "void __thiscall Game::StrategyAIAgent::RebuildAndRunTasks() -- THE TASK SELECTION LOOP. Phase 20 of the AI Process Turn body (called from 0x006cfc94). Order: (1) 0x006b34f0(this, &this->+0x2f8) refreshes the per-fleet world model over client->+0x60..+0x64; (2) PruneTasks 0x006b3640(this); (3) `switch (client->+0x150->+0x5c)` over 0..6 through the 7-entry jump table at 0x006cf880 -- four distinct arms (case 0/2/3/6 -> 0x006cf665, case 1 -> 0x006cf6f8, case 5 -> 0x006cf75d, case 4 -> NOTHING) each calling a fixed, source-ordered list of per-task-family creators in 0x006ab6c0..0x006c0e60; (4) if this->+0x8, that object's vt[1](this); (5) PruneTasks again; (6) TaskList_SortByPriority 0x006bf9c0(&this->+0x31c, player->+0x5c) -- std::list::sort, STABLE, DESCENDING by IAITask::vt[10](); (7) RunTaskList 0x006b3320(this, &this->+0x31c, 0, &this->+0x2e8) then again with pass=1; (8) PruneTasks again; (9) if player->+0x2d8 in {1,2}, 0x006cf4c0 then 0x006cf590; (10) 0x006a8eb0(this) -- reaches client order method 0x00763a20; (11) if this->+0x124, cl_SetResearchRate(*(float*)0x009e2ea0) and clear the flag; (12) this->+0x128 = 0",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#1 -- lane AI2 2026-09-08, instruction-stream read of dumps/sots.exe (objdump -b binary -m i386 -M intel, swept to the next function start per rule 17); jump table read from the image"
},
{
"name": "StrategyAIAgent_TaskListSortByPriority",
"addr": "0x006bf9c0",
"convention": "thiscall",
"prototype": "void __thiscall std::list<Game::IAITask*>::sort(Pred) on the agent's task list -- the MSVC 7.1 binlist sort: eh_vector_constructor_iterator over 26 (0x1a) 0x0c-byte std::list bins, the `_Bin == 25` overflow branch, merge helper 0x006a9850. The Pred is a 4-byte functor carrying player->+0x5c, and the inlined comparison IGNORES it: the whole ordering key is IAITask::vt[10]() (see AITask_slot10_GetPriority). std::list::sort is STABLE, so ties keep creation order -- which makes the per-arm creator call order in RebuildAndRunTasks part of the answer, not an implementation detail",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#1 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "StrategyAIAgent_TaskListMerge",
"addr": "0x006a9850",
"convention": "thiscall",
"prototype": "void __thiscall std::list<Game::IAITask*>::merge(list& right, Pred) -- RET 8. THE COMPARISON, inlined at 0x006a9879..0x006a9895: `a = A->vt[10](); b = B->vt[10](); if (a > b) splice A before B;` (`cmp [ebp-0x10],eax / jle` -- so a strictly-greater test, descending order, ties left alone). Both calls are __thiscall with no stack args, which pins IAITask::vt[10] as `int GetPriority(void)`",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#1 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "StrategyAIAgent_RunTaskList",
"addr": "0x006b3320",
"convention": "cdecl",
"prototype": "void (Game::StrategyAIAgent* agent, std::list<IAITask*>* tasks, int pass, std::vector<IAITask*>* pending) -- THE TASK EXECUTION LOOP, run twice per turn with pass = 0 then 1. For each node of `tasks` in list order (i.e. priority order after the sort): task = node->value; erase task from `pending` (std::find 0x0069af70 + memmove compaction); push_back task onto the agent's active-task stack at agent->+0x12c/+0x130/+0x134 (growth helper 0x00483410, \"vector<T> too long\"); call `task->vt[5](agent, pass)`; then if back() is still that task, pop_back. The push/pop bracket makes agent->+0x12c a task CALL STACK, which is how goal tasks (AITColonizeGoal, AITInvadeGoal, AITEscortGateInvadeGoal) nest sub-tasks",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#1 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "StrategyAIAgent_PruneTasks",
"addr": "0x006b3640",
"convention": "cdecl",
"prototype": "void (Game::StrategyAIAgent* agent) -- `for each node of agent->+0x31c: task = node->value; if (task->vt[6](agent)) { erase task from the vector agent->+0x2e8..+0x2ec; agent->RemoveTask(task) 0x006af900; }`. Called three times inside RebuildAndRunTasks: before creation, after creation, and after execution. This is what pins IAITask::vt[6] as `bool IsFinished(StrategyAIAgent*)` -- returning true destroys the task",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "StrategyAIAgent_RemoveTask",
"addr": "0x006af900",
"convention": "thiscall",
"prototype": "void __thiscall Game::StrategyAIAgent::RemoveTask(Game::IAITask* task) -- unlinks the task from four containers: the master list at this+0x31c and the vector at this+0x2e8 (via 0x006ae930), the 0x0c-stride vector at this+0x1cc (via 0x006a95e0), and the 0x20-stride vector at this+0x208..+0x20c (find 0x0069b0e0 then a rep-movsd compaction of 0x20-byte records)",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "AITask_PriorityForType",
"addr": "0x00691f00",
"convention": "cdecl",
"prototype": "int (int taskTypeId) -- the AI's whole task-ordering policy as one switch: `if ((unsigned)id > 0x20) return 0; jmp [id*4 + 0x00691ffc]`, 33 arms each a single `mov eax,imm32; ret`. Values (id -> priority): 0 AITSteamroll 1250, 1 AITExplore 600, 2 AITExploreInForce 550, 3 AITEscortGate 700, 4 AITEscortGateInvade 400, 5 AITEscortGateInvadeGoal 950, 6 AITDeployGateAt 1400, 7 AITColonize 900, 8 AITColonizeGoal 970, 9 AITColonizeAt 1300, 0xa AITInvade 500, 0xb AITInvadeGate 1000, 0xc AITInvadeGoal 930, 0xd (no class) 200, 0xe AITDefendColonyIncoming 1100, 0xf (no class) 300, 0x10 AITDefendGateIncoming 1200, 0x11 AITKillEasterEgg 800, 0x12 AITInterceptEnemy 850, 0x13 AITMining 350, 0x14 AITMiningReturn 375, 0x15 AITAttackBlockade 100, 0x16 AITAdvanceIdleShips 0, 0x17 AITStockFreighters 50, 0x18 AITRespondAttackSystem 980, 0x19 AITRespondDefendSystem 990, 0x1a AITNodeBore 1275, 0x1b AITBuildStations 910, 0x1c AITBuildPoliceShips 75, 0x1d AITBuildDeepScanShips 60, 0x1e AITRaid 399, 0x1f AITRetrieveArtifact 1, 0x20 AITReturnArtifact 2. The last two table entries are DEAD: both artifact classes override vt[10] with fixed 0x4ec/0x4ed (1260/1261). Ids 0xd and 0xf have priorities but no surviving class",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#3 -- lane AI2 2026-09-08, jump table and all 33 arms read from the image"
},
{
"name": "AITask_slot10_GetPriority",
"addr": "0x00694220",
"convention": "thiscall",
"prototype": "int __thiscall Game::IAITask::GetPriority() -- vtable slot 10, the DEFAULT implementation, used by 21 of the 31 concrete tasks (5 more reach it through the thunk 0x00682650): `return AITask_PriorityForType(this->vt[1]());`. Overrides: AITInvade 0x00683670 and AITEscortGateInvade 0x006835e0 return the globals at 0x00a1795c / 0x00a17960 when `this->+0x4 & 1` is clear, else default; AITAttackBlockade 0x00685600 scans a 0xc-stride vector at this->+0x8->+0x1cc for a related task and filters on its type id (1, 2, 7, 0x11, ...); AITRetrieveArtifact 0x005465a0 returns 0x4ec and AITReturnArtifact 0x00546800 returns 0x4ed unconditionally",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#3 -- lane AI2 2026-09-08, instruction-stream read of every slot-10 body"
},
{
"name": "AITask_vt_slot1_GetTypeId",
"offset": "0x04",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 1 (byte offset 4) -- `int GetTypeId(void)`, PURE in the interface (vftable 0x009fa354), and in all 31 concrete classes a single 16-byte `mov eax,imm32; ret` returning a value in 0..0x20. It is the key into AITask_PriorityForType and the discriminator every cross-task filter uses",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08; all 31 bodies read from the image"
},
{
"name": "AITask_vt_slot5_Execute",
"offset": "0x14",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 5 (byte offset 0x14) -- `void Execute(Game::StrategyAIAgent* agent, int pass)`, RET 8, PURE in the interface. THE task body: 27 distinct implementations across the 31 classes, 48..288+ bytes each, dispatched from StrategyAIAgent_RunTaskList 0x006b3320+0x167 (`mov edx,[task_vt+0x14]; push pass; push agent; mov ecx,task; call edx`). This is the only slot from which a client order method is ever reached",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "AITask_vt_slot6_IsFinished",
"offset": "0x18",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 6 (byte offset 0x18) -- `bool IsFinished(Game::StrategyAIAgent* agent)`, RET 4, PURE in the interface. Called ONLY from StrategyAIAgent_PruneTasks 0x006b3640+0x2b; true means unlink and destroy. 27 distinct implementations; the two shared trivials are 0x005eda80 `return false` (AITAdvanceIdleShips, AITSteamroll -- never retire) and, e.g., AITBuildDeepScanShips 0x00682fe0 `return !0x0069a7f0(agent, 0x20, 0)`",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "AITask_vt_slot8_GetTypeName",
"offset": "0x20",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 8 (byte offset 0x20) -- `const char* GetTypeName(void)`, PURE in the interface; in all 31 classes a 16-byte `mov eax,<rdata ptr>; ret` returning the class's own unmangled name (\"AITRaid\", \"AITColonizeGoal\", ...). Pairing slot 1 with slot 8 across the 31 vtables yields the complete task-type enum with no gaps except ids 0x0d and 0x0f",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08; all 31 bodies read from the image"
},
{
"name": "AITask_vt_slot9_Describe",
"offset": "0x24",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 9 (byte offset 0x24) -- `void Describe(void)`, no args, PURE in the interface. Every implementation is a single log call of the form `Log(\"<TypeName>: %s -> %s\\n\", NameOf(vt2()), NameOf(vt3()))` (AITAdvanceIdleShips prints the literal \"AITAdvanceIdleShips: n/a -> n/a\\n\"). It is what pins slots 2 and 3 as the task's source and destination target getters",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "AITask_vt_slot7_OnObjectDestroyed",
"offset": "0x1c",
"convention": "offset",
"prototype": "Game::IAITask vtable slot 7 (byte offset 0x1c) -- `void OnObjectDestroyed(void* obj)`, RET 4, NOT pure: the interface default 0x005f8ac0 is a bare `ret 4`. The dominant override 0x00682540 (17 of 31 classes) nulls whichever of this->+0xc and this->+0x8 holds an object whose +0x4 equals the argument -- i.e. it drops dangling target references",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#2 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "PushCurrentClient",
"addr": "0x00578020",
"convention": "cdecl",
"prototype": "void (Game::StrategyClient* c) -- `g_StrategyClients[g_CurrentClientIndex + 1] = c; ++g_CurrentClientIndex;` (written as `mov [eax*4+0x00ae47e8],ecx` with eax = the old index, then `inc [0x00ae4808]`). So 0x00ae47e4 is a STACK of client scopes and 0x00ae4808 is its stack pointer, not a plain index -- the whole cl_* family reads `[idx*4 + 0x00ae47e4]`, i.e. the top of stack. 18 callers; the AI-relevant one is StrategyAIAgent::OnEvent 0x006d0ad0, which brackets the ENTIRE AI turn in Push(agent->+0x10) / Pop. That is the mechanism by which every cl_* call the AI makes -- cl_Chance, cl_RandRange, cl_SetResearchRate, cl_EndTurn -- lands on that AI's own client and its own RNG at client+0x134",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#6 -- lane AI2 2026-09-08, instruction-stream read; closes lane AI1's open item 6 on who sets g_CurrentClientIndex"
},
{
"name": "PopCurrentClient",
"addr": "0x00578040",
"convention": "cdecl",
"prototype": "void () -- `--g_CurrentClientIndex;`, the two-instruction pop matching PushCurrentClient 0x00578020. 18 callers, the same set. No other instruction in the image writes 0x00ae4808: an image-wide absolute-reference scan finds 42 referencing functions and every one of the other 40 only READS it",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#6 -- lane AI2 2026-09-08, image-wide absolute-reference scan plus instruction-stream read"
},
{
"name": "StrategyApp_RunPendingAITurns",
"addr": "0x00838c60",
"convention": "thiscall",
"prototype": "void __thiscall Game::StrategyApp::RunPendingAITurns() -- called EVERY FRAME from StrategyNetworkClient::Update 0x007842b0+0xf7 with ECX = the StrategyApp singleton 0x00b29f98. `if (this->+0x1c == this->+0x20) return;` (empty pending-AI-player-id vector). Otherwise: t0 = clock 0x008d0b70; show the Game::AIProcessingDialog at 0x00b1149c if it exists; then FOR EVERY entry of +0x1c..+0x20 IN INDEX ORDER, find the client in +0xc..+0x10 whose client->+0x148 matches, update the dialog with client->+0x150, and StrategyClient::RaiseEvent 0x00783ee0(client, 0x26 /*SEResumePlaying*/, &ev) -- which is what runs that AI player's whole turn. The loop has no early exit and no frame yield. After it, the pending vector is emptied, and only THEN: `remaining = this->+0x2c - (clock() - t0); if (remaining > 0) Sleep((int)(remaining * 1000));` before hiding the dialog. THE THROTTLE IS A TRAILING SLEEP, NOT A COMPUTE BUDGET: AIProcessMinTime cannot change a decision and cannot defer an AI turn across frames",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#7 -- lane AI2 2026-09-08, instruction-stream read; settles lane AI1's open item 7"
},
{
"name": "StrategyApp_off_AIProcessMinTime",
"offset": "0x2c",
"convention": "offset",
"prototype": "float -- Game::StrategyApp+0x2c, in SECONDS. Set once in StrategyApp::CreateGame 0x00888e80+0x90: the GameOptions key \"AIProcessMinTime\" (string at 0x00a32e30) is read through 0x00898bc0, converted with the CRT string-to-long at 0x009dd320, `fild`ed and divided by the double 1000.0 at 0x009e22f8, then clamped at 0 before `fst [esi+0x2c]`. Its only consumer is the trailing Sleep in StrategyApp_RunPendingAITurns 0x00838c60+0x10e",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#7 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "StrategyApp_off_PendingAIPlayers",
"offset": "0x1c",
"convention": "offset",
"prototype": "std::vector<int> -- Game::StrategyApp+0x1c.._+0x20, the queue of player net ids whose AI turn is due. Drained in index order by StrategyApp_RunPendingAITurns 0x00838c60, which is therefore the ONLY thing that decides in what order the AI players are stepped and hence the order their TurnCommands blocks reach the host. WHO PUSHES TO IT WAS NOT FOUND by this lane -- neither an absolute-reference scan for the singleton nor an enumeration of the methods called on it located the enqueue site",
"status": "mapped",
"source": "findings/subsystems/ai-task-system.md#6 -- lane AI2 2026-09-08; the drain is instruction-verified, the fill is open"
},
{
"name": "ClientOrder_FleetTask",
"addr": "0x007634d0",
"convention": "thiscall",
"prototype": "bool __thiscall Game::StrategyClient::<fleet task order>(void* fleetObj, int mode, bool flag) -- RET 0xc. `if (this->+0x15c) return false;` then builds the 12-byte record {i32 fleetId = fleetObj->+4, i32 mode, bool flag}, calls the local-apply/validate 0x00821cf0(this->+0x148 /*playerId*/, &rec), and on true appends it to the accumulating TurnCommands at this+0x160 via the LIST 14 adder 0x00842a00. THIS IS LANE Q'S UNEXPLAINED LIST 14. The AI reaches it through 0x006987e0, which calls it TWICE per fleet -- (fleet, 0, true) then (fleet, 1, true) -- and 0x00842a00 keys its insert-or-update on BOTH fleetId (node+0x8) and mode (node+0xc), so an AI fleet order deposits TWO list-14 elements. The UI path (0x005e6fa0) and OnResumePlaying 0x00777480 also call it",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "TurnCommands_AddList14",
"addr": "0x00842a00",
"convention": "thiscall",
"prototype": "void __thiscall Game::TurnCommands::<add list-14 entry>(const rec* r) -- operates on the std::list at this+0x10c, which is lane Q's LIST 14 (member 14 of 27, +0x70 + 14*0x0c - 0x0c = +0x10c). Scans for a node with node->+0x8 == r->fleetId AND node->+0xc == r->mode; if found, overwrites node->+0x8/+0xc/+0x10 in place; otherwise push_back via 0x00766c20. The node payload is exactly lane Q's observed element record {i32, i32, bool}",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "AI_IssueFleetTask",
"addr": "0x006987e0",
"convention": "cdecl",
"prototype": "void* (Game::StrategyAIAgent* agent, std::vector<void*>* route, void* dest) -- the AI's single fleet-order bridge and the busiest AI->TurnCommands edge in the module. Pushes `dest` through cl_* helper 0x00578cd0, opens a route build with 0x0057b4a0, appends each element of `route` with 0x0057aa50, closes with 0x0057b4d0, resolves the resulting handle through 0x008f4b30, and if non-null calls ClientOrder_FleetTask 0x007634d0 twice: (obj, 0, true) then (obj, 1, true). Three callers -- 0x006b76a0, 0x006c15e0, 0x006c16c0 -- which between them are reached from the Execute (slot 5) body of 24 of the 31 task classes",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08, instruction-stream read plus a depth-4 direct-call closure over the AI band"
},
{
"name": "ClientOrder_SetSystemRates",
"addr": "0x00763270",
"convention": "thiscall",
"prototype": "Game::StrategyClient order method appending to TurnCommands LIST 5 (+0xa0, the planetary-budget/system-rates list lane O observed in zuul-turn17-orders2.sav) through helper 0x008490b0, which is `add ecx,0xa0; call 0x00843fa0`. Called from the AI at 0x0069dd80 (AI Prepare Turn's one-shot NextInt scheduler) and from seven non-AI sites including the cl_* façade at 0x00579110. It is one of the five order methods lane AI1's 21-row table missed",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "ClientOrder_Colonize",
"addr": "0x00769640",
"convention": "thiscall",
"prototype": "Game::StrategyClient order method appending to TurnCommands LIST 7 (+0xb8, lane O's `{i32 shipId, i32 w}` colonize list) through helper 0x00842890. Called from the AI at 0x006af790 -- phase 32 of the AI Process Turn body, i.e. AFTER cl_EndTurn -- and from ten non-AI sites, seven of which are the cl_* façade family 0x00578fc0..0x005790e0. Also missing from lane AI1's table",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08, instruction-stream read"
},
{
"name": "TurnCommands_off_ListBase",
"offset": "0x70",
"convention": "offset",
"prototype": "Game::TurnCommands -- the first of the 27 std::list members lane Q enumerated, stride 0x0c, so list N (1-based, as lane Q numbers them) is at +0x70 + (N-1)*0x0c and the last, list 27, is at +0x1a8. Recorded here because the order-method -> list mapping in ai-task-system.md#4 is expressed entirely in these offsets: the adder for list N is the function whose first `this`-relative access is +0x70 + (N-1)*0x0c",
"status": "verified",
"source": "findings/subsystems/ai-task-system.md#4 -- lane AI2 2026-09-08; agrees with lane Q's findings/objects/turncommands-block.md section 3"
}
]
}