Outward
You are viewing a potentially older version of this package. View Latest Version
Install

Details

Date Uploaded
2 days ago
Downloads
114
Size
302KB
Dependency string
CeruleanCutlass-CompanionKit-0.4.1

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.txt overrides 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 by scripts/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's hl_folk_<uid>.txt).
  • Special attacks stay consumer-side; CompanionCombat.DealDamage is 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.

Thunderstore development is made possible with ads. Please consider making an exception to your adblock.