Creature spawning (creature_spawn_template / 0x00430af0)¶
creature_spawn_template(template_id, pos_xy, heading) is the primary translation layer from
"spawn ids" (quests, tutorial timelines, and other scripted spawners) to initialized creature_t
entries.
It is not a static struct/table: it always performs base creature initialization, runs a large template switch, may allocate additional creatures and/or spawn-slot entries, and then applies shared tail modifiers (difficulty/hardcore, demo gating, etc).
The port (creature_spawn_template in src/crimson/creatures/spawn.py) mirrors that shape: it writes
each creature straight into the slot the allocator hands out, in native order, so fields a template
leaves alone keep the recycled slot's values.
Spawn id name provenance¶
The canonical spawn-id names combine the Windows template_id values with developer-facing creature
names recovered from the PS4 1.00 remake's data/creatures/creature-variants.xml.
The remake's legacy_variant_index is not the Windows spawn id. We cross-mapped fixed creatures by
type and the exact health, speed, reward, damage, and size fingerprint (remake scale * 64), then
retained the native Windows value as each identifier's hexadecimal suffix:
| Windows id | Canonical name | Remake variant |
|---|---|---|
0x0f |
ALIEN_GHOST_0F |
AlienGhost |
0x21 |
ALIEN_HIDDEN_1_21 |
AlienHidden1 |
0x22 |
ALIEN_HIDDEN_2_22 |
AlienHidden2 |
0x23 |
ALIEN_HIDDEN_3_23 |
AlienHidden3 |
0x25 |
ALIEN_SMALL_GREEN_MAN_25 |
AlienSmallGreenMan |
0x26 |
ALIEN_SMALL_GRAY_26 |
AlienSmallGray |
0x27 |
ALIEN_BONUS_CARRIER_27 |
AlienBonusCarrier |
0x29 |
ALIEN_BIG_GRAY_29 |
AlienBigGray |
0x2b |
ALIEN_DEADLY_FAST_2B |
AlienDeadlyFast |
0x3a |
SPIDER_BOSS_3A |
SpiderBoss |
0x3c |
SPIDER_PLASMA_SHOOTER_3C |
SpiderPlasmaShooter |
0x40 |
SPIDER_SMALL_BLUE_40 |
SpiderSmallBlue |
0x42 |
ZOMBIE_SMALL_WHITE_42 |
ZombieSmallWhite |
Spawner names are cross-mapped by their den family, child variant, spawn interval, and surviving stat fingerprints. Some balance fields changed in the remake, so these are semantic names rather than a claim that the records are byte-for-byte identical:
| Windows id | Canonical name | Remake variant |
|---|---|---|
0x07 |
DEN_ALIEN_BASIC_07 |
DenAlienBasic |
0x08 |
DEN_ALIEN_BASIC_SLOWER_08 |
DenAlienBasicSlower |
0x09 |
DEN_ALIEN_WEAK_SMALL_09 |
DenAlienWeakSmall |
0x0a |
DEN_SPIDER_BASIC_0A |
DenSpiderBasic |
0x0b |
DEN_SPIDER_PLASMA_SHOOTERS_0B |
DenSpiderPlasmaShooters |
0x0c |
DEN_LIZARD_WEAK_0C |
DenLizardWeak |
0x0d |
DEN_LIZARD_WEAK_SLOWER_0D |
DenLizardWeakSlower |
0x10 |
DEN_SPIDER_WEAK_10 |
DenSpiderWeak |
Inputs¶
template_id(akaparam_1): spawn id used by quest tables and other spawners.pos_xy(akaparam_2): spawn position (two floats).heading(akaparam_3): radians; sentinel-100.0means "randomize heading".- Globals consulted:
- RNG stream:
crt_rand()(MSVCRTrand()). demo_mode_active: skips the spawn burst effect when nonzero.terrain_texture_width/terrain_texture_height: bounds check for the burst effect.config_blob.hardcoreandquest_fail_retry_count(difficulty level): final stat modifiers.
Outputs and side effects¶
-
Returns a pointer into
creature_pool(creature_t *). Some templates return the last creature allocated (for formations), not necessarily the base creature. -
May allocate additional
creature_poolentries (formation spawns, escorts). - May allocate and configure spawn-slot entries (deferred child spawns driven by
creature_update_all). - May spawn a burst effect at the spawn position (skipped in demo mode or when out of bounds).
Grid templates 0x14..0x18 allocate one root plus 27 children: nine columns
at x offsets 0, -64, ..., -512, each with y offsets 128, 192, 256.
The vertical step is 64 units. Allocation proceeds down each column before
moving to the next. The last child receives the requested heading and shared
tail modifiers. Grids 0x14..0x17 also reach the native fallback that sets
that child's health to 20; bronze grid 0x18 retains health 260 before
difficulty modifiers. See the native replay evidence.
Algorithm sketch (high level)¶
1) Base init (always)¶
- Allocates a creature slot (
creature_alloc_slot()), then writes base fields: ai_mode = 0,pos_xy,vel_xy = 0active = 1,state_flag = 1- collision defaults (
plague_infected = 0,dot_tick_timer = 0) death_timer = 16,attack_cooldown = 0- Seeds a transient random heading early:
crt_rand() % 0x13a * 0.01. - If
heading == -100.0, randomizes the final heading:crt_rand() % 0x274 * 0.01. creature_alloc_slot()itself consumes RNG to seed per-creature defaults (notablyphase_seed).
Full pool: the phantom slot¶
creature_alloc_slot() has no failure path. When all 0x180 slots are active it logs "No free
creatures to spawn!" (with cv_verbose) and returns 0x180, one past the pool, without clearing flags
or drawing a phase seed. Callers write that creature anyway, into creature_pool[0x180]: the entry
creature_pool_global_init still covers (it initializes 0x181 entries) but nothing iterates.
- A formation spawned with
kfree slots puts its root and firstk - 1members in the pool; every later member overwrites the phantom slot, and the tail applies to it when it is the last member. - The phantom slot's fields persist between writes, flags included: a spawner that landed there keeps
flag
0x4for later spawns, whose tail then adjusts its spawn slot's interval again. - Links and spawn-slot owners can name the phantom slot (
0x180).creature_update_allnever visits it, so its spawn slot never ticks and is never freed untilgameplay_reset_state, butplayer_apply_move_with_spawn_avoidancestill blocks the player around its position. - A phantom slot that carries flag
0x4with a formation link inlink_indexmakes the tail index the spawn-slot table out of bounds; the port skips that write.
2) Template switch (template-specific)¶
Large switch/if-chain on template_id assigns template-specific constants and behavior:
-
Stats:
type_id,flags,health,move_speed,reward_value,size,tint_rgba,ai_mode, and various AI/link fields. -
Formation spawners: allocate N linked children and arrange them using circular offsets (
cos/sin) and AI link modes (e.g.ai_mode = 3withlink_index = parent). -
Spawn-slot spawners: allocate a slot (
creature_spawn_slot_alloc()), store the slot index inlink_index, and configurecreature_spawn_slot_*arrays (timer/count/limit/interval/template/owner).
3) Tail modifiers (shared end-of-function)¶
Applied after the template switch to the returned creature:
- If not in demo mode and inside terrain bounds:
effect_spawn_burst(pos, 8). max_health = health.-
Note: difficulty/hardcore scaling applies to
healthafter this assignment, somax_healthretains the pre-scaled value. -
Spider SP1 special case: when
type_id == 3and flags do not include0x10or0x80, sets0x80, clearslink_index, and applies amove_speed *= 1.2buff. -
Template
0x38special case: in hardcore, appliesmove_speed *= 0.7. - Overwrites
headingwith the final (possibly randomized) heading argument. - Difficulty / hardcore scaling:
-
Non-hardcore:
- For flag
0x4spawners:spawn_slot_interval += 0.2. - If
quest_fail_retry_count > 0, scales reward/speed/contact/health, and for flag0x4spawners addsmin(3.0, difficulty * 0.35)tospawn_slot_interval.
- For flag
-
Hardcore:
- Clears difficulty (
quest_fail_retry_count = 0). - Buffs speed/contact/health.
- For flag
0x4spawners:spawn_slot_interval -= 0.2clamped to>= 0.1.
- Clears difficulty (
Spawn template ids (direct type/flags map)¶
The large template switch inside creature_spawn_template assigns type_id and sometimes
flags. The table below lists only these direct assignments (useful for labeling).
It does not capture randomized parameters, formation spawns, spawn slots, or tail modifiers.
Spawn id 0x02 is SPAWN_ID_UNUSED_02 (spawn_id_t,
tools/match/include/crimsonland_gameplay.h): it has no branch in
decomp/1.9/crimsonland/quests/creature_spawn_template.cpp and is omitted below.
Generated by uv run scripts/gen_spawn_templates.py.
| Spawn id (template_id) | Type id | Creature | Flags (creature_flags) | Anim note |
|---|---|---|---|---|
0x0 |
0 |
zombie |
0x44 |
long strip (0x40 overrides 0x4) |
0x1 |
4 |
spider_sp2 |
0x8 |
|
0x3 |
3 |
spider_sp1 |
`` | |
0x4 |
1 |
lizard |
`` | |
0x5 |
4 |
spider_sp2 |
`` | |
0x6 |
2 |
alien |
`` | |
0x7 |
2 |
alien |
0x4 |
short strip (ping-pong) |
0x8 |
2 |
alien |
0x4 |
short strip (ping-pong) |
0x9 |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xa |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xb |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xc |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xd |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xe |
2 |
alien |
0x4 |
short strip (ping-pong) |
0xf |
2 |
alien |
`` | |
0x10 |
2 |
alien |
0x4 |
short strip (ping-pong) |
0x11 |
1 |
lizard |
`` | |
0x12 |
2 |
alien |
`` | |
0x13 |
2 |
alien |
`` | |
0x14 |
2 |
alien |
`` | |
0x15 |
2 |
alien |
`` | |
0x16 |
1 |
lizard |
`` | |
0x17 |
3 |
spider_sp1 |
`` | |
0x18 |
2 |
alien |
`` | |
0x19 |
2 |
alien |
`` | |
0x1a |
2 |
alien |
`` | |
0x1b |
3 |
spider_sp1 |
`` | |
0x1c |
1 |
lizard |
`` | |
0x1d |
2 |
alien |
`` | |
0x1e |
2 |
alien |
`` | |
0x1f |
2 |
alien |
`` | |
0x20 |
2 |
alien |
`` | |
0x21 |
2 |
alien |
`` | |
0x22 |
2 |
alien |
`` | |
0x23 |
2 |
alien |
`` | |
0x24 |
2 |
alien |
`` | |
0x25 |
2 |
alien |
`` | |
0x26 |
2 |
alien |
`` | |
0x27 |
2 |
alien |
0x400 |
bonus_id=WEAPON (3), amount_override=5 (packed in link_index) |
0x28 |
2 |
alien |
`` | |
0x29 |
2 |
alien |
`` | |
0x2a |
2 |
alien |
`` | |
0x2b |
2 |
alien |
`` | |
0x2c |
2 |
alien |
`` | |
0x2d |
2 |
alien |
`` | |
0x2e |
1 |
lizard |
`` | |
0x2f |
1 |
lizard |
`` | |
0x30 |
1 |
lizard |
`` | |
0x31 |
1 |
lizard |
`` | |
0x32 |
3 |
spider_sp1 |
`` | |
0x33 |
3 |
spider_sp1 |
`` | |
0x34 |
3 |
spider_sp1 |
`` | |
0x35 |
4 |
spider_sp2 |
`` | |
0x36 |
2 |
alien |
`` | |
0x37 |
4 |
spider_sp2 |
0x100 |
|
0x38 |
3 |
spider_sp1 |
0x80 |
|
0x39 |
3 |
spider_sp1 |
0x80 |
|
0x3a |
3 |
spider_sp1 |
0x10 |
projectile_type=9 |
0x3b |
3 |
spider_sp1 |
`` | |
0x3c |
3 |
spider_sp1 |
0x100 |
projectile_type=26 (packed in orbit_radius) |
0x3d |
3 |
spider_sp1 |
`` | |
0x3e |
3 |
spider_sp1 |
`` | |
0x3f |
3 |
spider_sp1 |
`` | |
0x40 |
3 |
spider_sp1 |
`` | |
0x41 |
0 |
zombie |
`` | |
0x42 |
0 |
zombie |
`` | |
0x43 |
0 |
zombie |
`` |
Notes:
src/crimson/creatures/spawn.pycontains both the spawn-id labeling index (direct type/flags) and the pure plan builder (formations/spawn slots/tail modifiers).
Spawn id sources (call sites)¶
template_id is supplied by a mix of scripted spawners and data tables:
-
demo_setup_variant_0(0x00402ed0),demo_setup_variant_2(0x00402fe0),demo_setup_variant_1(0x004030f0),demo_setup_variant_3(0x00403250): mode setup helpers called fromdemo_mode_start(0x00403390) (hard‑coded spawn ids like0x34,0x35,0x38,0x41,0x24,0x25). -
survival_update(0x00407cd0): milestone spawns using0x12,0x2b,0x2c,0x35,0x38,0x3a,0x3c, and1. Regular enemy waves are spawned viasurvival_spawn_creature(0x00407510), which selects type/stats based onplayer_experience(not a spawn id). Python:survival_updateinsrc/crimson/sim/mode_updates.py,survival_spawn_creature. -
Rush mode (
rush_mode_update,0x004072b0): spawns edge waves viacreature_spawn(type ids2/3), notcreature_spawn_template. Python:rush_mode_updateinsrc/crimson/sim/mode_updates.py,creature_spawn. -
Tutorial timeline (
tutorial_timeline_update,0x00408990): scripted spawns using0x24,0x26,0x27,0x28,0x40. Python:tutorial_timeline_updateinsrc/crimson/tutorial/timeline.py. -
Quest/timeline spawner (
quest_spawn_timeline_update,0x00434250): pulls spawn ids from the table atquest_spawn_table(spawn id at+0x0c, count at+0x14). Python:quest_spawn_timeline_updateinsrc/crimson/quests/timeline.py. -
AI subspawns (
creature_update_all): periodic spawns usingcreature_spawn_slot_table[i].template_id(entry size0x18), which is seeded for some template ids insidecreature_spawn_template(0x00430af0).
Quest spawn table (quest_spawn_table)¶
Quests populate a fixed table at quest_spawn_table (entry size 0x18, count in
quest_spawn_count). quest_spawn_timeline_update (0x00434250) walks the table and
spawns entries whose trigger time has elapsed.
Entry layout (dwords):
| Offset | Field | Notes |
|---|---|---|
| 0x00 | x | base spawn X; if offscreen, the group spreads along Y instead of X. |
| 0x04 | y | base spawn Y. |
| 0x08 | heading | passed as heading to creature_spawn_template. |
| 0x0c | spawn id | cast to int and passed as template_id to creature_spawn_template. |
| 0x10 | trigger time | compared against quest_spawn_timeline (quest clock). |
| 0x14 | count | number of spawns in the group; decremented to 0 after firing. |
Notes:
-
quest_start_selected(0x0043a790) chooses a quest builder from the table atquest_meta_table. The function pointer lives at&quest_selected_builder; when null, it falls back toquest_build_fallback(0x004343e0) (two entries with spawn id0x40, counts 10/0x14, trigger times 500/5000). -
quest_build_zombie_time(0x00437d70),quest_build_lizard_raze(0x00438840), andquest_build_surrounded_by_reptiles(0x00438940) are examples of quest builders that write multiplequest_spawn_tableentries with varying spawn ids and timings.
Repo references¶
- Recovered native source:
decomp/1.9/crimsonland/quests/creature_spawn_template.cpp - Native-oracle tests:
tests/native_oracle/test_spawn_template.py,tests/native_oracle/test_spawn_full_pool.py - Creature pool + spawn-slot fields:
docs/creatures/struct.md - Rewrite model (pure plan builder):
src/crimson/creatures/spawn.py - Survival mode:
survival_updateinsrc/crimson/sim/mode_updates.py,survival_spawn_creatureinsrc/crimson/creatures/spawn.py - Tests:
tests/modes/test_survival_milestones.py,tests/modes/test_survival_wave.py,tests/modes/test_survival_spawn.py - Rush mode:
rush_mode_updateinsrc/crimson/sim/mode_updates.py,creature_spawninsrc/crimson/creatures/spawn.py - Tests:
tests/modes/test_rush_mode_spawn.py - Tutorial timeline:
src/crimson/tutorial/timeline.py tutorial_timeline_update(spawns into the world after the world render, before the level-up check)- Tests:
tests/modes/test_tutorial_timeline_update.py,tests/native_oracle/test_tutorial_timeline.py - Quest timeline:
src/crimson/quests/timeline.py quest_spawn_timeline_update(spawns into the world),quest_spawn_table_empty- Tests:
tests/modes/test_quest_spawn_timeline.py - MSVCRT-compatible RNG for deterministic replays:
src/grim/rand.py(Crand)