
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.CompanionKit β persistent-companion mechanics for Outward mods
π Full documentation: CompanionKit wiki page
A BepInEx library for modders. It doesn't add anything a player sees on its own β install it because a mod you're using (a follower, a summon, a spawner) declares it as a dependency.
It exists because Outward has no way to spawn a fresh creature at runtime: every creature is baked
into the scene it lives in. CompanionKit's answer is donor-scene harvesting β any area is an
ordinary scene you can load additively, clone a live creature out of, and unload again in about a
second. DonorHarvest wraps that whole recipe, plus the audio/Photon/save guards that make it safe,
behind one call. On top of that sits everything needed to turn a cloned body into a companion that
follows, fights, and survives zone changes.
Requires: BepInEx 5 (Outward's Mono branch β see Compatibility) and ForgeKit. No SideLoader.
Installing (for players)
Drop the CompanionKit folder into BepInEx/plugins/, alongside ForgeKit/. A mod manager does
this for you. On its own the kit does nothing except expose the expedition command channel and
config described below.
What's in the kit
| Piece | What it gives you |
|---|---|
BodyFactory |
Clone a live creature into a usable body: consume vs no-consume modes, the deep brain-strip that makes a clone inert and safe (its AI, colliders and networking are the things that bite), a ghost stand-in path for humanoid rigs, and visibility diagnostics/repair. |
CompanionBody |
NavMeshAgent-driven follow: zone re-placement, leash/warp with a run-home grace window, root-bone auto-calibration (the fix for creatures that drift and snap as they walk), per-rig yaw offsets, animator driving. Exposes OnAfterMove so subscribers run after the frame's pose is final. |
CompanionAnchor |
The invisible vanilla ally that acts as the companion's real combat body β a stripped clone is untargetable, so the anchor absorbs enemy detection, aggro and damage, welded to the body every frame. Owns HP and stat seams. |
CompanionCombat |
Manual combat for a brain-stripped body: target selection (commanded > defend-the-anchor > assist-the-owner), a damage pipeline that shows up in CombatHUD, species-typed damage profiles, engage/disengage. |
AttributeCapture |
Reads a live Character's real stats (resistances, health, natural weapon damage, movement) into a portable Core.CreatureAttributes. |
DonorHarvest + its guards |
"Body anywhere": additively load any donor scene, clone, unload β with the audio-registry, PhotonView-collision and mid-harvest-save guards that make it survivable. The speciesβscene table ships embedded (DonorScenes.txt) and is overridable from BepInEx/config/. |
Companion / CommandStance |
The aggregate whose lifetime is the bond, not the body β bodies get destroyed and rebuilt, standing orders and anchor state must not. |
ExpeditionHarvest / BodyTemplateCache |
Creatures from oversized scenes (region terrains, which cannot be loaded additively β it crashes): a real round trip through the vanilla loading screen that batch-caches dormant body templates for the rest of the session. Now DonorKit's (lane 5E-2 β store: DonorKit.BodyTemplateStore); these names forward, and BodyTemplateCache keeps the puppet-shaped PuppetFrom/ClearAllAndForgetMisses that are genuinely this kit's. |
CompanionPersistence<TState> / CompanionSaveStore<TState> / BodyAcquisition |
The save/re-form spine (2026-07-26): per-character files with crash-safe atomic writes, the sceneLoadedβplayer-ready state machine (owner guard, session-end teardown, the loud cold-load identity funnel), and the body-acquisition ladder (nearby β cache β donor harvest β ghost stand-in) with its unit-tested retry budget. Your state schema and codec stay YOURS β the kit never sees a field. |
CompanionKit.Core is a game-reference-free, unit-tested compute layer: weld policy, target
selection, the attribute codec, the donor table, the expedition manifest and warm policy, the
persistence flow rules (ReformFlow) and harvest retry budget (HarvestPacing), plus
scene/species helpers.
Expeditions β the creature-supply chain
Most creatures can be fetched on demand by loading their dungeon scene in the background. But open-world regions and towns are too large to load that way (it crashes the game), so creatures that live only out in a region need a different route: an expedition β a real round trip through the vanilla loading screen that caches body templates for every species that scene can donate. Pay it once, and those creatures are available for the rest of the session.
The tier is DonorKit's since lane 5E-2 (2026-07-26) β ExpeditionHarvest/ExpeditionOrchestrator/
ExpeditionManifest/StorySense and the [Expedition] config all live in DonorKit
(cobalt.donorkit.cfg; see src/DonorKit/README.md + docs/wiki/kits/donorkit.md), and the
template cache is DonorKit's BodyTemplateStore (the companion-body kind of its K2 store).
CompanionKit remains the reference CONSUMER: CompanionKit.ExpeditionOrchestrator/
ExpeditionHarvest/BodyTemplateCache forward (ExpeditionShims.cs, plus the BodyTemplateCache
facade that keeps the genuinely companion-shaped PuppetFrom and ClearAllAndForgetMisses), and
the verbs below still answer on this kit's channel.
Verbs β CompanionKit polls BepInEx/config/ck_cmd.txt (write a line, it runs on the next poll;
works while paused):
| Verb | What it does |
|---|---|
expedition |
Status: harvest state, template-cache census, manifest, config, this launch's auto-warm decision |
expedition <scene|species> |
Round trip to an oversized donor scene, caching a body template for every species it donates. Hands-off β the loading-screen gates are passed automatically. Species with an ordinary dungeon donor are refused toward that cheaper path. |
templateclear / templateprobe |
Free the session template cache / probe a cached template |
Config β [Expedition] in cobalt.donorkit.cfg (moved 5E-2; entries left in this kit's cfg are
inert, defaults identical): CaptureOnSceneEntry, AutoWarmAtBoot, AlwaysWarmSpecies,
AutoWarmRetrySeconds, ReturnRetrySeconds, AllowCoop, GuestAutoContinue β the full table
lives in DonorKit's docs.
Manifest β BepInEx/config/ck_expeditions.txt records every scene whose capture completed
(the historical filename is kept across the move). Safe to edit or delete.
Consumer seams (reachable through the CompanionKit.ExpeditionOrchestrator forwarders or
directly on DonorKit.ExpeditionOrchestrator): ActiveSpeciesProvider (a Func<string> naming the
active companion's species; feeds needed-mode warm), AutoWarmDeferred (set true in your Awake
to fire AutoWarm() yourself once your own save state is loaded), and
RegisterAlwaysWarmSpecies("β¦") (your mod's boot-warm default β applies while the
AlwaysWarmSpecies cfg key is empty).
Example configuration
BepInEx/config/cobalt.companionkit.cfg β created on first launch. Excerpt (all three sections
at their defaults):
[Effigy]
## Show other players' pets on this machine as local cosmetic bodies.
EnableCompanionEffigies = true
## Upper bound on simultaneous effigy bodies on this machine.
MaxBodies = 4
## Master-side sourceless harvest when an effigy species has no cached template.
EnableEffigyHarvest = true
HarvestRetryMinutes = 10
## De-risk spikes, both default off.
PinToAnchor = false
AnchorAnimSpy = false
(The [Rig] and [Expedition] sections moved to cobalt.donorkit.cfg with the 5E/5E-2
extractions β see DonorKit's README/wiki for their tables and example.)
Other config surfaces:
- Command channel β
BepInEx/config/ck_cmd.txt(write a verb, it runs on the next poll). - Config-override data table β
BepInEx/config/DonorScenes.txtoverrides the embedded speciesβscene table (Species=Scene1,Scene2,β¦, repeated keys append). - Session manifest β
BepInEx/config/ck_expeditions.txt(records completed captures; safe to edit/delete). - Committed shared overlay β
config/shared/cobalt.companionkit.cfg.overlay(the workspace's pinned-key overlay applied byscripts/sync-config.py).
Consuming the kit
<!-- your .csproj β both kit DLLs ship from the kit's own folder, never copy them into yours -->
<ProjectReference Include="..\CompanionKit\CompanionKit.csproj" Private="false" />
<ProjectReference Include="..\..\core\CompanionKit.Core\CompanionKit.Core.csproj" Private="false" />
[BepInPlugin(GUID, NAME, VERSION)]
[BepInDependency(CompanionKit.Plugin.GUID, BepInDependency.DependencyFlags.HardDependency)]
public class Plugin : BaseUnityPlugin
{
CompanionHost _host;
void Awake()
{
// ONE host handle per consumer β it carries your logger + your ICompanionSettings and
// derives your log-tag suffix from the GUID ("cobalt.hireling" β "[ANCHOR/HIRE]" etc.;
// pass tagSuffix: "" for plain tags, combatTag: "β¦" to rename the combat log noun).
// The old CompanionRuntime.Log / DefaultSettings globals are obsolete one-wave shims:
// they had no ownership rule, so with two consumers installed BepInEx load order decided
// whose logger got all kit output. Don't set them.
_host = CompanionHost.Create(GUID, NAME, Logger, new MySettings());
}
}
Lifecycle:
var companion = new Companion(_host); // settings ride the host; per-companion override = 2nd arg
companion.Anchor.OnAnchorDeath += () => { /* your downed policy */ };
companion.Anchor.OnAnchorCriticallyHurt += () => { /* warn the player */ };
// (wire these BEFORE AdoptBody, or accept the one-time "[COMPANION] note: no anchor consequence
// events wired" line β a downed companion silently respawns after AnchorRespawnSeconds otherwise)
// acquire a body (any path):
CompanionBody body = BodyFactory.BuildPuppet(src, owner, consume: true); // consumes the wild one
CompanionBody body = BodyFactory.BuildPuppet(src, owner, consume: false); // the wild one survives
// or DonorHarvest.HarvestChain(scenes, species, owner, b => ...) // no wild source needed
// or the ghost stand-in trio: SpawnGhostActive / GhostVisualReady / FinishGhostPuppet
// weld the anchor to the body and (optionally) enable combat; humanoids may ask for the
// draw/sheathe flourish here (an AdoptBody parameter, not a field to poke β Wire() is internal):
CompanionCombat combat = companion.AdoptBody(body, enableCombat: true, weaponPosture: false);
combat.OnEnemyDefeated += ...; // your kill-credit policy
// on your tick (Companion.Tick includes the bodiless branch β
// ICompanionSettings.BodilessAnchor decides destroy-vs-upkeep while no body exists):
companion.Tick(owner, host);
// stat seam (idempotent, re-push any time):
companion.Anchor.ApplyVitals(maxHealth);
companion.Anchor.ApplyCreatureStats(effectiveAttributes); // null = clear back to defaults
combat.SetAttackProfile(effectiveAttributes);
// orders:
companion.Stance.CommandEngage(target); // priority 0, any range
companion.Stance.CommandDisengage(); // passive + run-home grace
// persistence: the LIFECYCLE is the kit's (CompanionPersistence<TState> + CompanionSaveStore<TState>
// + the BodyAcquisition ladder β see the wiki's Persistence section), the STATE is yours: your
// schema/codec (CreatureAttributes.Flatten/Parse is one save-safe string), your save triggers.
Scope and caveats
- The API is instance-based, but one companion per owner is what's actually been proven in play. Several at once should work and is unexercised β the vanilla summon slot is one-per-player, and multi-anchor aggro is untested.
- The kit ships the attribute codec, every body-acquisition path and (since 2026-07-26) the
persistence lifecycle β but no save schema: consumers own their format via the store's
encode/decode hooks (BW's
bw_pets_<uid>.txt, Hireling'shl_folk_<uid>.txt). - Special attacks stay consumer-side;
CompanionCombat.DealDamageis the public seam. - Beast bodies are the well-trodden path. Humanoid bodies use a different (ghost stand-in) route that should work but has had far less mileage.
Compatibility
Outward must be on its Mono Steam beta branch, not the default IL2CPP build. If your game runs
but no BepInEx mods load and there's no crash log, this is almost always why (Properties β Betas β
select mono in Steam).
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.