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.
| Date uploaded | a week ago |
| Version | 0.4.5 |
| Download link | CeruleanCutlass-CompanionKit-0.4.5.zip |
| Downloads | 96 |
| Dependency string | CeruleanCutlass-CompanionKit-0.4.5 |
This mod requires the following mods to function
CeruleanCutlass-ForgeKit
Dependency-free dev-tooling for Outward BepInEx mods: file-driven dev command loop, self-test harness, on-screen toasts, player-ready lifecycle wait, embedded/override table loaders, and a shared dev-verb pack (movement/combat/skill/status probes).
Preferred version: 0.4.4CeruleanCutlass-AggroKit
Reusable aggro/threat control tooling for Outward BepInEx mods: per-pair targetability gates, detection veto/redirect, taunt and force-target primitives. Inert until a mod uses it β a library dependency, installed automatically.
Preferred version: 0.1.3CeruleanCutlass-DonorKit
The donor-scene harvest engine for Outward BepInEx mods: additively load any area, clone a live creature into a dormant template, and unload cleanly. Library dependency, installed automatically when another mod (SpawnKit, CompanionKit) needs it.
Preferred version: 0.1.5CeruleanCutlass-NetKit
Shared Photon co-op transport layer for Outward BepInEx mods: channels over one relay, a hello/peer ledger, per-channel counters/heartbeat, PUN diagnostics, and a replicated-record store for consumer state mirroring.
Preferred version: 0.2.4README
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.
//
// β UPDATE 2026-08-02 (HYENAVANISH): a consumer that skips this line no longer gets its
// body reaped β the reaper now FAILS SAFE and disables itself entirely when no consumer
// has registered (that exact pairing shipped and destroyed a player's pet: Beastwhispering
// 0.2.1 predated the census while CompanionKit 0.4.2 carried the reaper). But skipping it
// still costs you the census, the reaper's self-heal for genuine leaks, and every
// `[REAPER]`/`[CENSUS]` diagnostic β so it remains MANDATORY.
BodyCensus.RegisterClaimSource(() => new[] { _myCompanion?.Body });
// β MANDATORY since 2026-08-02 (HYENAVANISH): declare which CompanionKit consumer contract
// you were BUILT against. CONSUMER_CONTRACT is a const, so it inlines here at YOUR build
// time β that is what makes a you-and-CompanionKit-published-from-different-commits skew
// visible in the logs instead of as a destroyed companion. See ConsumerContract.cs.
ConsumerContract.Declare(NAME, CompanionKit.Plugin.CONSUMER_CONTRACT);
}
}
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.
CHANGELOG
CompanionKit changelog
0.4.7 β 2026-08-19
- Second-review adjudication: drop a dead dedupe, lift the claim rule into Core
- Review fixes: ck.proxy.status must never read as live when it isn't
- Review fixes: LocoRig's param cache must not latch an uninitialized animator
- ck.proxy.status: master streams the proxied anchor's status list to its owner
- Fix companion locomotion params never reaching the skin-driving animator
- Fix companion locomotion freeze on optimized-rig species (Veaber/Pearlbird)
0.4.7 β 2026-08-19
- Second-review adjudication: drop a dead dedupe, lift the claim rule into Core
- Review fixes: ck.proxy.status must never read as live when it isn't
- Review fixes: LocoRig's param cache must not latch an uninitialized animator
- ck.proxy.status: master streams the proxied anchor's status list to its owner
- Fix companion locomotion params never reaching the skin-driving animator
- Fix companion locomotion freeze on optimized-rig species (Veaber/Pearlbird)
0.4.6 β 2026-08-19
- Review fixes for the locomotion wave: LocoRig re-probes an animator that reported no parameters yet (the uninitialized-Animator trap would have latched "writes nothing" for a body's whole life), and every skin-driving animator is fed, not just the first
- Fix BUG-PETICESKATE: feed locomotion params to the SKIN-driving animator too (LocoRig), and raise the moveForward blend ceiling from a walk (3) to the run rung (8) β the parameter is in m/s
- Fix BUG-PETANIMFROZEN (round-1 regression): never disable the animator that drives the skin
- Review nits: TagRig const, HasParam asymmetry comment, live-retest watchpoints
- Fix BUG-PETANIMSTUCKIDLE: disable child override animators so the root attack rig renders
- Review fixes for V6b: the tab-injector field, and the row keeps its id
- Fix V6b: the Quiet-tier exemption was inverted (ambient ungated, verbs gated)
- Merge branch 'fix/sa-0815-leafkits' into feature/pet-self-feed
- Static-analysis wave F3c: leaf-kit fixes (A7-1/2/4/8/9/13/14/15/16/20/23/24/25/27)
- Merge branch 'fix/sa-0815-bw-glue' into feature/pet-self-feed
- Review fixes for the SA lane-F3 MP/net wave (H1, M1, M2, L1-L3)
- BW glue: self-feed kill-switches, armor-wear save debounce, inert armor survival
- SA 2026-08-15 lane F3: MP/net teardown, mirror-race + hello-warn fixes
- Pet Equipment inventory section + review-fix wave; BUG-PREBUILTADOPT filed
- Pet armor: CompanionKit equipment fabric + PetArmor.json-driven Pet Leather Armor
- DonorKit: refuse a body-template capture with no AI graph
- Forge shell fixes: SSH commands via bash -c (fish login shell); set/cfgdump on every channel mod
- Forge shell: catalog dump + response protocol + set/cfgdump in ForgeKit; forge CLI/REPL + completion packs; wiki-enriched name db
- Merge branch 'release/pre-bundle-bodies'
- Feed scene tune: 0.5s settle beat on arrival + spot 1.75β2.05m (Hyena live tune)
- CompanionScene: SceneSpot hold β pet walks onto the mark and stays until the scene concludes
- Feed/bandage interaction scene: pet runs over first, THEN the player's SetupGround anim, then the effect β on a reusable CompanionKit scene engine
- Bundle body tier: Pearlbird ships as an AssetBundle rig β expedition bypassed (B1/B2/B3a live-verified)
0.4.5 β 2026-08-11
- Merge phantom view-ID work: root-cause writeup (donor-stray duplicate registration evicts scene-baked views via PUN's destroy-old resolution), DuplicateViewPolicy veto in DonorPhotonGuard, unknown-view runaway telemetry + hb top= attribution, corpse-mute window accepted as bounded (NK-PHANTOM1/2 + V-PARKLEAK A/B retests owed)
- Phantom view-ID root cause + fixes: NetKit hb per-id attribution (top=[idΓn]) + unknown-view RUNAWAY detector, DonorPhotonGuard duplicate-registration veto over live scene-baked views (DuplicateViewPolicy, unit-tested), V-PARKLEAK bounded-window acceptance + pt-stamped mute line; analysis doc + NK-PHANTOM1/2 retest rows β built, retest owed
- Fix BUG-BOONFXSILENT + SR-D10 in the FX capture/strip pipeline
- Slope-tilt review wave: pure smoother + hysteresis deadband + per-species axis
- Merge feat/companion-aura: CompanionAura registry (Burning Man fire / Wendigo frost infuse auras) + leg-A VFX fixes
- Companion auras: abstract persistent-visual system + Burning Man/Wendigo infuse auras
- Merge feat/runic-pet-lantern-bolt: CompanionProjectile registry + visible bolt from the pet's lantern
- Leg E: a real bolt from the pet's lantern, via a CompanionKit pet-projectile registry
- Infuse rider audit: no defect found; ship the diagnostics that settle it
- Shaman resonance leg D: Infuse Fire/Frost -> a typed damage rider on the pet
- Merge fix/sa-0808-tests: test-debt wave (SA 2026-08-08 Β§4)
- Merge fix/sa-0808-bw: BW credit/identity wave (SA 2026-08-08)
- Merge fix/sa-0808-spawn: spawn recovery wave (SA 2026-08-08 Β§8)
- Wave D-bw review fixes: per-SHAPE synergy arm, charged-weapon grace, save one-shot at the write site
- Static-analysis 2026-08-08 wave E: close the test-debt list (Β§4)
- SA-0808 Wave C data half: DonorScenes pin wave + sweep-doc corrections
- Wave D-bw: Hunt-Synergy armed-skill binding + BW P3 hygiene (2026-08-08 static analysis)
- NetKit hardening wave (static-analysis 2026-08-08 Β§7 items 1-7 + P1-6/P2-8)
- Merge branch 'feat/pet-health-orb'
- Pet health orb: replace the IMGUI bar with a green Outward-style vitals circle
- β¦ and 38 more (see git history)
Unreleased
-
Diagnostics (built, NOT live-verified): the master's
ck.proxy.hitdrop reasonno-targetis split intotarget-unknown(the UID did not resolve on the master) vstarget-dead(resolved but no longer alive) β dead-on-the-kill-swing is expected and benign (the verb is aggro replay only; damage rides vanilla replication), sotarget-unknownis now the signal worth watching. Both stay silent drop-counts; ladder order unchanged. The[ANCHOR] hp drift OUT OF COMBATprobe line gains appendedlocked=(the tick-start AI lock β the attacker evidence the calm paths erase),ai=(state=β¦ locked=β¦)(the current AI view via DescribeAI) andcalmedThisTick=(whether the stale-lock sweep or the B6 re-calm suppressedfightingin the same Upkeep pass) so it distinguishes "no attacker anywhere" from "attacker present but the fighting flag was suppressed" β the probe's two documented false-negative windows. Existing tokens on both surfaces stay byte-stable (append-only). -
Fixed (MP-PETAIMDRIFT; built, NOT live-verified): a guest-owned pet visibly desynced on the host β its master-side proxy anchor had NO position link to the guest's puppet (two independent walkers; a proven ~3.8 m at-rest offset). New verb
ck.proxy.pos(GβM) makes the guest's puppet the single position authority: the position + facing yaw stream offCompanionBody.OnAfterMove, change-detected against the last-sent value (β₯0.25 m per axis / β₯5Β° circular yaw, with movement hysteresis so a grid-boundary sitter or the Β±180Β° yaw seam can never dither) behind a ~0.2 s attempt-rate floor β the whole build/latch path runs at most ~5 Hz on the guest's per-frame hot path β plus a 4 s at-rest resend (Core.ProxyPosPacing). The master's new per-frameProxyDriveplaces the proxy anchor on the stream through the weld's one engine apply path (AnchorWeld.Apply, extracted fromGlueTickβ snap past 3 m, else MoveTowards at a speed cap, the vanillaNetworkCharacterControlAI-replica semantics; drive snaps log under[PROXY], keeping[GLUE]a pure local-weld signal). Every other peer converges via the anchor's native replication β one new wire hop total. While the stream is live AND actually being applied the row counts as WELDED (CompanionAnchor.ExternallyDrivenrequires fresh receive + recent apply, so a drive that stops writing reverts to normal recovery; the leash teleport and passive recall stand down;AISWander.FollowTransformreleased); an anchor stuck at void coords is snap-rescued onto the sane stream. Targeting is untouched (the anchor still aims and fights). Staleness fallback: no pos for[Proxy] PosStaleSeconds(default 10 s, floored to 2Γresend+2 s so one lost resend cannot fake a blip) reverts the row to the shipped owner-follow with one transition line each way β an OLD guest that never sends pos behaves exactly as before, and an OLD master drops the verb with NetKit's usual warn-once accounting (no version bump; the release train owns versions). CodecNetProtocol.BuildPos/TryParsePosis refusing (drop reasonsunparseablevsinsane-posβ the decode-timeWorldPosition.IsSanegate). The mirror re-seeds on every announce via the bond-levelCompanion.ResetProxyPosMirror(the pos mirror exists whether or not combat does). Observability:proxydump/effigydump/[PROXY] poslines gaindrive=(live|stale|none) driveAge= applyAge= lastPos=and apt=shared-clock stamp (appended β existing tokens byte-stable); the verb is QUIET on the ck channel (see NetKit) so a ~5 Hz stream cannot wreck the log or the trace ring. -
Fixed (MP-ANIMASYM; built, NOT live-verified): a guest-owned pet's attack animation never played on a machine running the shipped agent-follow effigy mode (
[Effigy] PinToAnchor=false, the default) β every foreign swing dropped asno-pin, because the attack vocabulary lived on the pin-mode-onlyEffigyPin. The vocabulary is now the sharedEffigySwingMirror, attached to effigy bodies in BOTH drive modes (it only fires Animator triggers β agent-follow's CompanionBody/agent movement brain is untouched); both swing carriers (ck.effigy.swingand the vanilla-attack-RPC belt) route through it; theApplySwingdrop reasonno-pinis renamedno-mirror(a genuine fault now, not a mode choice); agent-follow swing lines log as[SWING/EFFIGY:<species>]while pin lines keep their byte-stable[PIN/EFFIGY:β¦]grep contract;effigydumpsurfaces theswings=/attackParams=counters for agent-follow bodies too. Also closes the latent latch bug:[Effigy] PinToAnchoris read at plugin Awake now, not first inside the effigy tick, so a body built before the first tick can no longer run agent-follow with pin=true in the cfg. Pure trigger selection isCore.SwingVocabulary(unit-pinned). -
Refactor (no behavior change):
BodyFactory's two census sites read their PhotonView / NetworkCharacterControl counts fromNetKit.Views.CountNetworkinstead of the localCountAll(phase-2 dedup; identical semantics, census log lines byte-unchanged). -
Fixed:
BodyFactory's component strip is now plural and children-inclusive, re-checked after activation, with a loud tripwire if aPhotonViewsurvives onto a live puppet and a byte-stable[CLONE]#n strip census:breadcrumb (the MP10 guest-tame world-origin fix; also removes a latent hazard where a kept-look humanoid puppet's equipped-item child PhotonViews survived carrying the source NPC's item viewIDs). -
New: stuck-body detection (
Core.StuckBodyPolicy+ per-body opt-inCompanionBody.StuckDetection, one-shotSTUCK-WATCHwarning at the trip edge) andCompanion.DropBody()β destroy just the body while keeping the anchor, stance and bond aggregate, so a consumer can rebuild a wedged body in place. Census lines carry astuck=token while detection is on. CONSUMER_CONTRACT 1 β 2 (three new hard-referenced seams). -
Anchor replica recognition is ownership-aware in its logging: a machine seeing its OWN pet's proxied anchor replica now logs
own-pet companion anchor replica recognizedinstead of claiming it is foreign; all foreign-case lines are byte-unchanged, and the dressing behavior itself is deliberately identical for both (Core.AnchorReplicaPolicydocuments why every pass applies to every replica). -
New: spell FX on companion bodies.
BodyFxplays a one-shot flourish or wears a persistent visual on a brain-stripped body (puppet or effigy) β the two things a Character-less body cannot do for itself, since the engine's own bind path needs a live Character. -
New:
CompanionPetFxreplicates those visuals across a co-op session on theckchannel (apetfxrecord store keyed by owner and slot, plus a transient cast message and the guestβmaster relay). One apply path serves the owner's real body and every other machine's effigy, so a guest can no longer be skipped by construction. -
New:
petfxdumponck_cmd.txtβ what this machine RECEIVED and whether it is on a body, including a sweep for orphaned FX clones. It needs no consumer mod installed. -
Off-switch:
[PetFx] EnablePetFxincobalt.companionkit.cfg. -
Fixed (MP10 fix wave 2, root cause): the strip tripwire above used to CLAIM it destroyed a survived
PhotonView, but the destroy itself could be silently refused by Unity the same way the original strip was β the strip census could readpv=1right after the tripwire "ran". Clone hygiene now delegates the Photon half toNetKit.Viewsβ every dangerous field (removedFromLocalViewList,viewID, sync mode, send group) is neutralized with plain field writes BEFORE any destroy is attempted, so a refused destroy can no longer let a stray view evict the real replica. The tripwire's log is now honest:VerifySurvivorsreports "REFUSED, not attempted-and-failed" and disarms what's left, instead of asserting a destroy that may not have happened. -
New: build-time contamination verdict (
Core.ContaminationPolicy,pv/ncc/charAI/caidonly β a benign ghost-rigCharacterand legitimate Item children are deliberately excluded). Census lines gained a trailingitems=N.token (the period moved onto it) and acontaminated=Ntoken whenever the body is dirty, so a downstream watchdog can trip on the actual contamination instead of inferring it from position. -
Fixed: the pet spell-FX one-shot (
BodyFx) had the same latent "runs inside an animation event" bug as the tame path. It now strips the WHOLEVFXPlayedrebind family (including the per-frame bone follower and the particle-reparent step, both of which could leak an orphaned GameObject), re-enumerates survivors after stripping, pins the body ONLY when a survivor is actually found (a clean strip keeps the creature's own clip-driven root motion), and defersPlayone frame whenever a survivor was found so queued destroys flush before the broadcast goes out.
0.4.4 β 2026-08-02
- Reaper fail-safe: never judge a body when no consumer has spoken
0.4.3 β 2026-08-02
- CompanionKit: Unity fake-null bugs, guard ordering, and the logs that were missing
- CompanionKit: one weapon-neuter, one settings snapshot per sweep
- CompanionKit.Core: five pure-logic bugs the unit suite could not see
- Docs sweep: archive, condense, and validate the whole documentation tree
0.4.2 β 2026-08-02
- E8d/EFFIGYVANISH wave: placement gate, universal agent bind, body census + reaper
- Pin-session fix wave: recall moves the anchor, replica fuse defused, glide, swing mirror
- Effigy-pin B-wave: attack mirror + BEV-V1 reconcile hardening + E8d instrumentation
- Fix the MP session's three critical bugs: cross-machine cast replay, master tame, effigy yaw+speed
0.4.1 β 2026-07-30
- Cap all-status build-up resistance below the engine's refusal sentinel
- Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule