


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.
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.
| 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. |
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, plus
scene/species helpers.
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.
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.companionkit.cfg):
CaptureOnSceneEntry (default off) — when on, walking into a region that still has uncached
species runs the capture in place, costing zero loading screens. It is off by default because it is
the one path where the kit does substantial work the player never asked for, on every region entry.
Turn it on if you'd rather stock creatures opportunistically than run expedition <species>.AutoWarmAtBoot = off | needed | all — a once-per-launch warm at first player-ready.
needed only fires when the consumer's active companion actually needs a body it hasn't got.AlwaysWarmSpecies — comma-separated species warmed regardless of the mode; the packaging knob for
a mod whose headline creature is region-only.Manifest — BepInEx/config/ck_expeditions.txt records every scene whose capture completed. Safe
to edit or delete.
Consumer seams: ExpeditionOrchestrator.ActiveSpeciesProvider (a Func<string> naming the active
companion's species; feeds needed-mode warm) and ExpeditionOrchestrator.AutoWarmDeferred (set true
in your Awake to fire AutoWarm() yourself once your own save state is loaded).
<!-- 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
{
void Awake()
{
CompanionRuntime.Log = Logger; // or leave the kit's own logger
CompanionRuntime.DefaultSettings = new MySettings(); // ICompanionSettings over YOUR config
}
}
Lifecycle:
var companion = new Companion(); // or your subclass with policy state
companion.Anchor.OnAnchorDeath += ...; // your downed policy
companion.Anchor.OnAnchorCriticallyHurt += ...;
// 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
companion.Body = body;
var combat = body.gameObject.AddComponent<CompanionCombat>();
combat.Anchor = companion.Anchor;
combat.Stance = companion.Stance;
combat.OnEnemyDefeated += ...; // your kill-credit policy
companion.Anchor.AttachBody(body); // welds the anchor to body.OnAfterMove
// on your tick:
companion.Anchor.Upkeep(owner, host, bodyFighting: body.CombatTarget != null);
companion.Anchor.ApplyVoice(body);
// 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 is YOURS: store the species + Attributes (CreatureAttributes.Flatten/Parse is one
// save-safe string) in your own save file. The kit ships no save format.
CompanionCombat.DealDamage is the public seam.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).
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.