You are viewing a potentially older version of this package. View all versions.
CeruleanCutlass-DonorKit-0.1.9 icon

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.

Date uploaded 6 days ago
Version 0.1.9
Download link CeruleanCutlass-DonorKit-0.1.9.zip
Downloads 98
Dependency string CeruleanCutlass-DonorKit-0.1.9

This mod requires the following mods to function

BepInEx-BepInExPack_Outward-5.4.19 icon
BepInEx-BepInExPack_Outward

BepInEx pack for Outward.

Preferred version: 5.4.19
CeruleanCutlass-ForgeKit-0.4.13 icon
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.13
CeruleanCutlass-NetKit-0.2.10 icon
CeruleanCutlass-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.10

README

DonorKit

πŸ“– Full documentation: DonorKit wiki page

The donor-scene harvest engine β€” "get me a live body for species X, from anywhere in the game". Extracted from CompanionKit on 2026-07-26 (static-analysis wave, lane 5E) so SpawnKit no longer has to depend on the pet library for its creature supply.

Requires: BepInEx 5 (BepInExPack_Outward), ForgeKit and NetKit β€” both hard dependencies ([BepInDependency]), so BepInEx refuses to load DonorKit if either folder is missing from BepInEx/plugins/. NetKit is needed for the auto-warm room-left re-arm. No SideLoader; DonorKit builds on a clean runner.

