Spawn system
Goal
A generic SDK R.spawn(template, transform) that instantiates any entity
(rat/pet/enemy/prop) at runtime. The loader already exposes typed FFI by symbol
(_internal.resolve/call), so the SDK side is light once the spawn fn + ABI
is known — the whole cost is the RE.
Pipeline
Entities are not spawned by a global spawn(template, pos). The path is a
generic spawner component that holds a candidate-template list and instantiates
on tick:
- Picker —
Enemy_RuntimeSpawnPicker(FUN_140330db0). Enumerates enemy defs from the class registry (Registry_EnemyDefinition_desc0x141470208), filters by tier/flags/tribe, weighted-random picks, then lazily resolves each picked def’s entity ref viaResourceRef_Resolve(FUN_140491690) → resolved template ptr atdef+0x2b8. - Spawn-data entry — an
oCEntitySettingsRootPtr(0x30 bytes): allocFUN_140338fa0, fillFUN_1406e5ef0(typed-ref setter — stores the resolved template atentry+0x18, refcounts it, registers on the template’s resolve/unresolve notify lists). A live, refcounted, auto-invalidating handle. - Spawner candidate vector on the component —
spawnerCpnt+0x68data,+0x70count,+0x74cap (stride 8). The picker push_backs here. The prepare stepFUN_140330c30resizes two parallel arrays at+0x80/+0xa0and stores an RNG seed at+0xb8. - Instantiation — NOT located. A later component virtual consumes
+0x68/+0x80and creates live entities at the spawner transform (a 0x40-byte 4×4 f32 matrix, not bare xyz). This is the remaining RE target.
Why the instantiator is unreachable
Three Ghidra-MCP passes plus two headless passes converged: the runtime
create-entity routine is vtable-dispatched on the runtime component class
(oCEntityCpntEntitySpawner / oCSpawnerGoEntityCollector), whose metaclass/
vtable registrations sit in code regions Ghidra left undefined. The seeds
that looked promising resolved to Settings class registrars (reflection
metadata only — no instantiation):
0x140228547→ registrar foroIEntityCollectorSettings(hash0x12cccf23).0x1402fadb9→ registrar foroIEntityCpntSettings(hash0xc608329).
The oCEntitySpawnerGo cluster (FUN_140745620 build / FUN_140745200
activate) is the editor/preview gizmo layer — it builds a preview sub-level,
never a gameplay entity. Surgical static RE is exhausted.
Levers (cheapest first)
- A. Hijack a live spawner. Enemy camps already hold configured spawners with
valid transform/faction/scene context. Append a resolved-template entry to an
existing spawner’s
+0x68vector, then let its tick instantiate with correct context. (Caveat: appending to the selector’s+0x68and ticking it frees entries — the live spawner must be the instantiating component, not the selector.) - B. Call the instantiator directly. Needs the create fn + its full context ABI RE’d first.
- C. Cold-construct a spawner. Most general, most work.
Recommended next step — dynamic trace
Static RE can’t pin the create fn. Detour the selector prepare virtual
FUN_140330c30 (clean unique pattern, like hook_skills.cpp) to capture a live
spawnerCpnt + its owning entity at runtime, then trace the subsequent vtable
calls to the actual create call site with real pointers. Needs a loader build +
playtest.
Symbols
In data/symbols.json: Enemy_RuntimeSpawnPicker FUN_140330db0,
Registry_EnemyDefinition_desc 0x141470208, the ResourceRef resolve path.
To add once located: the entity factory, SpawnEntry_Alloc FUN_140338fa0,
SpawnEntry_SetTemplate FUN_1406e5ef0, oCEntitySpawner::vftable 0x140f023f8.