Beastwhispering
Tame wild creatures of Aurai and keep them as real, persistent companions: feed and bond with them, fight alongside them with Hunt as One and For the Kill, manage their comfort and temperature, and grow a lasting bond across saves.
| Date uploaded | 6 days ago |
| Version | 0.2.18 |
| Download link | CeruleanCutlass-Beastwhispering-0.2.18.zip |
| Downloads | 68 |
| Dependency string | CeruleanCutlass-Beastwhispering-0.2.18 |
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.13CeruleanCutlass-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.
Preferred version: 0.4.29CeruleanCutlass-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.6CeruleanCutlass-SkillKit
Ship a learnable custom skill for Outward without rediscovering the traps: SideLoader pack-load wiring, effect-bucket routing, the stuck-cast guard, dynamic quickslot icons, native cast sync. Requires SideLoader. Auto-installed as a dependency.
Preferred version: 0.1.6CeruleanCutlass-StoryKit
Add an NPC to Outward as plain data: StoryKit builds the character, spawns it at a fixed spot, wires its dialogue, and compiles a trainer skill tree that sells from the vanilla Trainer UI. Requires SideLoader at runtime. A library dependency.
Preferred version: 0.1.12CeruleanCutlass-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.10CeruleanCutlass-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.9README
Beastwhispering — tame the wild animals of Aurai
Tame wild creatures and keep them as real, persistent companions. A tamed pet follows you, fights beside you, and survives zone changes and save/reload — it's not a summon, it's a character in your game the same way you are.
⚠ Updating from a build made before 2026-07-30? Your save loses every Beastwhispering item (chow, recipe scrolls, blankets, sigils, feathers), every Beastwhispering skill you have learned, and the fletching enchantment on arrows you already fletched — the mod moved onto its community-allocated item-id range and the old numbers no longer resolve. Your pets are not affected: bonds, loyalty, names and stats are stored by name, not by item id. A fresh character avoids the loss entirely. Full detail in CHANGELOG.md.
⚠ Known issue — taming works in single-player only. Right now the taming itself (using chow on a wild creature to bond it) only succeeds in a single-player session. Once a creature is tamed, it is fully playable in multiplayer: your pet comes with you into co-op, follows, fights, feeds, bonds and persists exactly as it does solo. So the workaround is simply to tame your companion in single-player first, then take it into your co-op session. This is a bug, not a design decision, and we are working diligently to resolve it.
This is actively-developed software. It is playable and it is being tested, but you will find rough edges. Back up your saves before you install. See the release notes for known issues.
Currently tamable: Hyena, Armored Hyena, Veaber, Pearlbird — each with their own diet, combat style, and species-specific bonuses.
- Taming & bonding — win a wild animal over with the right food, then build loyalty over time through feeding, care, and fighting alongside it.
- Combat — Hunt as One and For the Kill give your pet (and you) real combat presence, not a passive follower.
- Care — feed, manage temperature/comfort, and keep your companion alive through the world's hazards. Heating and cooling blankets are crafted; their recipes are also meant to turn up in world loot containers — chests, and the odd containers like broken tents, hollowed trunks and junk piles. (The drop path is new and hasn't been confirmed in a real game yet — craft from a known recipe if you don't see one.)
- Persistence — your pet remembers you. Loyalty, stats, and bond state survive quitting, reloading, and traveling between regions.
- Co-op — play with your companion in a multiplayer session. (Taming itself is single-player only for now — see the known issue above.)
Roadmap
The tameable roster is small right now, and that's deliberate, not a shortcut. Every release keeps adding more of Aurai's creatures — but each one is built as a deep, lore-accurate companion in its own right, not a reskin with a different portrait. We'd rather ship fewer pets that each feel like a real reason to pick that species — a distinct playstyle niche, abilities and stats grounded in how the creature actually behaves in Outward's world — than a long list of interchangeable ones. If your favorite creature isn't tameable yet, it's very likely on the list; it just hasn't gotten its due care yet.
Requires: SkillKit, CompanionKit, ForgeKit, StoryKit and AggroKit — all five declared
[BepInDependency] hard dependencies, installed automatically by a mod manager. Those in turn pull
in NetKit and DonorKit. SideLoader must also be present at runtime. BepInEx refuses to load a
plugin whose hard dependency is missing, so a partial hand-install means Beastwhispering never
starts at all — check BepInEx/LogOutput.log, which names the missing GUID.
Settings
BepInEx/config/cobalt.beastwhispering.cfg, created on first launch. It is a large file; these are
the keys most people want:
[Keys]
## F7 tame · F8 feed · F9 recall · F10 self-test · F12 diagnostics.
TameKey = F7
FeedKey = F8
RecallKey = F9
[Taming]
## Chance a tamable creature drops its taming-food recipe scroll on death (0-1).
RecipeDropChance = 0.33
[Temperature]
## Chance each blanket recipe scroll drops from a placed world loot container (0-1).
BlanketRecipeDropChance = 0.05
Every generated entry carries its own # Default value: comment and a full description. ⚠ BepInEx
never migrates a changed default into an existing cfg, so an install made before a value changed
keeps whatever number is already written in the file — compare against those comments before
reporting a bug. The values above are illustrative of the shape; the file itself is authoritative.
Dev/diagnostic commands are read from BepInEx/config/bw_cmd.txt (write a verb on its own line;
help lists the full set). They exist for debugging, not for play.
Combat style — the Pearlbird's opposite stance
Each species has a combatStyle axis in its manifest (data/species/<Name>.json →
CombatStyleTable): chase (default — close to melee on the target) or opposite (the Pearlbird:
station on the FAR side of the target so the mob sits between you and the bird, then hold there).
The stationing itself is CompanionKit's — the deadband, the far rule and the cap are documented
in the CompanionKit README; the knobs are bound HERE (BepInEx/config/cobalt.beastwhispering.cfg,
[Combat]: StationRingFraction = 0.6, StationLineAngleDeg = 20, StationRestationMeters = 3,
StationRestationSeconds = 2, StationArriveMeters = 0.8, StationMaxRestations = 6,
StationFarMeters = 3, StationProgressMeters = 1 — the real defaults; the last one is the
converge-or-chase gate: two far re-stations in a row that close less than this much ground on the
station mean the mob is dragging it away faster than the bird can walk, so the bird gives up the
stance and plainly chases). Beastwhispering adds the station-pin: once the
bird is planted and the mob is at its reach, a short taunt pins the mob onto the bird so you can
kite. Same file, [Combat] (defaults shown):
[Combat]
## Opposite stance: briefly pin the enemy's attention onto the pet once it reaches its station.
StationPin = true
## Opposite stance: how long (s) one station pin holds the enemy's attention on the pet.
StationPinSeconds = 3
## Opposite stance: minimum seconds between station pins.
StationPinCooldownSeconds = 3
[Brace] EnableTaunt = false silences the pin too (it is the one taunt kill-switch). In co-op a
GUEST's bird has no local anchor (the master's proxy anchor is its body), so the pin skips the
anchor and "already locked on the bird" rungs there and routes anchor-less to the master
([TAUNT] … routed to the master). Verbs: petstyle [chase|opposite] (print or override the live
body's style — a re-formed body goes back to the table) and stationdump (knobs → table → live
station / corridor angle → pin state). Log tag [STATION] (set: why=first / restation #n why=… / planted / cap fallback / pin #n); plan docs/pearlbird-station-testplan.md.
Converge (the "mob moves itself" half). The planted pin only arms after the bird has reached
its station — i.e. after the chase already succeeded, and against a mob that keeps repositioning
it never does (the why=far churn). Two additive pieces close that: the converge pin arms the
same taunt while the bird is still walking to its station, once the mob is within
StationConvergeReachMultiplier × AttackRange (7.8 m by default) — shorter hold, wider cooldown —
so the mob turns onto the bird's anchor and walks into the corridor the bird is building; and the
fast hold stops the body from re-stationing for "the enemy left me" while the enemy is running
faster than StationEnemyFastMetersPerSecond (off by default: a running mob drags the station away
as fast as the bird walks). The gate ladder is pure (Beastwhispering.Core.ConvergePin,
ConvergePinTests); StationPin.cs only gathers the inputs. Same file, [Combat] (defaults shown):
[Combat]
## Opposite stance: also pin the enemy's attention onto the pet while it is still WALKING to its station.
StationConvergePin = true
## Opposite stance: the converge pin arms when the enemy is within AttackRange x this of the pet.
StationConvergeReachMultiplier = 3
## Opposite stance: how long (s) one converge pin holds the enemy's attention on the pet.
StationConvergePinSeconds = 1.5
## Opposite stance: minimum seconds between converge pins.
StationConvergePinCooldownSeconds = 5
## Opposite stance: while the enemy moves faster than this (m/s) the pet does NOT re-station for "the enemy left me". 0 = off.
StationEnemyFastMetersPerSecond = 0
Converge pins log under their own tag, [CONVERGE] pin #n: … (walking to station, reach …), and a
walking-phase refusal is [CONVERGE] pin held back: moving to station (converge: …) — the planted
[STATION] literals and every existing why string are untouched. stationdump adds a
[CONVERGE] config: line, fastHolds= on the live line and (converge n) in the pin tally.
Backpedal / strafe — the allowsBackpedal species axis
Each species may declare "allowsBackpedal": true in its manifest (data/species/<Name>.json →
bwspecies build → embedded SpeciesBackpedal.txt → BackpedalTable). It means "this rig ships
back/strafe clips", and lets CompanionKit feed the animator the body-frame blend (a signed
moveForward + moveSide) instead of an unsigned forward speed — the fix for the Pearlbird's
moonwalk (docs/pearlbird-movement-handoff.md). Only the Pearlbird opts in today; everything else
stays forward-only. Per-species override without a rebuild — BepInEx/config/SpeciesBackpedal.txt,
same Species=true|false format, reloadbackpedal re-reads it live (effigies next frame, the local
pet on its next body stamp):
# BepInEx/config/SpeciesBackpedal.txt
Pearlbird=true
Note on Pearlbirds: Pearlbirds live only in Chersonese. If you tame or keep a Pearlbird companion, the game will briefly load into Chersonese and back on its own to fetch their body — a short, automatic loading screen, not a bug.
Credits
Artwork by Proboina — the skill and passive icons, the item icons, and the paw-shaped companion status badges.
Beastwhispering requires SideLoader by sinai-dev at runtime, and runs on BepInEx.
CHANGELOG
Beastwhispering changelog
0.2.18 — 2026-09-05
- Release prep 2026-09-05: body-ladder order gate, bundle-origin yaw, six-stage release protocol + whole-release r2modman check
- Static-analysis wave 5: release prep for Beastwhispering 0.2.18 + DangerousRoads 0.1.13
- StoryKit 0.1.12: complete the partial bump (manifest/thunderstore/csproj + consumer pins)
- ForgeKit 0.4.13 / DonorKit 0.1.9 / SpawnKit 0.6.1: session-3 fixes
- Fix release-blocking version skew: NetKit 0.2.10, Beastwhispering 0.2.17
- Beastwhispering: damage-scaled evade chance — burst compounds the dodge exponentially
- DangerousRoads 0.1.11 / StoryKit 0.1.11: bug-swarm close-out (BUG-TROGVSTROG, FC13, SK-D1-NRE, STALEBUSY)
- StoryKit 0.1.10: the StoryKit→NetKit edge is ratified and landed (BepInDependency + KitContract.Declare + DependencyDirectionTests; Echoes gains the direct NetKit refs the R2 dist-dup rule demands)
- Beastwhispering 0.2.16: MP-hardening sweep close-out (BW-D1, BW-D2, SK-D10, BW-M1, BW-M2, BW-M4)
- Beastwhispering: SK-D10 give the ambiguous body a bounded multi-frame wait
- Beastwhispering: BW-M4 clear the save row even when the player object is gone
- Beastwhispering: BW-M2 bind bw.tame.despawn to the peer that actually tamed
- Beastwhispering: BW-M1 bind the leap/stomp sender to the character it names
- Beastwhispering: SK-D10 stop minting a pet container on NPC bodies
- Beastwhispering: BW-D2 gate the tame key behind a dev flag, shipped off
- Beastwhispering: BW-D1 notify on tame success and name the invoking route
- packaging: raise ForgeKit dep pins to 0.4.12 and StoryKit to 0.1.8 (fresh-install floor)
- Beastwhispering: SelfTest anchor-leash pin follows the 28f default (was 20f; pass=79 fail=0)
- ForgeKit 0.4.12: FS21 transient-override ledger (SaveWithoutTransients) + PlayerHalt refcounted freeze; BW marenhere saves explicitly
- CompanionKit 0.4.28 / Beastwhispering 0.2.15: recall ends Stay (CS9) — PlaceAt gains a warpReason and raises OnWarpedToOwner; dependency pins raised to the compiled floor
- … and 15 more (see git history)
0.2.18 — 2026-09-04 (built, not live-verified)
Static-analysis wave (docs/static-analysis-2026-09-04.md), Beastwhispering lanes 1–4 + the BW half of
the cross-cutting lane. Every item below is BUILT and unit-tested where a pure half exists; nothing here
has been booted. Retest rows: docs/static-analysis-fixes-testplan.md SA-0904-BW1…BW10.
-
BUG-YAWBYORIGIN — the bundled Pearlbird no longer faces backwards (2026-09-05, live-PASSed headless on legion). The rig-yaw table can now pin a yaw per BODY ORIGIN:
Pearlbird@bundle=180applies to the shipped AssetBundle rig, the plainPearlbird=0row still applies to a harvested/wild one. Manifest fieldyawByOrigin; the sameSpecies@bundle=degform works in theBepInEx/config/SpeciesYawOffsets.txtoverride and the[Pet] SpeciesYawOffsetscfg string. A liveyaw <deg>tune or a plain cfg row still wins for every body. -
Body-source order is now a gate (Cobalt's ruling 2026-09-05): session template cache → the shipped rip (registered INTO the cache at scene load, replacing a harvested template — IB9) → additive donor harvest → expedition.
BodyLadderOrderTestspins the wiring, and the live check (a Pearlbird save parked in Emercar) re-formed from the bundle withauto-warm … nothing to warm— the boot expedition that could relocate a save (testplan F1) is not reached on a stock install. -
Loyalty decay rounding is now one rule (L4-01/L4-13, Cobalt's ruling): the event table rounds halves to even like the live simulation. No shipped number changes (default decay 15 → 8 per axis either way); only a species decay of 5/25 would previously have disagreed by one point.
-
L1-01 (P1) — the
UnloadAmmunitionfletch guard (G4) could hang the game: a same-ID prefab whoseMaxStackAmountis 0 made the top-up loop spin forever on the main thread, leaking one Item per pass, with nothing in the log. It now carries the same bail G3 has, with the termination decision lifted intoCore.FeatherFletching.PlaceableAmount(unit-tested). -
L1-02 (P2) — a pet that self-fed from its cargo on the very tick its loyalty hit 0 was despawned anyway (and its save file cleared): the abandonment check read the PRE-feed status snapshot. It now re-reads the live loyalty (
Core.PetRules.HasAbandoned), so the self-feed genuinely rescues the bond. -
L2-01 (P2, co-op) —
BlanketWrapEffecthad no remote-replay guard: a co-op partner wrapping THEIR pet replayed on your machine and wrote their blanket into YOUR pet's save (and tried to consume an item you never spent). It now carries the sameIsLocalPlayerguardTamingFoodEffecthas; a source-scan test requires the guard on everyEffectsubclass in the mod. -
L3-01 (P2) — the For-the-Kill execute heal stamped its one-payment-per-corpse ledger BEFORE the rungs that can refuse the heal, so a kill resolving inside the anchor's re-form window paid nothing and refused the death-path retry. The ledger is now stamped at the point of payment (
Core.ExecuteHeal.Decide/SettlesLedger, unit-tested); a guest or bodiless refusal leaves it open. -
L4-02 / L4-03 (P2) — the save codec accepted
NaN/Infinity/ overflowing cells (every clamp is a one-sided compare, false for NaN): a NaN hunger cell read as fed-forever, a NaN health fraction reached live HP, and the clamp complaint printed "clamped to NaN".ParseDoublenow refuses non-finite (mirroring the write side) with anon-finite … using 0warn — except the health-fraction column, whose pre-feature default is FULL HP: an unreadable or non-finite cell there keeps 1.0 (keeping full HP), never a 0 that would re-form the pet at 1 HP (same-tier review). -
L4-04 (P3) — a header-less save was diagnosed as a truncated v18 row, and a header-less row whose species id starts with
vwas eaten as a garbage header. A header is now a single-tokenv<n>line; a header-less file reads as v1 (no shipped format ever omitted the header, so this only changes the WARNINGS a hand-edited file gets — every column present is still read). -
L1-03 (P3) — the feed dry-run (
WouldConsume) did not apply the REFUSED-offer gate, so a refused weather row that a higher rung claims (Rancid Water) played the interact scene for a feed that never happens. The gate now sits in both mirrors. -
L1-04 (P3) — a special-attack windup killed by
OnDisablenever told the Synergy attempt, so the close readTimedOutinstead ofMissed. OneClearWindupnow serves cancel and disable. -
L3-02 (P3) —
WeaponHitProc.ProcAbsorbignored the[HuntAsOne] ProcWeaponHitEffectskill-switch, so flipping it off still let vampiric leech proc from the synced strike;hitfxdumpnow reports both latch flags. -
L3-03 (P3) — the rename field re-showed its pencil with a hardcoded "has pet" after an edit ended, even when the bond had ended mid-edit. It now asks the live pet.
-
L4-15 (P3) —
InfuseRider.Explainformatted without a culture (fraction 0,25on a de-DE guest). -
L6-11 — four silent
catch {}blocks that turned a throwing table/seam into a well-formed "nothing configured" answer (EvadeDriver, IdleBreakDriver, RunicDriver, SynergyCounterSuccess) now log one line. -
Refactors, behaviour-preserving: the two float species axes (
PowerScaleTable,SynergyResistTable) share oneSpeciesFloatAxis(L3-04);PetBadgeOrderno longer allocates per panel per frame (L2-03, a Core buffer overload); onePruneDeadsweep for the two inventory-display maps (L2-04);Core.PetScavenge.SlotLabelsreplaces two copies (L3-05);EvadeDriverreads its two hop knobs through the same null-safe accessors as its siblings (L1-05);Core.Num.Clampreplaces four private copies and a dead one (L4-09/10);SavePolicy.ShouldSaveis allocation-free (L4-11);PetHungerFacts.HungerSecondsPerDayis documented as the EFFECTIVE day (L4-12); the fourItemDisplayOptionPanelaction ids live in oneItemActionIdsfile, unchanged values, with a source-scan gate (L6-03); the equipment-slot postfixes onInventoryContentDisplaydeclare their order (HarmonyPriority.Low) like theStartInitone already did (L6-04). -
Tests: L4-05 (sim decay vs
Loyalty.DeltaForidentity), L4-06 (non-finite save cells, every numeric column), L4-07 (SpeciesCombatStyle.ParseTable), L4-08 (LoyaltyCurveedges). -
Deliberately NOT changed — L4-01 / L4-13: the sim's daily decay rounds a
.5to even whileLoyalty.DeltaForrounds away from zero (22.5 → 22 vs 23 at the shipped Uneasy defaults). Making them agree changes numbers players see and cuts against the standing "accept the slower decay" ruling, so it awaits Cobalt's call; the disagreement is pinned by a named test until then. -
Docs: 39 bound settings had no wiki mention (L6-10) — now in
docs/wiki/mods/beastwhispering/config-reference.mdwith the config path and an excerpt.
0.2.17 — 2026-09-04 (built, not live-verified)
- Evade chance scales with the burst (Cobalt's ruling 2026-09-04). The species' evade chance is now
calibrated as the chance against a 15-raw-damage hit (pre-armor — the ReceiveHit prefix's DamageList
total, the same read the armor-wear leg takes) and compounds exponentially with a bigger hit:
effective = 1 − (1 − chance)^(damage/15), so a Hyena's 10% reads ~19% vs 30 damage and ~65% vs 150,
and scales DOWN the same way below the reference (a 5-damage scratch ≈ 3.5%). Pure math in
Core.PetEvade.DamageScaled01(guards: certain/zero chances are preserved; an unknown damage read — a hit with no damage list — falls back to the unscaled chance). New[Evade] ReferenceDamage(default 15, host-authoritative in co-op); the[EVADE]dodge/land lines now printdmg=andbase=beside the effective chance.
0.2.16 — 2026-08-30 (built, not live-verified)
- A tame that succeeds says so, and names the route that asked (BW-D1). A successful tame used to be
silent on screen — the only evidence was a log line.
PetLifecycle.TameSpecificnow notifies at BOTH success exits: the master path (afterWildDespawn.Remove, reading the species name BEFORE the body is gone) and the guest-routed path (afterGuestTame.TameAsGuest). The guest exit was not in the brief; a guest tame is just as silent, so it got the same treatment. Chow is the one route that does NOT get a second line, becauseTameOnUseends with its own flavour text and one tame must never print two success messages. New Core vocabularyCore.TameRoutes(dev-key/dev-verb/chow/unknown) and every caller is named: the key press isdev-key, thetame/clonevisualverb isdev-verb, chow ischow, andPetLifecycle.Tamedefaults todev-verbso an unnamed programmatic caller is not mislabelled a key press.TameAttempt.Rollprintsroute=on both the rolled and the SKIPPED line. (tameany/tamecachedbypassTameSpecificentirely and are a separate finding, BW-M7.) - The F7 tame key is behind a dev flag, shipped OFF (BW-D2). New
[Taming] EnableDevTameKey, default false; the throttle that explains the refusal once, then stays quiet, and re-arms when the flag flips on is the pureCore.DevTameKeyGaterather than something eyeballed. Thetame/clonevisualVERB is deliberately NOT gated — that is the dev workflow's escape, and the reason a gate was acceptable instead of a cooldown. Dev hosts keep the key through the shared overlay (config/shared/cobalt.beastwhispering.cfg.overlaypins it true), since a changed default never migrates into an existing cfg; shipped profiles take no such line. Note for whoever ever turns this on for a player build: the key path still skips the split-screen-P2 veto and theEnableTamingFoodskill-switch (BW-M8), which would need porting first. - No pet container is minted on an NPC body (SK-D10). The root cause was TIMING, not the predicate:
both
Faction==Playerand!IsAIare stamped late on a SideLoader-templated body, so at ProcessStart a merchant reads exactly like a player. The gate is rewritten asCore.PetContainerGate— positive-and-early (OwnerPlayerSys != null; a player owns a PlayerSystem, a scripted body never does) → Mint; scripted / AI / non-Player → Refuse; the ambiguous shape → wait and re-ask, once per frame, up toDefaultMaxAttempts(20). StoryKit'sScriptedBodymarker is consulted too, but it is stamped in the rig pass — also late — so it cannot carry the fix alone; the deferred re-ask is what makes either signal usable. The bounded wait, not a single frame, is the point:PetInventory.Minthas exactly one call site, so a real player whoseOwnerPlayerSyslanded one frame late would have got no container EVER — the fatal mistake thePetInventoryheader names, against a leak that is merely cosmetic. NPCs still exit on the first frame any stamp lands, so the leak side pays nothing for the budget. The final-refusal line carries the re-ask count (1–2 = stamps landed at once; a body that runs all 20 never got stamped and is worth reporting), and theDeferpath with no plugin instance now warns by name instead of no-opping silently — that was the one route that could leave a container missing with zero log evidence. bw.ftk.leapbinds its sender (BW-M1).FtkLeap.AuthorizePlayerOwnerruns three rungs through the newCore.SenderBinding: the uid resolves, it is a player, and it is OWNED BY THE SENDER. The deferral reason recorded in the file header — "ResolvePlayerUidOwner is internal to CompanionKit" — was false, and being false is why the hole lived; it is corrected in place rather than deleted. The warn-once tag is distinct (ftkleap) so the resolver's per-(uid, rung) bookkeeping cannot silence the tame path's warning for the same uid.TameOutcomeRequest.OwnerDropnow delegates to the same ladder, pinned by a theory test, so the two call sites cannot drift to different drop reasons for one verdict.SenderBindingis boolean-typed on purpose: BW.Core does not reference NetKit.Core and must not start.bw.tame.despawnruns on the master only, and the claimed tamer must be the sender (BW-M2). It was the only BW handler still on the defaultAnyrole; it is nowRunOnMasterOnly.msg.OwnerUidwas log text — it is now BOUND through the sameSenderBindingladder asbw.tame.outcomeandbw.ftk.leap. The audit's proximity check was deliberately NOT added: a distance threshold chosen without a live reading would refuse honest tames at the edge of the taming radius, and the binding is the authority fix. If the binding turns out too strict the symptom is "guest has a pet, wild still standing" — the exact degradationTameAsGuestalready warns about (watch rowBWH13).- The save row is cleared even when the player object is already gone (BW-M4).
DespawnPetused to skip the clear in that window and leave a ghost row that resurrected the bond on every load.Core.SaveClearTargetpicks the key — the live player, else the bond's remembered owner uid captured before teardown — and the fallback goes through the newPetSaveStore.ClearUid→CompanionSaveStore.ClearUid, which existed all along; the old comment literally asked for this. A blank uid is treated as ABSENT rather than as a key, since an empty uid resolves to the ownerless global record. Known cost, accepted and recorded in code:PetArmorService.ReturnOnBondEndcannot mint into an absent player, and its retained-cell rationale was "the bond survives to be re-adopted" — it no longer does, so an equipped pet armor is lost in that window instead of returned. One item in a rare window against a bond that resurrects every load; rowBWH16wants a live measurement. marenheresaves Maren's stamped home explicitly. It used to rely on BepInEx's implicit save-on-assign. ForgeKit's FS21 fix holdsSaveOnConfigSetfalse on a ConfigFile while anyset … transientoverride is live, so after a single transientseton BW's channel the stamp would have lived in memory only and vanished at relaunch with no error.StampHerenow callsDevCfg.SaveWithoutTransientson the Maren config file (which also guarantees the save cannot drag a transient value out to disk with it). The copy-paste log line is unchanged.- All six sweep findings are BUILT and unit-green; none has been in front of a live session. Rows
BWH1–BWH16indocs/bw-mp-hardening-testplan.md; the findings themselves are written up indocs/bw-mp-audit-2026-08-30.md. - Pins raised to the sweep floors: CompanionKit 0.4.29, DonorKit 0.1.8, NetKit 0.2.8, StoryKit 0.1.9.
0.2.15 — 2026-08-30
recall/ F9 now ENDS a Stay order (CS9; confirmed live 2026-08-30, fix built and unit-tested, not live-verified). Pins CompanionKit 0.4.28. The recall put the pet at the player's feet and the pet immediately ran the whole way back to its stay spot at full speed, still in Stay — thestance: STAY ended — warped to the owner (…)line never appeared. Cause:PetLifecycle.RecallPetcleared only the commanded engage TARGET (Stance.DropCommanded()posts no stance signal) and then placed the body with the bareCompanionBody.PlaceAt(spot)— the dumb placement primitive, which raises no event. SoCompanionCombat.OnWarpedToOwnernever ran, the pure ruleStay + WarpedToOwner → Follow(Core.CommandCycle) never fired, and theCK_StaySpotobject stayed alive and outranked the owner in goal selection. The recall now places through the reason-carrying overloadPlaceAt(spot, "Recall"), so the Stay ends through the existing, tested machine and the log names the reason:stance: STAY ended — warped to the owner (Recall), stancewhy=warp:Recall.resummonwas never affected (it warps throughTryWarp, which always raised). Multiplayer needed no change: the guest's recall runs this same local path, and the master's copy of a proxied pet has an anchor but no puppet body or stay spot —ProxyPets.OnRecallonly drops the mandate and teleports the anchor, and the guest's stance mirror carries the resulting stance across.
0.2.14 — 2026-08-29
- Bundle tier takes precedence over a harvested template (IB9, Cobalt's ruling).
BundleBodies.EnsureRegisteredused to skip a species whenever ANY template was resident, so a warm-cache config (Expedition.AlwaysWarmSpecies,CaptureOnSceneEntry) that harvested a wild Pearlbird before the bundle registered shadowed the shipped rig silently —bundlebodyreadUntried, the pet came up on the unpatched wild controller and idle breaks parked (headless session on ba2d9763). Now a resident whoseOriginis notprebuiltis dropped (dormant destroyed) and the bundle registers over it:[BUNDLE] '<key>': a harvested template was resident … replaced by the shipped bundle. The idle-break parked-clock reason now names the shadowing case too.
0.2.13 — 2026-08-29
- Pearlbird idle breaks — the pet pecks and preens when you stand still (Cobalt's ask 2026-08-29; built,
not live-verified —
docs/pet-idle-break-testplan.mdIB1–IB8). Pins CompanionKit 0.4.27.- The Cierzo pearlbirds Cobalt pointed at are scene props on a legacy
Animationclip, not creatures; the pet gets the Mecanim peck/preen clipsPearlBirdIdleBreak1/2/3_ainstead (vanilla's cosmetic-pet controller has them; the wildac_PearlBirdDefaultour body runs did not).scripts/bundle-authoring/ BuildBundle.csnow patches a COPY of the wild controller (ac_PearlBirdDefault_bw:IdleBreaktrigger,IdleBreaksint, anIdleBreakssub-state machine with exit-time returns +IsMovingcancel) intobw_bodies.pearlbird— REBUILT 2026-08-29 via the Unity autobuild (log:controller patched (IdleBreak/IdleBreaks, 3 states), 10IdleBreakrefs in the_bwYAML); the committed bundle carries it. A bundle without the patch warns once and parks the clock (no spam). - NEW species axis
idleBreaks→SpeciesIdleBreaks.txt(Pearlbird=IdleBreak,IdleBreaks,3; overrideBepInEx/config/SpeciesIdleBreaks.txt,reloadidlebreaks).Core.IdleBreakTimerkeeps vanillaPet.cstiming (first 2–8 s after settling, repeats 5–15 s) as[IdleBreak]config shared by every species. IdleBreakDriver(10 Hz) fires when the pet is out of combat, settled and its owner is loafing (or the pet is on a Stay spot):SetIntegeron every animator owning the int +CompanionPetFx.PlayTrigger; eligibility lost within ~1 s of a fire resets the trigger. Ships ON ([IdleBreak] Enable = true) — cosmetic, fail-safe. No MP cue in v1 (peers see a still bird). Verbsidlebreak [n],idlebreakdump.
- The Cierzo pearlbirds Cobalt pointed at are scene props on a legacy
0.2.12 — 2026-08-29
- Pet evade / anchor — Cobalt's eyes-on rulings 2026-08-29 (build a3690730). Pins CompanionKit 0.4.26.
[Anchor] GlueOffsetBehinddefault 0.3 → 0 (EV23 PASS: "much better, still a little clipping into the hyena jaw, no jitter"). Enemies measure their stop distance to the ANCHOR; with it 0.3 m back on the hindquarters they stepped into the model to reach it and their swings passed through the body. The old rationale (hit-VFX read better on the body) is kept in the help text as history. The Bind now carries anAcceptableValueRangeof −0.5…1 — negative = the anchor a little AHEAD of centre, toward the jaw — so the residual jaw clip can be A/B'd live (set Anchor.GlueOffsetBehind -0.15+anchorphys);AnchorGlue.WeldPositionis sign-agnostic. Pinned at 0 inconfig/shared/cobalt.beastwhispering.cfg.overlay(a changed default never reaches an existing cfg). SelfTest's defaults pin follows.- EV24 wall-stuck guard. The a3690730 session ended with the pet off-mesh inside
mdl_env_buildingWoodHouseSmallAand a[PUPPET] warped to player … reason=CantReachrescue. Suspect (a): a CHAINED hop grounds a mid-arc point, and if that point is a navmesh sliver on the far side of a thin wall the next hop is "legal" from inside. Now a chain is allowed only whenNavMesh.Raycast(previous hop start → current position)crossed no boundary — else[EVADE] dodged (no hop: blocked (chain crossed an edge))(pose + sound; the running hop lands on its own; counterchainsRefused). Suspect (b) — enemies shoving the puppet — gets the breadcrumb: the first!isOnNavMeshread after any hop (polled fromCompanionBody.OnAfterMove, suspended agents excluded) logs[PUPPET] off-mesh after a hop on '<body>' at (x, y, z) — last hop: …; clip: …; last 3 hop start(s): …once per hop, so a following CantReach warp can be attributed.evadedumpaddschainsRefused=,offMeshBreadcrumbs=and ahop trailline. Pure:Core.HopChainGuard,HopTrail,OffMeshWatch. - Hit-frame damage rides CompanionKit 0.4.26 with no BW code: the pet's bite is dealt when its attack
clip's
HitStartedevent fires, and a dodge during the wind-up now drops the ORDINARY bite too (swing cancelled (evade) … — pending bite dropped). Gate:cobalt.companionkit.cfg[Combat] DamageOnHitFrame. Testplan EV25.
0.2.11 — 2026-08-29
- Pet evade — hop CHAINING + dodge cancels the attack (Cobalt's eyes-on rulings 2026-08-29 on build
6b63444b; still ships DARK). Pins CompanionKit 0.4.25.
- Every negated hit hops. A second eligible hit inside the previous 0.3 s tween used to be negated but
skipped as
leap in flight(our own hop suspends the agent exactly like an FTK leap), and a hit inside the ~0.83 s Dodge clip re-fired a trigger the Animator ignored — "it acts like the dodge animation is on a cooldown". Now OUR OWN running hop is cut where the body is (StopCoroutine+ a by-hand landing:PlaceAt+ release) and the new hop starts from the current position:[EVADE] hop chained (cut the previous at X.X m), counterhopsChained. Only someone else's suspension (an FTK leap in flight) still skips. Pure sequencingCore.HopFlight(Start | Chain | Skip + serial-matched release; tests). - The pose restarts per hop through
CompanionPetFx.ReplayTrigger(CK 0.4.25), the dodge sound plays per hop, and the face-the-attacker turn + re-engage hold re-arm on each chained hop (the hold is extend-only). - A dodge CANCELS the pet's own in-flight attack:
CompanionCombat.CancelSwing("evade")before the pose — clears a queued Attack1 and aborts aPetSpecialAttackwindup still on its timer ([SPECIALATK] windup cancelled (evade) — no strike, no status, no synced strike; cooldown stands; the For-the-Kill strike rides the same windup, so it is covered).evadedumpcounters gainhopsChained= swingsCancelled= hopInFlight=. Overrules the plan's "known cosmetic mismatch" (the bite no longer lands through a dodge). - EV7 fix: vanilla AREA spells are
Blastsources with a PLAINPunctualDamage(Ice Witch frost nova), which the AoE-component test called direct — aBlast-typed source is nowclassified aoe (Blast)and never rolled (Core.PetEvade.IsDirectStrike(…, isBlast); the three-flag overload stays). Projectiles / weapons / attack skills are unchanged. - EV2 instrument:
evadedumpprints a 10-bucket histogram of the last 100 natural rolls (bias vs chance). - Review rulings folded in: an FTK leap in flight keeps its strike (
… leap in flight; strike kept); a cancelled signature windup reportsMissedto Hunt synergy (unless the press suppressed synergy) so the window closes.
- Every negated hit hops. A second eligible hit inside the previous 0.3 s tween used to be negated but
skipped as
0.2.10 — 2026-08-29
- Pet evade — the eyes-on follow-ups (Cobalt's rulings 2026-08-29; still ships DARK).
- EV18 hop COLLISION: the flat hop path is clipped at the first NavMesh edge (
NavMesh.Raycast) AND the first world collider (Physics.SphereCast, r 0.4 m at chest height, environment mask) — nearer end wins (Core.PetEvade.ClipHop), then the landing is sampled onto the mesh. Under 0.5 m = no hop:[EVADE] dodged (no hop: blocked), counterhopsSkipped,last hop: skipped (blocked). - Degenerate direction: a pet standing ON its target (flat
anchor − dealer< 0.5 m) hops straight back along its own facing (Core.PetEvade.HopDirection; host-side, so guests get the same dir) — the dodged line carriesdir=back-along-facing. [Evade] HopMetresdefault 2 → 3.4 (the 170% ruling); overlay-pinned.- New
[Evade] ReengageHoldSeconds(0.6, 0–3): after the hop the combat drive holds where it landed (CompanionCombat.HoldReengage, CK 0.4.23) instead of re-closing within 0.3 s. - Face the attacker before the Dodge trigger (
CompanionBody.FacePointtoward −hopDir) so the back-hop clip reads as a back-hop. forceevade [n|off]dev verb: the next n eligible direct strikes dodge without a roll ([EVADE] FORCED dodge …); shown inevadedump.- Warn-once-per-source classifier line at Message level:
[EVADE] source '<name>' (<type>) classified direct|aoe|none — PunctualDamage=<n> AoE=<n>;evadedumplists the last 8 (EV7's discriminator).
- EV18 hop COLLISION: the flat hop path is clipped at the first NavMesh edge (
- Requires CompanionKit 0.4.23.
0.2.9 — 2026-08-29
- Pet evade — the hyena back-hop dodge (
evadespecies axis). SHIPS DARK ([Evade] EnableEvade = false). A species with an evade row (SpeciesEvade.txt, generated from the manifests; Hyena10,18,Dodge) has a loyalty-scaled chance — 10% at loyalty 0 rising to 18% at Eternal — to dodge an incoming DIRECT strike (melee / arrow / bolt; never an AoE or a status tick): the hit is negated on the anchor through the ONE sharedReceiveHitprefix (fifth consumer, asked after Brace and Unerring so one protection per hit), and the visible pet playsDodge+ its dodge sound and hops[Evade] HopMetres(2 m) away from the attacker overHopSeconds(0.3 s) — a tween, never a teleport, so the anchor weld stays on its cheap path. 3 s cooldown per pet, stamped on success only.[Evade]config,BepInEx/config/SpeciesEvade.txtoverride,reloadevade,evadedump,[EVADE]/[EVADETABLE]log tags, SelfTest attach + seam checks. Co-op: the HOST rolls for every pet in the room (its own and each guest's proxy anchor — a guest's own override/flag never applies); pose + sound play on every machine via CompanionKit'sck.pet.cue; the hop runs on the owner's machine and reaches peers through the position stream. The guest's announce now carries RAW loyalty as a 5th field so the host can price the top of the ladder. NOT live-verified —docs/pet-evade-testplan.mdEV1–EV16. Pins CompanionKit 0.4.22 / NetKit 0.2.7.
0.2.7 — 2026-08-28
-
Fix stale dependency pins across the fleet; DonorKit 0.1.7
-
tier strip: drop the gold hairline beside each padlock (Cobalt: stray lines left of the lock)
-
loyalty tier strip: 14 boxes + padlocks above the Companion-tab Loyalty bar
-
review nits: payout-band wording, stale test name
-
loyalty tiers: pure rename of ordinals 4–11 (Cautious/Steady/Trusting/Devoted/Unshaken/Boundless/Sworn/Fierce)
-
loyalty ladder: drop the Fraying lock (locks start at Steady) — Cobalt 2026-08-26
-
LT22: templateclear spares prebuilt bundle bodies; bundlebody reload actually reloads; [STATS] from-save breadcrumb
-
LT20/LT21: species stat ledger + one stat-source rule; powerdump verb; deploy-test relaunch guards
-
LT17/LT18/LT19 root causes: cfg layer resolves first (ForLayered), ladder label from installed table, buffdump names the Communion gate
-
review fixes: stale setloyalty range + ladder step comments
-
Loyalty overhaul: 14-tier ladder, locks (save v18), 0-8 payout stretch, [Loyalty] config, TierMax 8 (CK 0.4.20); integrate LoyaltyPower via LoyaltyLadder.PowerStep
-
Merge branch 'main' into worktree-agent-aa0d23223d47cd5ab
-
Loyalty 14-tier ladder WIP: LoyaltyLadder/LoyaltyCurve, locks (save v18), 0-8 payout stretch, [Loyalty] cfg, TierMax 8 (CK 0.4.20)
-
Loyalty power applier + powerScale species axis + dormant relics (plan §3/§3b)
-
BW: pet self-feed work in progress (another session's in-flight edits)
-
BW: [Pet] LeashDistance 42 -> 84 (one leash number); decouple the coverage-gap backstop
-
CK 0.4.18: the anchor's own leash is unreachable by design under GlueMode=Always
-
firecamp dev verb + the pet self-drink trigger, established
-
Fix two live-found P1 regressions: the first-crossing +5 and the pouch leak
-
Pet sustenance + weather tolerance + refused waters
-
… and 89 more (see git history)
-
14-tier loyalty strip above the Loyalty bar (Companion tab → Bond). One small box per ladder row, filled green left-to-right as the pet's tier rises and greyed again on a drop; a tiny padlock before every lock row (4, 6, 8, 10, 12, 13). The padlock reads the persisted decay floor, not the live value — faded open until the pet has reached that tier, solid gold once the floor is secured — because a lock is a ratchet, not a gate. Pure model
Core.PetTierStrip(length followsLoyaltyLadder.Rows, soLadderOverridereshapes it);petpaneldumps it astiers=[####L##L##|..|..|.|.].[HUD] ShowLoyaltyLockGlyphs=falsedrops the padlocks for a plain gap. NOT live-verified —docs/loyalty-14-testplan.mdLT24–LT27. -
[Pet] LeashDistance42 → 84 — one leash number, in and out of combat (Cobalt's ruling, 2026-08-25). The earlier 30 → 42 (+40%) move was the right knob but not enough rope: a live boundary sweep pinned the out-of-combat warp at exactly 42 m ([PUPPET] … reason=Leash) and Cobalt chose 84 over 50/60/75 deliberately, so the out-of-combat leash now MATCHES[Combat] CombatLeashDistance(84). In practice the warp stops being a leash you feel and becomes a stuck-backstop. The Bind description no longer says "keep it under 50 (the zone-change re-place distance)" — that guidance is deliberately overridden and the doc now explains why instead of contradicting the shipped value.config/shared/cobalt.beastwhispering.cfg.overlaypins the new value, because BepInEx never migrates a changed default into an existing cfg.[Follow] LoafDistanceMin/Maxstay at 3/5 (ruled: leave them alone) and[Anchor] AnchorLeashDistancestays at 28 (inert for local pets underGlueMode=Always). SelfTest's stale[Pet]/[Combat]default pins (leash 30, combat leash 60) were corrected to 84/84 in the same pass — they would have logged false FAILs. NOT live-verified:docs/follow-polish-testplan.mdF21 owns the new 84 m boundary, F14's PASS remains the record of the 42 m one, and F15 was re-specified because its premise ("a pet fighting 50–84 m from a standing owner must never be yanked home") no longer says anything distinct once the two leashes are equal. -
The unattended self-feed's trigger is now testable and written down (no behaviour change). A live session's "the pet never self-drinks salt water from its own cargo" leg came back INCONCLUSIVE because its POSITIVE CONTROL — a clean waterskin at ~90% thirst — also went untouched for 3.5 minutes. The path is not broken (SF2/SF10 both drank a cargo waterskin on command); it has preconditions that lived only as inline expressions in
PetFeeder. They now live inBeastwhispering.Core.SelfFeed(IsThirsty,MayDrinkUnattended) with unit cover inSelfFeedTests, and the full list — including the invisible one, thatTickSystemsis skipped entirely whileNetworkLevelLoader.IsGameplayPausedholds while every dev verb keeps answering normally — is indocs/pet-self-feed-testplan.md§ "The trigger", with a recipe for a control that cannot be silently missing. SF12 is OWED for the re-run. -
The first region crossing actually grants its +5 now (V28 regression fix). The arrival ruling announced a first crossing and moved the pet's loyalty by nothing. The award call was never missing — the row's NUMBER was:
CrossedRegionBondedcarried the literal+5FACE value it was written with before the 2026-07-25 gain squish existed, and every positive delta is scaled by[Systems] LoyaltyGainPercent(default 5%), so a crossing paid 0.25 of a point into the carry bank and nothing visible. The combat rows were resized in face value for the squish in the 2026-08-07 wave; this one was missed. Face is now sized the same way (Loyalty.CrossedRegionFace100 x 5% = the promisedCrossedRegionPoints5), so the gear gain multiplier and a non-defaultLoyaltyGainPercentstill apply exactly as they do to a kill. The[TRAVEL]arrival line now prints what actually LANDED (+N granted, plus the carry) instead of the pet's current value, so a payout of zero can never again read as a successful award. Built; live-verify owed (skilltree-overhaul-testplan V28). -
The Beast of Burden pouch +3 can no longer outlive the bond (BB5b regression fix). On the no-backpack release path the +3 stayed on
PouchCapacityafter the bond ended, while the applier itself believed it held nothing — a bookkeeping desync, so the withdrawal had nothing to remove. A withdrawal is now authority-first and cannot be skipped: it removes by SourceID straight off the live stat (newCompanionKit.PlayerStatBuff.Purge, CK 0.4.16 — and it recomputes the stat's cachedCurrentValue, which vanilla only refreshes every third frame),ClearPouchlost thewe hold nothing -> returnearly exit that made the one call that could have caught this a no-op,PetBagPerk.Clearwithdraws the POUCH before the backpack stamp, and the teardown ends with a final sweep AFTER the pet aggregate is dropped, where no re-stamp is possible. That sweep is silent unless it finds a stranded stack, and WARNs with the amount and the path when it does. A capacity bonus that survives its bond is the save-adjacent bug class, so this holds on EVERY bond-end path (release, abandonment, permanent death, session teardown), not just the one that failed. Built; live-verify owed (skilltree-overhaul-testplan BB5b, and BB5 as the control). -
A refused item heal now says why. The two NO-WASTE refusals — a pet at full health, and a wound too small for the offer (a quarter of the item's own value must be able to land) — used to leave no trace of their own: the offer fell through to the diet rung and surfaced as a generic NotInterested/Satiated, indistinguishable from "this pet doesn't eat apples". Both now print
[ITEMHEAL] '<item>' -> <pet>: nothing to heal …/… too little to heal …, from the existingCore.ItemHeals.Explainsurface, deduped so the dry run and the real run do not double it. The boring declines (kill-switch off, an item with no heal at all, no live anchor, a lower rung claiming the offer) stay quiet on purpose. Built; live-verify owed (pet-item-heal-testplan IH4, IH12). -
Docs: the healing-potion family was named after items that do not exist. "Health Potion" / "Greater Health Potion" / "Mega Health Potion" are not in this game — the vanilla family is Life Potion (
4300010) and Great Life Potion (4300240), two tiers, not three. The drift came from the PREFAB names, which are pre-rename (4300240_MegaHealthPotion_…), so a decompile read gives the old marketing names and the item list does not. Corrected throughout the item-heal and status-cure docs, the config descriptions and the code comments. Behaviour is unchanged — nothing was ever keyed on a name — but the[Systems] EnableItemHealingand[Cure] EnableStatusCuresdescriptions inBepInEx/config/cobalt.beastwhispering.cfgnow name items a player can actually find. -
Pets travel freely across regions — the Wild Unknown travel gate is gone. Region fast travel (the long, ration-consuming journeys and the merchant caravan) can no longer break your bond, in any configuration of the game: no pre-travel warning toast, no despawn on arrival.
Wild Unknownstays purchasable from Maren (500 silver, still the tree's breakthrough) and still grants the tame-tier loyalty head-start and full pet damage — and now grants its +5 first-crossing loyalty bonus unconditionally, so every pet earns it. RETIRED config key:[Skills] EnableWildUnknownGate. An existingcobalt.beastwhispering.cfgkeeps the orphan line until it is regenerated; it is inert — do not hand-delete the cfg (see.claude/rules/config-sync.md). Dev verbstravelcheck/travelcrossnow report crossings, not a gate. Note there is now no config left that turns any of this off: the fast-travel hooks always capture their (tiny, in-memory) travel record, even for a player with no pet, and the first-crossing bonus always applies. Built; live-verify owed (skilltree-overhaul-testplan V27, V28). -
Pet leashes widened 40%.
[Pet] LeashDistance30 -> 42 m (out of combat),[Combat] CombatLeashDistance60 -> 84 m (while fighting) and[Anchor] AnchorLeashDistance20 -> 28 m, for more room to roam before a warp-home.[Combat] OwnerFocusRangeis unchanged at 60 m and so still sits under the combat leash. Existing installs need the overlay (scripts/sync-config.py --apply) — BepInEx never migrates a changed default into a generatedBepInEx/config/cobalt.beastwhispering.cfg. Live-verify owed: F14–F18 indocs/follow-polish-testplan.md(F15 is the one that matters — a pet fighting 50–84 m from a standing owner must never be yanked home). -
Beast of Burden now loads the pet and your pouch, not your backpack. The skill used to add a flat +5 to whatever pack you had equipped. It no longer touches the backpack at all: its capacity is split 70/30 — 70% onto the pet's own inventory (on top of whatever its species can already carry) and 30% onto your pouch. It is still one setting, not two:
[Skills] BeastOfBurdenCapacityinBepInEx/config/cobalt.beastwhispering.cfg, whose default moves 5 → 10 so the split reads +7 pet / +3 pouch;0still switches the skill's capacity off entirely. The split ratio is fixed in code.[Skills] BeastOfBurdenCapacity = 10A
.cfgthat already exists keeps its old5(BepInEx never migrates a changed default), which would read as +3.5 / +1.5 — set it to10by hand, or letscripts/sync-config.pypatch it from the shared overlay, where it is now pinned. Both shares are bond-scoped and withdrawn when the bond ends, exactly like the old backpack bonus was. The per-species backpack gift (a Phytosaur'sBagCapacity) is a different feature and is unchanged.buffdump/petstatusnow print all three destinations. Live-verify owed. -
Feeding your pet a health potion heals it. The heal is the item's OWN vanilla value, read straight off its effects — so Life Potion and Great Life Potion, healing mixtures and any modded potion all work with no data entry anywhere; there is deliberately no per-potion table. A potion is not food, so no species diet has to list it: any pet takes any potion, satiation never blocks it, and it grants no meal, no loyalty and no regen (an instant heal and the food Health Recovery buff stay different things). A pet already at full health refuses without consuming — and so does a pet whose wound is too small for the offer, so a scratch never burns a Great Life Potion (a quarter of the item's value must be able to land; reach for a smaller remedy). Damage effects are ignored, never applied. Precedence: a species relic or an authored buff food is still that rung's, and a diet food that happens to heal is fed as a meal on its authored heal (never both) — Healing Water keeps behaving exactly as before. A Great Life Potion heals and cleanses Bleeding on the same single consume. New feed rung
heal([Systems] EnableItemHealing, default true;[ITEMHEAL]log lines). Live-verify owed. -
Pets get hungry far more slowly, and chow is a proper meal. A hunger (and thirst) "day" is now two real hours instead of twenty minutes (
[Systems] HungerSecondsPerDay/ThirstSecondsPerDay1200 → 7200), and a fed pet refuses food for half of that. A species chow keeps your pet fed twice as long as any ordinary meal — satiated for 2 hours, hungry only after 4 ([Systems] ChowSatietyMultiplier, default 2). The stretch is remembered with the pet, so a zone change or a reload cannot quietly downgrade a chow to an ordinary meal. Live-verify owed. -
Chow's loyalty gift doubled, 20 → 40, so it stays the best food per hour. Because chow now lasts twice as long you feed it half as often — at its old value that would have made the most expensive item in the game the slowest way to build a bond (10 loyalty/hour, beaten by an ordinary 12). At 40 it leads on every axis: most filling, biggest single gift, and 20 loyalty/hour against the best ordinary food's 12. Live-verify owed.
-
Pets take much longer to get cold or hot — and the campfire no longer cooks them. The discomfort ramp went 30 s → 300 s per stage (
[Systems] TempEscalateSeconds), while recovery quickened to 10 s. That slow ramp is deliberately the main lever instead of ever-wider comfort bands: a short stop at a fire never escalates at all, while a long trek through a genuine extreme still does — so blankets, weather foods and the Weather Defense Potion keep their job. Comfort bands were re-cut around the fact that a lit campfire adds +3 ambient steps within 3 m and that HP drain only begins two steps out of band: temperate pets (Cold–VeryHot) can be made uneasy by a fire — including at a desert or volcanic camp, where a fire reaches the top of the scale — but never drained by one. The cold side is deliberately left narrow, so a real freeze still bites. Archetypes stay distinct — cold-leaning cousinsColdest–Hot, marsh sauriansVeryCold–Hot, desert and volcano nativesFresh–Hottest. Resting at a fire is safe: the pet clock runs on real play time, not the in-game world clock. Live-verify owed. -
The Glacial Tuanosaur still suffers beside a fire — on purpose. It keeps its
Coldest–Warmband; a glacier-dweller wilting at camp is the intended cost of that archetype. -
Your bond decays more slowly, and that is the point. The daily loyalty loss was left untouched while the hunger day quadrupled, so a neglected pet loses ground roughly six times more slowly than before. Deliberate: pets were abandoning during ordinary breaks in play.
-
Pets refuse salt water and rancid water. Both lost their "every pet may drink this" status and carry a new
refuseflag inWeatherFoods.json: never a drink, never a meal, and never taken unattended out of the pet's own inventory — including when offered inside a filled waterskin, which the feed path resolves to the water it contains. Offering one now says so ("… sniffs the Salt Water and will not drink it"). ⚠ One thing you could do before, you can no longer do: salt water will not douse a BURNING pet. A drink the pet refuses cannot cure it. This is intended and approved, not a regression — Clean, River, Sparkling, Leyline and Healing Water all still douse. Live-verify owed. -
All seven settings are pinned in the shared overlay (
config/shared/cobalt.beastwhispering.cfg.overlay), because BepInEx never migrates a changed default into an existingBepInEx/config/cobalt.beastwhispering.cfg:[Systems] HungerSecondsPerDay = 7200 ThirstSecondsPerDay = 7200 SatiationFraction = 0.5 ChowSatietyMultiplier = 2 TempEscalateSeconds = 300 TempRecoverSeconds = 10 [Temperature] CriticalDrainPerMinute = 4The hunger/thirst and temperature clocks are baked when a pet is created, so an existing pet needs a re-tame or a cold reload before the new pacing applies; satiation and the drains apply live via
reloadcfg. -
Fletched arrows now re-stack with matching arrows. A craft's output merges into any stack of the same arrow carrying the same fletching — including the stack loaded in your quiver — and the stacks one craft produces consolidate with each other, so fletching no longer fragments the quiver. Differently-fletched stacks still never combine, an empty quiver is never auto-equipped, and on a co-op guest the step is a no-op (the host consolidates). Live-verify owed (testplan V23).
-
One feather now fletches up to 15 arrows, gathered across stacks. Popular archery mods stop arrows from stacking in the survival crafting menu, which made a feather worth a single arrow. The craft now fills a budget (
[Gifts] FletchArrowsPerFeather, default 15) from every stack of that arrow type in your pouch and equipped bag (the two containers the crafting menu itself scans); a stack past the budget is split and the overflow is left as it was.0= the old uncapped whole-stack behaviour. Live-verify owed (testplan V22). -
Pets no longer attack AggroKit-protected characters (the DangerousRoads road merchant): the refusal lives in CompanionKit's target ladder, which the special attack and synergy paths read from, so
[SPECIALATK]can no longer land on one. Live-verify owed. -
Taming can fail. Every real attempt rolls: success / the beast refuses but lets you be for 60 s (an AggroKit truce — broken the moment you or your pet hit it) / the beast refuses and attacks (pack included). 1/3 each normally; from stealth (sneaking AND unnoticed) 1/2 · 1/4 · 1/4. The chow is spent on every roll; refusals (already bonded, out of range, untameable) still keep it.
[Taming] TameRollEnabled+ four chance keys +TamePassiveSeconds. Co-op: a guest's failed roll is applied by the host (bw.tame.outcome). Dev:tame [force].
0.2.5 — 2026-08-19
- Second-review adjudication: drop a dead dedupe, lift the claim rule into Core
- Review fix: a lower rung's REFUSAL is not the cure rung's invitation
- Review fixes: ck.proxy.status must never read as live when it isn't
- Correction: water DOES douse burning in vanilla — adopt the game's own drink cleanse
- Pet status cures: water douses burning, Antidote cures poison, Bandages stop bleeding
- Pet DoT auras: a burning/poisoned/bleeding/plagued pet visibly wears the status FX
- ck.proxy.status: master streams the proxied anchor's status list to its owner
- Rebuild pearlbird/veaber bundles from the WILD rigs; correct the size prediction
- Vampiric HAO/FTK fix: synthetic strikes now fire Weapon.ProcessAbsorbEffects
- Fix companion locomotion params never reaching the skin-driving animator
- Fix companion locomotion freeze on optimized-rig species (Veaber/Pearlbird)
- Park pet armor: EnablePetArmor default false (Cobalt 2026-08-18)
- Review nits: strip after UID stamp (log names our container), live-retest watchpoints
- PI19 fix: strip the pet cargo container's own interaction components on every path
- Re-assert pet-container keep-alive on the reuse path
- Vampiric overflow heals the pet once player health tops out
- Fill a waterskin that rides in the pet's inventory
- Pearlbird payoff rework: per-stack Synergy bonus + taunt-on-Synergy
- Merge V6b fix (log-level gate: 14 unwrapped sinks retyped + requested-output exemption), peer-reviewed
- Review fixes for V6b: the tab-injector field, and the row keeps its id
- … and 30 more (see git history)
0.2.4 — 2026-08-11
- Merge petinvincible dev verb: lethal-damage clamp on the pet anchor via the shared BraceReceiveHit choke point (test-automation item 11; PINV1-PINV8 retest owed)
- Merge FTK execute-credit fix: armed ExecuteCreditWindow pays the execute value at death (CL4 FAIL root-caused via decompile; retest CL4b owed)
- petinvincible verb: session-scoped lethal-damage clamp on the pet's anchor
- CL4 fix: For-the-Kill execute credit is armed by the blow, paid by the death
- Species relic HP retune: 1.09 -> 1.05 per-stack (Cobalt's ruling 2026-08-10)
- Breakthrough-synergies master feature flag: [BreakthroughSynergies] Enabled (default false) gates all five Shamanic Pet Resonance legs
- Slope-tilt review wave: pure smoother + hysteresis deadband + per-species axis
- Brace echo: Fable-review fixes (warn key name, dead-attacker Discipline)
- 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
- Pet orb: add the gold rim (clone Mana's ManaFrame ring)
- Pet orb: brighter still (#3FD649 -> #66FF6E)
- Pet orb: show HP/MaxHP inside the circle by default (CombatHUD look)
- Pet orb: much more vibrant green tint (#2F7A33 -> #3FD649)
- Merge feat/runic-echo-e: Shamanic Pet Resonance leg E — Runic Lightning lantern bolt (dormant until SR-E0 discovery)
- Leg E: Runic Lightning lantern bolt — discovery-bound driver, runicdump ritual, bw.runicbolt route
- Merge feat/infuse-echo-d: Shamanic Pet Resonance leg D — Infuse Fire/Frost typed pet damage rider
- … and 61 more (see git history)
Unreleased
- Pet thirst (built, NOT live-verified — docs/pet-thirst-testplan.md TH1–TH9): companions now get
thirsty on their own 20-minute clock, mirroring hunger — Thirsty/Dehydrated paw badges
(commissioned Proboina art), hydration rows on the Companion tab, and a full dry day starts
loyalty decay. Hunger and thirst now EACH carry half of the old combined daily loyalty loss, so
a pet both starving and dehydrated loses exactly what a starving pet used to (default 15), and
one axis alone loses half. Only a drink quenches thirst — any waterskin, tea or potion the pet
accepts; meals never do, and a pet can always drink (no satiation gate). Old saves load as
"just drank" (save format v14); config
[Systems] ThirstSecondsPerDay/ThirstyIconFraction; dev verbsetthirst. - New artwork by Proboina: the seven active skill icons, the four passive icons, the heating and cooling blanket icons, and the companion status badges (hunger, cold and heat, scent, synergy, killing favour, and the four fed-buff badges). Pet statuses now read as paw-shaped badges — bronze for a boon, red for a malus — so a companion's state is distinguishable at a glance from your own buffs and debuffs. Same-size drop-in replacements; no settings or saves are affected.
- Diagnostics (built, NOT live-verified): the guest's
[DIAG] player:and[DIAG] pet:position lines gain an appendedpt=shared-clock stamp (PhotonNetwork.time, the identical format the host's[PROXY]position lines carry) so both machines' position forensics align directly; existing tokens stay byte-stable. Also investigated scoping the announce-time stance/target/pos mirror resets to fresh announces only (~85 redundant stance sends/session measured) and deliberately KEPT them on the periodic re-announce: they are the only unconditional re-seed of a master-side proxy row resurrected mid-session (the guest has no reliable resurrect signal — the announce is un-acked andck.proxy.hpclearis lossy), and cutting them could leave a passive pet's resurrected row permanently engaged (the 2026-07-20 stance-desync bug class). The load-bearing comment now says so in code; no behavior change. - Fixed (built, NOT live-verified): as a co-op guest, feeding your pet, taming with a chow,
wrapping a blanket and applying a bandage never actually spent the item — the game only lets
the host destroy items, so the last one in a stack quietly survived. In practice that was an
exploit: infinite feeds and infinite tames from a single chow on a guest. All four now consume
the way the game itself does for a guest (the host is asked to destroy the item, and it
vanishes from your bag immediately), and every consume now re-reads the item afterwards and
logs a loud
consume read-back MISMATCHwarning when the stack count contradicts the consume — this catches the shortfall class of failure (a consume the item's own state refutes); for a single last item a lost host round-trip can still read as consumed, soinvdumpremains the ground truth for "did it really leave the bag". Known limitation, deliberately deferred: a guest's waterskin charge spent on feeding is only decremented locally and can revert on the next sync (the game has no guest-side channel for it; low severity). - Fixed: in co-op, taming a creature the host had spawned could leave your new pet stuck at the
world origin, flickering, until you quit to the main menu. The real cause has been found and
fixed at the source: eating a taming food runs the pet's body-build inside a Unity animation
event, a context where the engine silently refuses to remove components — so the build now waits
one frame before it happens, out of that context entirely. As a safety net, the watchdog that
notices a stuck pet body and quietly rebuilds it in place (bond, health, cargo and save all
untouched) now also catches a mis-built body INSTANTLY rather than waiting ~20 seconds, and the
petunstickdev verb still forces a rebuild on request. And your pet's spell-effect flourish can no longer land on the wrong character in co-op — it had the same underlying bug. Tunable under[Watchdog]incobalt.beastwhispering.cfg. - Your pet can carry for you. A new Pet section sits in your inventory between the pouch and your
backpack, wearing your companion's name; right-click any item for "Move to <name>" (or just drag
it in). Each species has a carry weight — most manage 3 weight, the Phytosaur hauls 8 — and a pet
can't be loaded past it. Cargo stays with the bond through knockouts and loading screens, and if
the bond ever truly ends the pet shakes its load off at your feet (into your backpack, or your
pouch). Configurable under
[Systems] EnablePetInventory/PetCarryWeightDefaultand[Pet] SpeciesCarryWeightsincobalt.beastwhispering.cfg. - Your pet now shows the spells you cast on it. Cast Runic Lantern and its glow floats over your companion; cast Mana Ward and a bubble encases it — and there is a flourish at the moment of the cast, with the creature playing its own animation where one has been set for its species. In co-op everyone sees it, on their own copy of the pet, whether the pet is yours or theirs. The lantern share that only ever worked for the pet's owner has been replaced by this, so a friend finally sees your pet lit up too.
- Which visual belongs to which spell is a small data file you can retune without rebuilding the
mod (
PetSpellFx.jsonin the mod's config folder, plusSpeciesFlourish.txtfor the per-creature animation), reloadable in a running session. The animation table ships empty on purpose — animation names differ per creature and can only be read off a live one, and a creature with no entry simply doesn't animate. - Off-switch:
[SpellFx] EnableSpellFx. The older[Lantern]settings are superseded and no longer read; ward protection itself is unchanged — only its visual moved.
0.2.3 — 2026-08-02
- Reaper fail-safe: never judge a body when no consumer has spoken
0.2.2 — 2026-08-02
- Fixed: installing through r2modman / Thunderstore Mod Manager produced a broken mod. If you
installed Beastwhispering that way, please update — none of the mod's custom skills or items
existed for you. The package was laid out in a way the mod manager rewrites rather than copies:
it flattened the
SideLoader/Items/…folders into a single pile, and SideLoader loads packs only from a folder of that name, so all 14 skill and item packs (Hunt as One, For the Kill, Pet Command, Release Pet, Pet Gift, Communion, Wild Unknown, the four sigils, and the rest) were silently skipped. Nothing errored — the plugin loaded normally and the mod was simply half missing, which is why this went unnoticed. Manual installs and the hand-off bundle were never affected. Updating replaces the whole mod folder, so there is nothing to clean up by hand. - The SideLoader pack now carries a
manifest.txtpinning its name toBeastwhispering, so it no longer inherits whichever folder name the installer happened to use. - Hunt as One and For the Kill now proc your weapon's on-hit effects. An enchantment that explodes
in fire when you hit, a weapon's own on-hit status build-up, an imbue — all of them fired on an
ordinary swing but stayed silent on both signature skills, because the player's half of those
skills is a synthetic hit and the engine fires weapon hit effects from a separate call that
nothing was making. The melee strike now makes it, exactly as the game's own skill-damage
component does. Bows are unchanged: that half is a real arrow and already procs at impact.
New kill-switch
[HuntAsOne] ProcWeaponHitEffects(default on, live viareloadcfg) and ahitfxdumpdev verb showing what your equipped weapon would fire. - Balance: For the Kill cooldown 60s → 35s, Hunt as One stamina 10 → 5.
- Fixed the inverted Appetite bar on the companion tab — it plots satiety, not raw hunger.
- Blanket recipe scrolls now drop from world containers, not creature kills; taming-scroll drop rate cut to 33%.
- Co-op fixes: cross-machine cast replay, taming as the host, and effigy yaw/speed.
- Effigy wave: placement gate, universal agent bind, body census and reaper.
- Pin-session fixes: recall moves the anchor, the replica fuse is defused, glide, swing mirror.
Under the hood: a static-analysis sweep across the mod and its compute layer — null-dereferences, wrong clocks, lying numbers, five silent-normalisation bugs (each with a test), swallowed exceptions and unsent guest requests now report instead of failing quietly.
0.2.1 — 2026-08-01
Documentation only — no code changes from 0.2.0.
- Document the known issue: taming only works in single-player. Using chow on a wild creature currently only succeeds in a solo session. An already-tamed pet is fully playable in multiplayer — it follows, fights, feeds, bonds and persists in co-op exactly as it does solo — so tame your companion in single-player and then bring it into your co-op session. This is a bug, we are working diligently on it, and the note comes out the moment it is fixed.
0.2.0 — 2026-07-30
- Pin BW's direct SideLoader dependency, not just the transitive one
- Thunderstore release prep: changelogs + real category tags for BW
- Fix the passive icons: 84x128, not square — and make the checker catch it
- Regenerate the For the Kill icon: wolf jaws closing on the skull
- Cap all-status build-up resistance below the engine's refusal sentinel
- Release-readiness fixes: a private hostname was shipping into player configs
- Opus review fixes: the changelog the release train would have mangled
- Pre-release review fixes: the public wiki, the unguarded scroll seam
- Close the review items; fix a live double-claim that killed both blankets
- Review fixes: the shared kits must not enforce OUR id allocation
- Fix the id stamper writing "$187000" into all 14 SideLoader packs
- Thunderstore release prep: BW/SkillKit/StoryKit manifests + descriptions
- DonorKit gets a painterly butterfly net; Disengage now reads "return and stay"
- HealPet: bandaged paw + a bold green cross that can't be clipped
- Draw every custom id from the community-allocated pool (87000-87999)
- Lane 3 review fixes: honesty gates on the container + scavenge-sim verbs
- Lane 3: container + caravan queue-unblocker verbs
- Fix pet-sigil V9: Spark now combos off pet circles (ProximityCondition alias)
- Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule
- Unify the BW iconography: one tan skill field, one item field, vanilla slot aspect
- … and 310 more (see git history)
⚠ Read this before updating from any build made before 2026-07-30
Your existing save loses every Beastwhispering item, skill and enchantment.
The Outward modding community allocates ranges of custom item ids so that no two mods ever ship the same number — a number that lands in your save file permanently the moment you pick the item up. Beastwhispering had been using a range it simply chose for itself, which nobody had granted it and which could have collided with any other mod you install. It now uses 87000-87999, the range allocated to it.
Every id changed, so the game no longer recognises the old ones. On an existing save you will lose:
- every Beastwhispering custom item — chow, recipe scrolls, blankets, pet sigils, Pearlbird feathers;
- every Beastwhispering skill you have learned (re-learn them from Maren — all ten are still in her training tree);
- the fletching enchantment on any arrows you have already fletched.
Your pets are not affected. Their bonds, loyalty, names and stats live in Beastwhispering's own save files, which store species and item names, never item ids. A bonded pet survives the update intact.
Starting a fresh character avoids the loss entirely. There is no migration: the old numbers were never ours to keep, and carrying them forward would have meant shipping a known collision risk into a public release.
Co-op
Host and guest should both run this build. The companion channel's version gate is exact, so a
mismatched pair still joins the Photon room but the ck channel never goes ready — pets desync
rather than the session being refused, and the log carries a handshake-mismatch warning. Matching
builds is the standing rule for co-op and this release does not change that.
Under the hood
- Ids are now recorded one-per-row in a checked-in ledger, generated from that single source into the code, the SideLoader packs and the item registry, and gate-checked in CI — including a scan that fails the build on any hand-typed id.
- A blanket added through the
Blankets.txtconfig drop now needs an explicitly allocated id and is refused with an explanatory message if it has none. Previously it silently invented one by hashing the blanket's name, which could land on another mod's item.