What it owns

  • DonorHarvest β€” the guarded cycle: additive load β†’ freeze at sceneLoaded β†’ payload β†’ unload β†’ repair. Generic Func<Character, object> / Func<Scene, object> payloads, so one load can stock many species (light-probe cycles are the scarce resource).
  • DonorSceneTable (+ the embedded DonorScenes.txt) β€” the speciesβ†’scene roster, pins, identity resolution, and build-scene name matching. DonorScenes.txt is generated by bwspecies build; see tools/BwSpeciesTool/Paths.cs.
  • DonorCreatureFinder β€” exact-then-substring live-donor lookup within a loaded scene.
  • The guards, each of which exists because the un-guarded version broke a real session: DonorPhotonGuard (view-id collisions), DonorSaveGuard (mid-harvest saves, deferred via the game's own SetSaveRequired re-arm), DonorSingletonRestore (the four Instance-hijack restores), AmbienceGuard + DonorAudioRegistry (the persistent audio registries), SoulSpotGuard (duplicate-UID throw mid-Awake), DonorDisplayRepair (skybox / display chain), TerrainGuard (LOD render holes), DonorNetworkInitGuard (guest replica conversion).
  • SkeletonRig / RagdollRig β€” the rig surgery a dormant clone needs to render and ragdoll.
  • The expedition tier (lane 5E-2, 2026-07-26 β€” moved here from CompanionKit): ExpeditionHarvest (the vanilla-loader round-trip state machine for region/town scenes too large to load additively), ExpeditionOrchestrator (the expedition verb body, batch capture payload, in-place capture, once-per-launch boot auto-warm), ExpeditionManifest (ck_expeditions.txt β€” the historical filename is kept so existing installs keep their record), StorySense (the prologue/first-visit story-safety reads). Config: [Expedition] in cobalt.donorkit.cfg. The expedition/ templateclear/expeditionreset/templateprobe verbs stay on CONSUMER channels (ck_cmd.txt + BW twins) β€” templateclear's full reset must also re-arm CompanionKit's effigy gate, which only the consumer can reach.
  • The K2 template store (TemplateStore + BodyTemplateStore, lane 5E-2): ONE store with per-consumer KINDS. The companion-body kind (BodyTemplateStore) carries identity + captured stats, eager-prunes, refuses substitutes on identity lookups, keeps unhealthy rigs; the spawn kind (SpawnKit's SpawnTemplates layers on it) holds bare normalized GameObjects with opt-in LRU cap eviction and rejects unrepairable rigs. The kinds share only the FABRIC (holder/registry/recency/ orphan-proof clear) β€” unifying the policies would change behavior, so they stay at their call sites. E3 seams for third mods: TemplateStore.RegisterSpecies(key, scenes) (runtime donor-table row) and TemplateStore.RegisterTemplate(key, dormantBody) (parks a body in the spawn kind through SpawnKit's own normalization seam).

What it deliberately does NOT own

  • Puppet-shaped code (the 5E permanent-shim rule β€” a DonorKitβ†’CompanionKit edge is forbidden): BodyTemplateCache.PuppetFrom (template β†’ CompanionBody via BodyFactory.BuildPuppet), ClearAllAndForgetMisses (also resets the effigy harvest gate), and the stat capture itself β€” the store calls back through BodyTemplateStore.StatCapturer, which CompanionKit wires to AttributeCapture.From in its Awake.
  • The pure donor logic β€” DonorTable, DonorChain, RigSurgery, TerrainMaintenance, the Expedition* planners β€” which stays in core/CompanionKit.Core. DonorTable cannot be split from SpeciesTable/Species/Regions, and its file-mates SpeciesFloats/SpeciesYaw are consumed by Beastwhispering.Core. DonorKit references that game-ref-free COMPUTE assembly, never the CompanionKit plugin.

Dev verbs (BepInEx/config/DonorKit_cmd.txt)

photondump Β· audiodump Β· audioprune Β· terraindump Β· terrainfix Β· scenedump Β· selftest

Consumers also register the first five on their own channel via DonorVerbs.RegisterAll(registry).

Config

BepInEx/config/cobalt.donorkit.cfg, created on first launch. Two sections, eight keys, all shown here at their shipped defaults:

[Rig]
## Rebuild a dormant clone's skinned-bone bindings so it renders. Moved here from
## cobalt.companionkit.cfg along with SkeletonRig, its only reader.
RepairSkinnedBones = true

[Expedition]
## Run the capture payload in place when you enter an oversized scene that still has
## uncached species. OFF by default β€” it is the only path where the kit does heavy
## work nobody asked for.
CaptureOnSceneEntry = false
## One-time template warm at the first player-ready of a launch: needed | all | off.
AutoWarmAtBoot = needed
## Comma-separated species warmed regardless of the mode. Empty = the species installed
## consumer mods register in code; a non-empty list REPLACES those; "none" disables all warming.
AlwaysWarmSpecies =
## How long the boot warm may wait for a busy loader before giving up (0 = don't wait).
AutoWarmRetrySeconds = 60
## Safety net for the RETURN leg β€” re-request the trip home if no load has started.
ReturnRetrySeconds = 20
## Allow an expedition to start with guests connected. OFF: each leg ends in a per-machine
## continue gate nothing host-side can pass, and one AFK guest wedges the whole party.
AllowCoop = false
## Guest-side auto-pass for those gates while a host-run expedition is in flight. Inert
## otherwise, so it is safe to leave on.
GuestAutoContinue = true

Each generated entry carries its own # Default value: comment and a much longer description than the summaries above. BepInEx never migrates a changed default into an existing cfg β€” if behaviour doesn't match, compare against those comments before assuming a bug.

Expedition manifest β€” BepInEx/config/ck_expeditions.txt (the historical filename is kept so existing installs keep their record). ⚠ Known bug (docs/expedition-harvest-testplan.md V42, confirmed live 2026-07-25, open): the manifest reports captured scenes in memory but the file is never written, so the record does not survive a relaunch.

API sketch

[BepInDependency(DonorKit.Plugin.GUID, DonorKit.Plugin.VERSION)]   // VERSION = the min-version floor, see docs/wiki/kits/versioning.md

if (DonorHarvest.TryGetDonorScenes("Pearlbird", out var scenes, out var term))
    StartCoroutine(DonorHarvest.HarvestChain(scenes, term,
        use:      src => Object.Instantiate(src.gameObject, MyHolder().transform),
        onResult: r  => UseOrFailLoudly(r as GameObject)));

The payload runs while the donor is loaded and frozen, must be synchronous, and must not keep references into the donor scene. Use DonorHarvest.IdentityFor(key, src) rather than src.Name β€” a pinned row's frozen donor reports its serialized base-prefab name.

CHANGELOG

DonorKit changelog

0.1.9 β€” 2026-09-05

  • ForgeKit 0.4.13 / DonorKit 0.1.9 / SpawnKit 0.6.1: session-3 fixes
  • Merge branch 'worktree-agent-a92939b0a3b7c1d30' into integrate/session3-fixes
  • Merge branch 'worktree-agent-ae841fe8e982c8457' into integrate/session3-fixes
  • Fix release-blocking version skew: NetKit 0.2.10, Beastwhispering 0.2.17
  • DonorKit/ForgeKit: PRE-1/PRE-2 harvest resource census + liveness heartbeat
  • DonorKit: N1 review nits β€” prove the guard attached, record the real safety argument
  • DonorKit: N1 guard ItemManager duplicate item UIDs
  • DonorKit 0.1.8: MP-hardening sweep close-out (L3a)
  • Merge branch 'main' into worktree-agent-ab73ab8f3cf01f201
  • DonorKit: L3a harvest stopwatch splits queue waits from the actual load (waited=/loaded=)
  • packaging: raise ForgeKit dep pins to 0.4.12 and StoryKit to 0.1.8 (fresh-install floor)
  • Spawn gate abstraction: Lifecycle.IsGameplayLive, StoryKit SpawnPolicy + posture census, WalkSpeed default
  • Beastwhispering 0.2.9: pet evade (hyena back-hop dodge) as the evade species axis + ck.pet.cue MP cue

0.1.9 β€” 2026-09-04 (built; log-diff verification owed)

  • ItemUidGuard: a duplicate item UID no longer wedges all item initialization on a guest. Vanilla ItemManager.UpdateItemInitialization calls ProcessInit() and only then m_itemsToInitialize.RemoveAt(0) β€” so an item whose init throws is never dequeued and every item queued behind it is blocked for the rest of the process. Field evidence: 3,368 ArgumentException throws, every frame to end-of-log, on a co-op guest; zero on the host. The trigger is a donor-harvested clone colliding with a live scene NPC's UID, giving two Bags one UID + "_Content" key. We already guarded CharacterManager.m_characters and the soul-spot registry and had missed this third sibling β€” grep AddStaticItemUID returned zero hits repo-wide. The prefix keeps the existing registration, never throws, and warns once per UID. Safety argument recorded in the source: the losing Bag is deactivated by vanilla one method later regardless, so registering its container would be pointless and re-keying would orphan a container under a dead bag.
  • Boot assertion that the guards actually attached. A mismatched Harmony patch neither applies nor errors, and a guard whose success looks like silence must be able to prove it is installed. Covers the soul-spot prefix too.
  • [MEM] per-cycle resource census (PRE-1): mono heap, total allocated/reserved and an object-count leak proxy at each donor-harvest cycle boundary, self-timing with a 2-of-3 back-off, and omitting rather than fabricating a skipped probe. src/ previously had zero memory instrumentation, which blocked both the stall work and the donor-ceiling test.
  • Retest: spawnkit-testplan.md BUG-ITEMUID (boot half confirmed live).

0.1.8 β€” 2026-08-30 (built, not live-verified)

  • The harvest stopwatch stops charging queue time to the load (L3a). [HARVEST] donor scene loaded in … now reports the load phase only and carries an explicit waited=X.XXs loaded=Y.YYs split, where waited is the admission hold plus guard-window serialization; the "proceeding after Xs wait" line times only its own wait. done in Xs total is unchanged. Read every pre-2026-08-30 "loaded in" timing as an upper bound under contention, not as a load cost. Row L3A1 in docs/guest-mirror-harvest-testplan.md.
  • Dependency pins raised to the sweep floors (NetKit 0.2.8).

0.1.7 β€” 2026-08-28

  • Dependency pin hygiene: manifest.json/thunderstore.toml now correctly declare ForgeKit>=0.4.10 and NetKit>=0.2.5, matching what this build actually compiles against (part of a repo-wide sweep after the same drift left SpawnKit 0.5.0 unable to load on a fresh Thunderstore install).
  • Kit versioning contract: KitContract handshake, COMPAT_SINCE, BepInDependency VERSION floors, one-bundle alpha profiles.
  • SpawnKit warm mirror Phase A: publish + mirror every peer's mintable species (observe only); co-op gate on cold guest-mirror donor harvests.
  • Save-replay/harvest guard hardening wave (AF2/AF8 series): the late abandoned-donor unload runs inside its own guard window, a throwing deferred-save replay re-arms instead of dropping the save, a merely-queued save no longer refuses harvest admission, and the harvest reports its own abandon to the save-replay decision rather than leaving it to infer one.
  • Bundle-body tier follow-ups: templateclear spares prebuilt bundle bodies; bundlebody reload actually reloads; a [STATS] from-save breadcrumb.
  • … and several smaller review/hardening fixes (see git history).

0.1.5 β€” 2026-08-19

  • Ragdoll restore: judge by the donor's manager caches, never the clone's joint count

0.1.5 β€” 2026-08-19

  • Ragdoll restore: judge by the donor's manager caches, never the clone's joint count

0.1.4 β€” 2026-08-19

  • Fix V6b: the Quiet-tier exemption was inverted (ambient ungated, verbs gated)
  • A10 verb re-homing: photondump body -> NetKit.ViewRegistryDump; waiver docs; V28-V31
  • 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'
  • Bundle body tier: Pearlbird ships as an AssetBundle rig β€” expedition bypassed (B1/B2/B3a live-verified)

0.1.3 β€” 2026-08-11

  • 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
  • SpawnKit perf wave: hot-loop allocs removed, adaptive replica enforce, prune throttle, viewID watermark + opt-in corpse release, AI sleep radius, caps 8->12
  • SA-0808 Wave C code half: spawn recovery β€” census, ghost fix-at-cause, quest-gate clear, TerrainManager guard, abandon quarantine
  • SA-0808 Wave C data half: DonorScenes pin wave + sweep-doc corrections
  • Static-analysis 2026-08-08 wave 0: nine P1/P2 fixes
  • Review fixes: strip the whole VFXPlayed rebind family; pin only when it defends something
  • BodyFx one-shot: a pin owns the pose, so a refused DestroyImmediate can't move the flourish
  • W4: fixes for everything D1 found, plus the join-race P1
  • MP fix waves W1/W2/W3, and what Block A found when we ran them
  • Log levels: a per-mod [Diag] LogLevel, gating at the source
  • DonorKit: fake-null sweep in the guards, rig forensics and expedition trace
  • A1: the fake-null accessors, and every Plugin.cs cleared

0.1.2 β€” 2026-08-02

  • DonorKit/SpawnKit: name the two silent paths
  • DonorKit/SpawnKit: five static-analysis fixes
  • DonorKit/SpawnKit cleanup: one rig gate, one Safe() reader
  • Docs sweep: archive, condense, and validate the whole documentation tree

0.1.1 β€” 2026-07-30

  • DonorKit gets a painterly butterfly net; Disengage now reads "return and stay"