CompanionKit — persistent creature companions for Outward mods¶
CompanionKit is a kit for Outward that gives a mod the machinery for a persistent creature companion — a beast that follows the player, fights, survives zone changes and reloads, and can be sourced from any species in the game. It is a modder's library: on its own it adds nothing a player sees. Players get companions through the mods built on it, Beastwhispering (tame wild animals as pets) and Hireling.
At a glance
- Type: reusable library (kit) — the mid-tier "companion spine"
- Requires: BepInEx 5 (Mono branch), ForgeKit, AggroKit, NetKit. No SideLoader.
- Config: BepInEx/config/cobalt.companionkit.cfg
- Commands: BepInEx/config/ck_cmd.txt
For players¶
You don't install or interact with CompanionKit directly. It arrives as a dependency of a mod that uses it — install that mod (and let its dependencies come along), then follow that mod's page: Beastwhispering or Hireling.
How it works¶
Outward has no way to spawn a fresh creature at runtime — every creature is baked into the scene it lives in, and the only Characters the engine can spawn and persist are humanoids and ghosts. So a persistent beast companion can't just be summoned. CompanionKit builds one out of the parts the engine does allow, in four moving pieces:
1. The puppet (the body you see). A live creature can be cloned with Object.Instantiate — its
visuals and animator copy cleanly, but its AI does not (the AI's state tree is a separate object that
isn't cloned, so a naive clone throws errors forever). CompanionKit therefore brain-strips the
clone: it destroys the AI, colliders, physics and networking, leaving a pure animated model, then
puppets it — driving movement through the creature's own NavMeshAgent and its animator, following
the player every frame. Colliders are off deliberately: a live-collider body with no AI would trap the
player in permanent combat. A root-bone auto-calibration step fixes creatures whose walk animation
would otherwise make the whole skeleton drift and snap.
2. The anchor (the combat body). Because the puppet has no colliders, enemies can't see or hit it. So an invisible vanilla ally — the "anchor" — is spawned as the companion's real combat body. It absorbs enemy detection, aggro and damage; it owns the companion's HP and its stat modifiers. The anchor is welded to the puppet every frame so the two move as one, while the puppet remains the single movement brain.
3. Persistence. The kit ships the body-acquisition paths and an attribute codec but no save
format — the consumer mod owns its own save file. Across in-session zone changes the body rides
DontDestroyOnLoad and is re-warped to the player on each scene load. Across a full reload the consumer
re-acquires a fresh body from its saved species + captured attributes.
4. Body supply. A companion needs a live creature to clone from. Three routes cover every case:
- Nearby wild — clone a creature standing next to the player.
- Donor harvest — no wild source needed. Any area is an ordinary scene that can be loaded
additively in the background (~1 second), its baked creatures wake up as live instances, one is
cloned, and the scene is unloaded.
DonorHarvestwraps this whole recipe plus the audio-registry, Photon-view-collision and mid-harvest-save guards that make it survivable. A species→scene table ships embedded and is config-overridable. - Expeditions — for creatures that live only out in an open-world region or town. Those scenes are too large to load additively (it crashes), so instead an expedition makes a real round trip through the vanilla loading screen to the region, batch-captures a dormant body template for every species that scene can donate, and returns the party to where it started. Paid once, those creatures are available (load-free) for the rest of the session — and the capture can be persisted and warmed automatically (see Expeditions below).
Expeditions¶
Most companions are cheap to source: their donor scene loads additively on demand. Open-world regions and towns can't be loaded that way, so a creature that lives only in a region needs an expedition — the loading-screen round trip described above. The template it captures is cached for the session, and the kit can persist and pre-warm that cache so a region-only headline creature is ready before a fresh save's first reload.
Expeditions are host/offline only and, by default, are refused while other players are connected (each leg ends in a per-machine "press any key" continue gate that can strand a co-op party mid-trip).
Manifest — BepInEx/config/ck_expeditions.txt records every scene whose capture completed. Safe to
edit or delete.
Settings¶
BepInEx/config/cobalt.companionkit.cfg. Edit while the game is closed, or edit and run ckreload
(BepInEx has no config file-watcher).
[Expedition]¶
| Key | Default | Effect |
|---|---|---|
CaptureOnSceneEntry |
false |
When on, entering an oversized region that still has uncached donor species runs the capture in place — zero loading screens, but it clones every uncached species on every such entry, so it's off by default. |
AutoWarmAtBoot |
needed |
Once-per-launch template warm at the first gameplay ready. needed = only when the active companion's species is region-only and has no cached body; all = re-warm every recorded scene with uncached species; off = never. |
AlwaysWarmSpecies |
Pearlbird |
Comma-separated species warmed at the same boot pass regardless of the mode (the packaging knob for a mod whose headline creature is region-only — a hand-off bundle can't ship a .cfg, so this ships as a code default). Set empty to opt out. |
AutoWarmRetrySeconds |
60 |
If a vanilla load is still in flight at boot, how long the auto-warm waits for the loader to settle before giving up. 0 = don't wait. |
ReturnRetrySeconds |
20 |
Safety net for an expedition's return leg: re-request the trip home if no load has started this long after asking. 0 = never retry (the watchdog still applies). |
AllowCoop |
false |
Allow an expedition to start while guests are connected. Off by design — a stuck guest continue-gate can wedge the whole party. |
GuestAutoContinue |
true |
Guest-side safety: while a host-run expedition is in flight, auto-pass this client's continue gate. Inert in ordinary play. |
[Rig]¶
| Key | Default | Effect |
|---|---|---|
RepairSkinnedBones |
true |
Repair mesh bones that reference transforms outside the harvested creature's own hierarchy (they die with the donor scene and the body renders as a stretched line). false = log only. |
[Effigy] — co-op cosmetic bodies¶
Effigies show other players' companions on this machine as local, purely cosmetic bodies that follow each pet's networked anchor. They have no combat, HUD or simulation.
| Key | Default | Effect |
|---|---|---|
EnableCompanionEffigies |
true |
Show other players' companions as local cosmetic bodies. Off despawns them on the next tick (identities are kept — flipping back on resumes with no rejoin). |
MaxBodies |
4 |
Upper bound on simultaneous effigy bodies (each is a full brain-stripped clone). Extra rows wait for a slot. |
EnableEffigyHarvest |
true |
When an effigy's species isn't cached and no wild is nearby, let the master run the same additive donor harvest a local pet would — paid once per species. Off = wild → cache → ghost only. |
HarvestRetryMinutes |
10 |
After an effigy harvest comes up dry, how long before that species may retry. Floor 10 seconds. |
PinToAnchor |
false |
Pin each effigy body onto its pet's networked anchor replica instead of navigating it independently. |
AnchorAnimSpy |
false |
Diagnostic: periodically dump every foreign anchor replica's animator state. |
Example configuration¶
BepInEx/config/cobalt.companionkit.cfg — created on first launch. Excerpt:
[Expedition]
CaptureOnSceneEntry = false
AutoWarmAtBoot = needed
AlwaysWarmSpecies = Pearlbird
[Rig]
RepairSkinnedBones = true
[Effigy]
EnableCompanionEffigies = true
MaxBodies = 4
Dev commands run from BepInEx/config/ck_cmd.txt, and the species→scene donor table is
config-overridable by dropping BepInEx/config/DonorScenes.txt (see How it works). The
shared-settings overlay is config/shared/cobalt.companionkit.cfg.overlay (see config/README.md).
Commands¶
Write a verb into BepInEx/config/ck_cmd.txt; it runs on the next poll (even while paused). An unknown
verb or help lists them all.
| Verb | What it does |
|---|---|
expedition |
No args: 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 (loading-screen gates pass automatically). Species with an ordinary additive donor are refused toward that cheaper path. Host/offline only. |
expeditionreset |
Force the expedition guard open after a wedge (doesn't teleport anyone — use goto for that). |
templateclear |
Free the session body-template cache (logs the count freed). |
templateprobe |
Per cached template: species + a defensive mesh-readability read. |
photondump |
Photon view-registry health (donor-harvest diagnostics). |
audiodump / audioprune |
Audit / prune the global audio registrant list (donor scenes self-register audio that must be detached on unload). |
terraindump / terrainfix |
Snapshot / repair terrain-tile render holes (a failed harvest can blank a LOD tile; a zone reload is the clean fix). |
proxydump / proxykill <ownerUid> |
Co-op guest-pet proxy census / tear down one proxy row. |
effigydump |
Co-op effigy census on this machine. |
netbusdump |
Co-op network census (delegates to NetKit's netdump). |
ckreload |
Re-read cobalt.companionkit.cfg from disk. |
For modders¶
CompanionKit gives you every path to acquire a companion body and drive it; you own persistence (the save format) and your player-facing features (skills, feeding, UI).
Reference both DLLs from the kit's own folders — 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
{
void Awake()
{
CompanionRuntime.Log = Logger;
CompanionRuntime.DefaultSettings = new MySettings(); // ICompanionSettings over YOUR config
}
}
Lifecycle — Companion is the aggregate whose lifetime is the bond, not the body (bodies get
destroyed and rebuilt across zones and reloads; standing orders and anchor state must not):
var companion = new Companion();
companion.Anchor.OnAnchorDeath += () => { /* your downed policy */ };
companion.Anchor.OnAnchorCriticallyHurt += () => { /* warn the player */ };
// acquire a body (any route):
CompanionBody body = BodyFactory.BuildPuppet(src, owner, consume: true); // the wild source is consumed
CompanionBody body = BodyFactory.BuildPuppet(src, owner, consume: false); // the wild source survives
// or, with no wild source present:
// yield return DonorHarvest.HarvestChain(scenes, species, owner, b => body = b, ...);
// weld the anchor to the body and (optionally) enable combat:
CompanionCombat combat = companion.AdoptBody(body, enableCombat: true);
// push captured stats (idempotent — re-push any time; null clears back to defaults):
companion.Anchor.ApplyVitals(maxHealth);
companion.Anchor.ApplyCreatureStats(effectiveAttributes);
combat.SetAttackProfile(effectiveAttributes);
// standing orders (survive body re-forms because they live on the bond):
companion.Stance.CommandEngage(target); // priority 0, honored at any range
companion.Stance.CommandDisengage(); // passive + a run-home grace window
Key public surface:
| Type | Role |
|---|---|
Companion |
The bond aggregate: Body, Anchor, Stance, Combat, and AdoptBody(body, enableCombat) which welds and wires them. |
BodyFactory.BuildPuppet(src, player, consume, …) |
Clone + brain-strip a live creature into a CompanionBody. Consume mode replaces the wild one; no-consume leaves it alive. |
CompanionBody |
The puppet: NavMeshAgent follow, zone re-placement, leash/warp, animator driving. Exposes OnAfterMove so subscribers run after the final pose is written. |
CompanionAnchor |
The invisible combat body: HP, stat seams (ApplyVitals, ApplyCreatureStats), the OnAnchorDeath / OnAnchorCriticallyHurt events. |
CompanionCombat |
Manual combat for a brain-stripped body: target selection (commanded > defend-the-anchor > assist-the-owner), typed damage profiles, and the public DealDamage / DealDamageTyped seam for consumer special attacks. Set profiles with SetAttackProfile. |
AttributeCapture.From(character) |
Read a live Character's real stats (resistances, health, natural weapon damage, movement) into a portable Core.CreatureAttributes. |
DonorHarvest |
"Body anywhere": additively load any donor scene, clone, unload, with all the guards. |
ExpeditionHarvest / BodyTemplateCache / ExpeditionOrchestrator |
Region-only creatures via a loading-screen round trip, plus the session template cache and the boot warm/persistence policy. |
CompanionKit.Core is a game-reference-free, unit-tested compute layer: weld policy, target selection,
the attribute codec (CreatureAttributes.Flatten / Parse is one save-safe string), the donor table,
and the expedition manifest and warm policy.
Consumer seams for the expedition tier: set ExpeditionOrchestrator.AutoWarmDeferred = true in your
Awake if you want to fire the boot warm yourself, after your own saved companion state is loaded, so
needed-mode warm knows which species is active.
Scope and caveats¶
- The API is instance-based, but one companion per owner is the well-trodden path. Several at once should work but is unexercised (the vanilla summon slot is one-per-player, and multi-anchor aggro is untested).
- The kit ships the attribute codec and every body-acquisition path but no save file — you own your own format.
- Special attacks stay consumer-side;
CompanionCombat.DealDamageis the public seam. - Beast bodies are the primary path. Humanoid bodies use a different ghost stand-in route that has had far less mileage.
Compatibility¶
Outward must be on its Mono Steam beta branch, not the default IL2CPP build. If the game runs but no
BepInEx mods load and there's no crash log, this is almost always why (Steam → game Properties → Betas →
select mono).
See also¶
- Kits index · Wiki home
- Dependencies: ForgeKit, AggroKit, NetKit
- Built on top: SpawnKit
- Mods that use it: Beastwhispering, Hireling