SpawnKit
[ALPHA] Runtime enemy spawner for Outward: spawn any donor-scene creature beside the player as a real vanilla hostile, with full AI, aggro, combat, and vanilla death/loot. Ephemeral and save-safe by design. Early software β see README for known issues.
| Date uploaded | a week ago |
| Version | 0.3.1 |
| Download link | CeruleanCutlass-SpawnKit-0.3.1.zip |
| Downloads | 25 |
| Dependency string | CeruleanCutlass-SpawnKit-0.3.1 |
This mod requires the following mods to function
CeruleanCutlass-ForgeKit
Dependency-free dev-tooling for Outward BepInEx mods: file-driven dev command loop, self-test harness, on-screen toasts, player-ready lifecycle wait, embedded/override table loaders, and a shared dev-verb pack (movement/combat/skill/status probes).
Preferred version: 0.4.1CeruleanCutlass-DonorKit
The donor-scene harvest engine for Outward BepInEx mods: additively load any area, clone a live creature into a dormant template, and unload cleanly. Library dependency, installed automatically when another mod (SpawnKit, CompanionKit) needs it.
Preferred version: 0.1.2CeruleanCutlass-CompanionKit
Persistent-creature companion mechanics for Outward mods: clone/brain-strip body factory, navmesh follower, invisible combat anchor, manual combat. Donor-scene harvesting and template caching moved to DonorKit; aggro control to AggroKit.
Preferred version: 0.4.4CeruleanCutlass-NetKit
Shared Photon co-op transport layer for Outward BepInEx mods: channels over one relay, a hello/peer ledger, per-channel counters/heartbeat, PUN diagnostics, and a replicated-record store for consumer state mirroring.
Preferred version: 0.2.2README
SpawnKit β spawn any creature, anywhere
π Full documentation: SpawnKit wiki page
β οΈ Alpha. This is early software: the core spawn/despawn/AI loop is solid and well-tested, but you will hit rough edges (see Known issues below) β roughly a quarter of the creature roster doesn't spawn yet, co-op is built but not yet live-verified on a real two-machine session, and there's an occasional cosmetic terrain glitch. Back up saves you care about before experimenting.
Press F11, pick a creature, spawn it beside you.
SpawnKit gives you a real vanilla hostile β not a dummy or a stand-in. It wanders, it aggros, it fights, it drops its normal loot, and it dies like the wild version, because it is the wild version. Spawn a pack of hyenas to train on. Drop a Tuanosaur next to a bandit camp and watch them fight each other. Restock a region you've already cleared out.
Spawns are ephemeral: they never enter your save file, and they vanish when you leave the area.
Requires: BepInEx 5 (Outward's Mono branch β see Compatibility), plus CompanionKit and ForgeKit. If you install through a mod manager these come along automatically.
Installing
Install from Thunderstore and the dependencies come with it. Installing by hand: drop all three
folders β ForgeKit/, CompanionKit/, SpawnKit/ β side by side into BepInEx/plugins/. If
BepInEx logs a dependency error and refuses to load SpawnKit, one of the other two is missing.
Using it
Launch the game, load a save, and press F11 for a draggable window listing every creature you can spawn.
- β means the creature is ready β it spawns instantly.
- β means it's cold: the first spawn pauses 1β6 seconds while SpawnKit fetches its body (see How it works). Click Prewarm to pay that cost up front instead.
- Set a count, hit Spawn, and use Despawn All / Kill All to clean up.
The key is rebindable ([Menu] MenuKey; Backslash is the classic debug-menu choice), and the
menu closes on Esc, on F11 again, or when you open a vanilla menu.
Settings
BepInEx/config/cobalt.spawnkit.cfg, created on first launch:
| Setting | Default | What it does |
|---|---|---|
[Spawner] Enabled |
true |
Master kill-switch. Off = no new spawns (cleanup still works). |
AllowSpawnInRoom |
false |
Let the host spawn while guests are connected. Off by default: spawns don't sync to guests yet, so a host spawn is a ghost that spams guests' logs and locks the host in unwinnable combat against their invulnerable replicas. Solo play is never affected. |
MaxActiveSpawns |
8 |
Cap on live spawns at once. |
SpawnDistance |
5 |
How far from you they appear, in meters. |
MaxCachedTemplates |
12 |
How many creature bodies stay in memory. Higher = more instant spawns, more RAM. |
DefaultCorpsePolicy |
Vanilla |
Vanilla leaves the corpse and its loot. NoBody deletes the corpse (and its loot) after death. |
[Menu] EnableMenu |
true |
The F11 menu. |
Example configuration
BepInEx/config/cobalt.spawnkit.cfg β created on first launch. Excerpt (defaults, all four
sections):
[Spawner]
Enabled = true
AllowSpawnInRoom = false
MaxActiveSpawns = 8
SpawnDistance = 5
MaxCachedTemplates = 12
## Vanilla leaves the corpse + loot; NoBody deletes both after death.
DefaultCorpsePolicy = Vanilla
DefaultCorpseLingerSeconds = 0
ForceLootableEnabled = true
[Menu]
EnableMenu = true
## The spawn-menu key; Backslash is the classic debug-menu choice.
MenuKey = F11
[Expedition]
EnableExpeditions = true
ConfirmMenuExpedition = true
[Coop]
EnableCoopSpawns = true
DespawnOnMirrorFailure = true
MirrorQueueTimeoutSeconds = 20
VerboseNet = true
HeartbeatSeconds = 30
The command channel is BepInEx/config/SpawnKit_cmd.txt (see Without the menu below). The
speciesβscene roster comes from CompanionKit and is overridable by dropping a
BepInEx/config/DonorScenes.txt (see How it works). The workspace's pinned-key overlay is
config/shared/cobalt.spawnkit.cfg.overlay, applied by scripts/sync-config.py.
Without the menu
Everything is also driven by writing a line into BepInEx/config/SpawnKit_cmd.txt β it runs on the
next poll, even while the game is paused. Useful for scripting or if a keybind conflicts.
| Verb | What it does |
|---|---|
spawn <species> [count] |
Spawn 1 or more beside you. Names may contain spaces: spawn Armored Hyena 3. |
despawnall [kill] |
Remove every spawn. kill gives them a real death (animation + loot) instead of deleting them. |
spawnex <species> [dist=N] [life=N] [faction=Name] [owner=tag] [body=vanilla|none] [linger=N] [keepquest] |
One spawn with the full option surface. |
spawnlist / spawndump |
List spawnable creatures / show what's live right now. |
spawnprewarm <species> / spawnprep <scene> |
Pay the body-fetch cost now so later spawns are instant. |
spawnclearcache |
Drop cached bodies (frees memory; next spawn re-fetches). |
skcoopdump |
Co-op state on this machine: handshakes, mirrored spawns, message counters (SpawnKit's mirror ledger + NetKit's netdump census). (More co-op dev verbs under help: skinject/skfail/skdrop/skresync/skgone/skstream.) |
selftest |
Sanity-check the install; look for [SELFTEST] β¦ DONE in the log. |
β οΈ Known issues β read this bit
Some creatures don't work. Roughly a quarter of the rows in the list fail to spawn β their body can't be found in the scene the table expects. You'll get a clear message in the log rather than anything harmful. This is being chipped away at.
Co-op works when everyone has the mod (built 2026-07-18, not yet live-verified). The host
spawns; every guest running SpawnKit automatically mirrors the creature as a real networked enemy β
it moves, fights, dies and drops loot for everyone, just like a normal world enemy. Guests still
can't initiate spawns (host-only, refused cleanly). The handshake protects you: if any player in
the room doesn't have SpawnKit, in-room spawns refuse and the log names who β because to them the
spawn would be an invisible ghost (log spam for an entity they can't see, and it wails forever on
their invulnerable replicas, locking the host in combat). [Spawner] AllowSpawnInRoom=true
overrides that refusal if you accept those players seeing nothing; [Coop] EnableCoopSpawns=false
turns the whole co-op layer off (back to refuse-in-room). skcoopdump shows the co-op state on
either machine.
Transport (NetKit cutover, 2026-07-18). The co-op wire is no longer SpawnKit's own relay:
SpawnNet is now a thin facade over NetKit's shared sk channel. NetKit owns the one Photon
relay (NK_Bus on CharacterManager's view), the single handshake (nk.hello) + peer/absence
detection, the join hooks, the heartbeat ([Net] HeartbeatSeconds now drives the [SKNET] hb line),
and the PUN diagnostics β SpawnKit keeps only the spawn semantics (the sk.spawn/sk.gone/
sk.ack/sk.fail/sk.resync verbs, their codecs, and the mirror-gate policy). Everything above is
behaviour-preserving; the visible change is that per-message logging now rides [Net] VerboseNet
(NetKit's .cfg) and netdump (or skcoopdump, which folds it in) is the transport census.
Cutover: new builds speak NK_Bus only β an old SpawnKit build (SK_Bus) looks unmodded to
the new handshake and is refused co-op, which is fine because the kits ship as one bundle.
A rare visual glitch. Very occasionally, after a lot of spawning in one session, a patch of ground can go invisible (you can still walk on it) β changing zone and coming back repairs it.
How it works
Outward has no way to create a creature from nothing β every creature is baked into the scene it lives in. So SpawnKit borrows one: it quietly loads the scene a species lives in, copies a live creature out of it, gives the copy a fresh identity, drops it on solid ground near you, and unloads the scene again. That's the 1β6 second pause on a cold spawn, and it's why the result behaves perfectly β it's the same creature the game itself would have spawned, not an imitation.
The creature roster comes from CompanionKit, which owns the table mapping each species to the
scenes it can be found in. spawnlist prints it. You can add or fix an entry without rebuilding
anything by dropping a DonorScenes.txt into BepInEx/config/:
Hyena=Emercar_Dungeon5,ChersoneseDungeonsSmall
Alpha Tuanosaur=Hallowed_Dungeon3,HallowedMarshNewTerrain
Region-only creatures need an extra step. Open-world regions and towns are too big to load in the
background β trying it crashes the game, so SpawnKit refuses to. Creatures that live only out in a
region (Pearlbird, Torcrab, Coralhorn and friends) therefore can't be fetched the usual way; their
rows are labelled (expedition-only) and a cold spawn fails with a clear message. To stock them,
write expedition <species> into BepInEx/config/ck_cmd.txt: CompanionKit takes you through the
loading screen and back, and from then on that creature spawns like any other.
Troubleshooting
| Symptom | What it means | What to do |
|---|---|---|
No spawnable species matches '<x>' |
Not in the table | Check spawnlist; add a line to BepInEx/config/DonorScenes.txt |
has NO viable donor (all candidates oversized) |
A region-only creature, not stocked yet | Run expedition <species> (see above) |
| First spawn of a creature pauses, later ones don't | Normal β that's the body fetch | Use spawnprewarm to pay it early |
| No mods load at all, no crash log | Wrong Steam branch | See Compatibility |
Compatibility
Outward must be on its Mono Steam beta branch, not the default IL2CPP build (Properties β Betas β
select mono). If the game runs but no BepInEx mods load and there's no crash log, this is almost
always why.
For modders
SpawnKit is also a library. Reference it and declare the dependency:
[BepInDependency(SpawnKit.Plugin.GUID)]
Then spawning is one line:
SpawnHandle h = Spawner.Spawn("Hyena");
Everything else is optional β per-spawn customization rides a SpawnOptions, and the returned
SpawnHandle is your window onto the creature's lifecycle:
Spawner.Spawn("Alpha Tuanosaur",
new SpawnOptions
{
Faction = Character.Factions.Tuanosaurs, // null = the donor's own faction
Distance = 12f, // ring-probe radius (clamped 1β50m)
// Position = somewhereExact, // skip the ring probe entirely
LifetimeSeconds = 120f, // auto-despawn clock (null = forever)
Corpse = Core.CorpsePolicy.NoBody, // destroy the corpse after death (null = the
// [Spawner] DefaultCorpsePolicy config;
// NoBody takes the LOOT with it β see traps)
CorpseLingerSeconds = 8f, // death-anim time before the NoBody destroy
OwnerTag = "mymod", // namespace your spawns (see DespawnAll)
// StripQuestEvents = false, // default true β leave it unless you KNOW
},
onReady: handle =>
{
if (!handle.IsAlive) { /* handle.FailReason says why */ return; }
handle.OnDied += _ => DropReward();
handle.OnDespawned += _ => CleanupMarker();
});
The full surface (SpawnKit.Spawner, all static):
| Call | What it does |
|---|---|
Spawn(species, options?, onReady?) |
Spawn ONE creature; returns a SpawnHandle immediately (Pending β Alive/Failed; sync refusals return already-Failed with a FailReason). One call = one creature β loop for more; a cold-species loop still costs one fetch (in-flight acquires dedup). |
Despawn(handle, kill=false) |
Remove one spawn: kill = real damage pipeline (death anim + loot, β Died); silent = destroy (β Despawned). Cancels a still-Pending handle. |
DespawnAll(ownerTag=null, kill=false) |
Sweep by owner tag. null = EVERYTHING β that's the operator verb; mods should pass their own tag. |
Prewarm(species, onDone?) / IsPrewarmed(species) |
Pay/check the body fetch off your hot path. |
IsExpeditionOnly(species) |
True when a species can only be stocked by an expedition (see How it works) β check before promising a boss fight. |
Active(ownerTag=null) |
Snapshot of tracked handles (Pending + Alive). |
SpeciesKeys() |
The spawnable species roster. |
Trap table β read before shipping on this
| Trap | Rule |
|---|---|
handle.Character can go Unity-null under you (scene unload, despawn) |
Check handle.IsAlive before every use; never cache the Character across scene loads. |
| Spawns are ephemeral by design | Scene unload = OnDespawned, and saves skip SK_ UIDs both directions. Don't build persistence on top. |
A cold species blocks ~1β6s before onReady |
Prewarm on your init path if the spawn must feel instant. |
MaxActiveSpawns is global across all consumers (pending spawns count too) |
Tag your spawns (OwnerTag) and never call DespawnAll(null) from a mod β you'd sweep other mods' spawns. |
| Callbacks run on the main thread inside the watch tick | Keep them cheap. A throwing callback is isolated (swallowed + logged) β it won't break SpawnKit, but you won't get a second chance either. |
Co-op clients always get Failed/NotMaster β guests never initiate |
Design your feature so only the host drives spawning. Host-side in-room spawns work (Phase 3) when every peer handshakes; otherwise Failed/PeersNotReady names the unmodded peer, or Failed/RoomWithGuests with [Coop] EnableCoopSpawns=false. |
An unmodded guest in the room blocks in-room spawns (PeersNotReady) |
That's the feature: a spawn would be an invisible ghost to them. AllowSpawnInRoom=true overrides, knowingly. NB their game logs one red PUN "has no method" line per handshake message we send β harmless, bounded. |
| A guest's first mirror of a species runs its own ~1s donor harvest | Until the mint lands, the guest logs bounded PUN "no such view" warnings (~10/s, counted in skcoopdump) β [Coop] MirrorQueueTimeoutSeconds bounds the worst case. Prewarm on both machines if it matters. |
Flipping EnableCoopSpawns off mid-session with live mirrors stops sk.gone |
Guests keep their replicas until their next scene change. Despawn everything first, then flip. |
| Late-join / scene-return flush covers Alive spawns only | A guest joining (or re-entering a scene) never sees pre-existing corpses; and a NoBody corpse removal can destroy a loot container a guest has OPEN (same shape as a scene unload mid-loot β vanilla tolerates it). |
| A dead spawn's viewID release is deferred until its corpse GameObject is destroyed | photondump listing manually-allocated ids with no registered view right after a kill is NORMAL while the corpse persists β they clear on the watch tick after the corpse (or its scene) goes. Only ids that survive the corpse are leaks. |
StripQuestEvents = false on a quest-flagged creature fires real quest events on death |
The default (true) exists because live donors really do carry QuestEventOnDeath β leaving it on is what keeps spawns from mutating quest state. |
CorpsePolicy.NoBody destroys the loot bag with the body |
The corpse GameObject IS the loot container β never combine NoBody with a drop-loot expectation. |
| Ring geometry (step angles, elevation band, navmesh snap) is not configurable | By design. If you need exact placement, pass SpawnOptions.Position and own the spot yourself. |
| The species roster and creature supply are CompanionKit's, not SpawnKit's | An expedition-only species fails cold spawns until an expedition stocks it. |
License
Apache License 2.0 β see LICENSE. You may use, modify, and redistribute this kit (including in
commercial mods) provided you keep the copyright/license notice; see the license text for the full
terms.
CHANGELOG
SpawnKit changelog
0.4.2 β 2026-08-02
- DangerousRoads 0.1.1 + SpawnKit 0.4.2: re-pin off CompanionKit 0.4.2
- DonorKit/SpawnKit: name the two silent paths
- DonorKit/SpawnKit: five static-analysis fixes
- DonorKit/SpawnKit cleanup: one rig gate, one Safe() reader
- Docs sweep: archive, condense, and validate the whole documentation tree
0.4.1 β 2026-08-02
- Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule