StoryKit
Add an NPC to Outward as plain data: StoryKit builds the character, spawns it at a fixed spot, wires its dialogue, and compiles a trainer skill tree that sells from the vanilla Trainer UI. Requires SideLoader at runtime. A library dependency.StoryKit — add an NPC, trainer, and skill tree
📖 Full documentation: StoryKit wiki page
A BepInEx library plugin for Outward (Definitive Edition, Mono branch) that lets a mod add an NPC: a standing character in the world with a dialogue graph and, if it's a trainer, a full in-game skill tree the vanilla Trainer window sells from. A consuming mod describes the NPC as plain data; StoryKit builds the character, spawns it at a fixed spot, wires its conversation, and compiles its skill tree — no custom UI, no NodeCanvas hand-editing.
StoryKit covers NPCs, trainers, and dialogue. A quest/story-event engine is not part of the
kit — there is no quest-authoring API. (It does ship read-only recon tooling over the vanilla
QuestEventManager — the qevent* dev verbs below — but that is diagnostics, not an authoring
surface, and it's off unless [Recon] EnableStoryRecon is turned on.)
Requires: BepInEx 5 (Outward's Mono branch — see Compatibility), ForgeKit, and SideLoader present at runtime.
Installing (for players)
You don't install or interact with StoryKit directly. It arrives as a dependency of a mod that adds an NPC — for example Beastwhispering, whose animal-taming trainer is a StoryKit NPC that sells the mod's pet-skill tree. The NPC and its dialogue are what you actually meet in-game; StoryKit is the plumbing behind it. A mod manager installs it automatically alongside whatever mod declares it as a dependency.
Consuming the kit
A consumer calls NpcRegistry.Register(NpcSpec) in Awake, describing the NPC's placement,
appearance, dialogue tree, and (if it's a trainer) the skills it sells. NpcDirector spawns the
registered NPC at player-ready on every scene load (built to be master-only in co-op and
duplicate-safe — that guard has not been live-verified on a two-machine session), and
DialogueBuilder compiles the described conversation into a real NodeCanvas graph reusing
SideLoader's own trainer dialogue nodes. See the wiki page for the full NpcSpec/SkillTreeDef
shape and worked examples.
Mobile, combat-capable and merchant NPCs
Since 2026-08-22 a spec can also describe a walking NPC with an AI, combat stats, a faction, a
backpack and a vanilla shop (the road-merchant wave; first consumer: DangerousRoads). All of it
is opt-in on the same NpcSpec; a spec that sets none of these fields builds the classic pinned
talker exactly as before.
NpcRegistry.Register(new NpcSpec
{
Id = "mymod.pedlar", Name = "Orrin the Pedlar",
Mobile = true, // SideLoader melee AI + NavMeshAgent; no pin/NoFall/snap
LookFollowEnabled = false, // the agent owns yaw on a mobile body
Faction = "Merchants", // Character.Factions by NAME
BackpackName = "Mefino's Trade Backpack", // display name; ' and ’ both match
Ai = new AiSpec { WanderSpeed = 1.1f, CanWanderFar = false, ChanceToAttack = 40f },
Combat = new CombatSpec { Health = 900f, Protection = 20f, DamageBonusMult = 0.25f,
TargetableFactions = new List<string> { "Bandits" } },
// MerchantSpec requires Mobile = true — the Merchant graft rides the mobile rig, and a
// static merchant is refused by SpecValidation rather than spawning without a shop.
Merchant = new MerchantSpec { StockTableNameContains = "MerchantCaravanTrader",
FallbackItemNames = new List<string> { "Bandages", "Makeshift Torch" },
RefreshRateGameHours = 72f },
Dialogue = new DialogueSpec
{
Greetings = { "Care to lighten my pack?" },
Choices = { Choice.Shop("shop", "Let's see what you're carrying."),
Choice.Reply("road", "Where to?", "Wherever pays.") },
},
});
Character body = NpcRegistry.SpawnAtAndGet("mymod.pedlar", pos, yaw); // drive its AISWander yourself
NpcRegistry.ReplaceGreetings("mymod.pedlar", new[] { "You saved my hide back there." }); // live swap
NpcRegistry.Unregister("mymod.pedlar"); // done with him: despawn if live, drop the SL template + entry
A consumer that mints a fresh spec id per event (SideLoader refuses a second template per UID, so a
respawn needs a new id) must Unregister the old one when it is finished, or the registry and
SideLoader's template table grow by one dead entry per event.
Random looks (2026-08-22): RandomVisuals = true rolls gender / skin / head / hair style /
hair colour instead of SideLoader's "same bald man" default, seeded from the spec id plus a
per-session salt (a respawn looks the same, the next session differs; bounds are read off the
game's CharacterVisualsPresets). OutfitPool rolls one OutfitSpec per spawn on the same seed,
each piece a display name (ChestName / HelmetName / BootsName, any may be null) resolved like
BackpackName; an unresolvable piece warns and is skipped, never the spawn, and a resolved piece
overrides the matching explicit id.
RandomVisuals = true,
OutfitPool = new List<OutfitSpec>
{
new OutfitSpec("Adventurer Armor", "Adventurer Hat", "Adventurer Boots"),
new OutfitSpec("Padded Armor", null, "Padded Boots"),
},
Saved characters (StoryKit.Saves, 0.1.6)
SaveScan.Scan reads the player's OTHER saved characters off disk (null = save path not ready yet,
empty = none); SavedCharacterSpec.FromRecord(rec, id, opts) turns one into an NpcSpec that looks
like them (NpcSpec.Visuals + worn gear by EquipSlot); SaveHandoff moves items across —
Mint (saved entry → live Item), OfferPick (vanilla container panel, one take, local player
only), RemoveFromSave (a NEW snapshot folder with the entry removed/reduced; refuses when the
character is in use or a save is in progress). Pure parts (SavedItemParser, SaveSnapshotRules)
are in StoryKit.Core and unit-tested. Log tag [SAVEHANDOFF]. Consumers: Echoes, DangerousRoads.
No config keys of its own. Full table + example: wiki Saved characters.
NpcRegistry.IsInDialogue(id) is true while the NPC's dialogue tree is running, the game's
conversation roster lists it, or its shop is open (Merchant.Buyer set) — poll it to stop a
walker mid-conversation.
Rules the validator enforces offline: Combat requires Mobile; a Choice.Shop requires a
Merchant (error) and sits at the root menu only; DamageResists is 6 entries. Specs carry no
config of their own — StoryKit's only settings are in BepInEx/config/cobalt.storykit.cfg (below);
per-NPC numbers belong to the consuming mod's config.
Compatibility
Outward must be on its Mono Steam branch, not the default IL2CPP build. If your game runs but nothing looks modded, check that first.
Config
BepInEx/config/cobalt.storykit.cfg, created on first launch. Three keys, shown at their shipped
defaults:
[Story]
## Master kill-switch. false = no NPC is built or spawned by this kit.
EnableStory = true
[Recon]
## Read-only taps over the vanilla QuestEventManager, for the qevent* verbs below.
EnableStoryRecon = false
[Diag]
## The Harmony patches StoryRecon installs. Only meaningful with recon enabled.
StoryReconPatches = false
Each generated entry carries a # Default value: comment; BepInEx never migrates a changed
default into an existing cfg, so compare against those before assuming a bug.
Dev verbs
BepInEx/config/StoryKit_cmd.txt — write a line, it runs on the next poll (works while paused).
| Verb | What it does |
|---|---|
storynpclist |
Every registered NPC and its placement |
storynpcstatus |
Whether each NPC is spawned, and where |
storynpcspawn <id> [here] |
Spawn one NPC — here uses your current position |
storynpcdespawn |
Despawn the spawned NPCs |
npcmerchanttest [despawn] |
Spawn (or remove) the built-in test merchant 3 m ahead — mobile, tanky, Mefino's backpack, caravan stock, a Shop row. Proves the shop path without any consumer. Master-only. |
storyreload |
Re-read registrations and rebuild |
| (no verb) | TorsoLookGuard.Enabled — the RM18b spine-arch guard, default off. It is a public static rather than a config key because it exists to be A/B'd live; DangerousRoads drives it from its own channel with roadsmerchant torsofix on|off. |
selftest |
[SELFTEST] PASS/FAIL … DONE |
storyrecon |
Recon summary (needs [Recon] EnableStoryRecon) |
qeventdump / qeventlisten |
Dump quest-event state / watch events as they fire |
qeventadd / qeventset / qeventdel / qeventage |
Read-write pokes at a quest event, for diagnosis only |
⚠ The qevent* verbs write to real quest state. They exist for investigation on a throwaway save,
not for play.
