
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) plus ForgeKit,
DonorKit, AggroKit and NetKit β all four declared [BepInDependency] hard dependencies.
No SideLoader.
Installing (for players)
Drop the CompanionKit folder into BepInEx/plugins/, alongside ForgeKit/, DonorKit/,
AggroKit/ and NetKit/ β BepInEx refuses to load CompanionKit if any one of them is absent, and
names the missing GUID in the log. 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 |
expeditionreset |
Clear this launch's expedition state and auto-warm decision |
bodycensus |
Every companion body this machine currently has, and who claims it |
effigydump / effigyrebind |
Effigy census / re-bind an effigy to its anchor |
proxydump / proxykill |
Remote-companion proxy census / destroy one |
netbusdump |
The ck channel's NetKit state |
ckreload |
Re-read cobalt.companionkit.cfg without relaunching |
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 is intended to record every scene whose capture
completed (the historical filename is kept across the move). Safe to edit or delete. β Known bug
(docs/expedition-harvest-testplan.md V42, confirmed live 2026-07-25, open): the in-memory
manifest reports captured scenes but the file is never written, so the record does not survive a
relaunch.
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. It has two sections; here
they are in full, at their shipped 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
## Effigy weld/motion tuning.
PinLerp = 1
PinCatchUpSpeed = 12
PinSnapDistance = 12
AnchorNetLerpSpeed = 4
AnchorNetMoveSpeed = 1
[Diag]
## What to do with a companion body nobody claims (see Known issues).
OrphanBodyPolicy = Reap
OrphanBodyGraceSeconds = 90
OrphanBodyCondemnSeconds = 20
Run ckreload on the command channel to re-read this file without relaunching. Values shown are
the defaults as bound in CkConfig.cs; the generated file carries each key's own
# Default value: comment, and BepInEx never migrates a changed default into an existing cfg β
if behaviour doesn't match this table, compare against those comments first.
(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());
// β MANDATORY since 2026-08-01 (E8d-A): register a claim source for every body your mod
// owns. The kit runs a convergent orphan-body reaper ([Diag] OrphanBodyPolicy, default
// Reap): any live CompanionBody NO claim source owns is warned about and then DESTROYED
// (~110s: 90s grace + 20s condemn) β intended as the self-heal for the confirmed
// leaked-body class. β The reaper itself is NOT live-verified: the fix wave landed
// 2026-08-01 and its retest (guest-pets-testplan E8d-A, T1-player) has not been run, so a
// destructive default is shipping unproven. A consumer that skips this line gets its
// companion's body reaped mid-session.
BodyCensus.RegisterClaimSource(() => new[] { _myCompanion?.Body });
}
}
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.