SpawnKit
[ALPHA] Runtime enemy spawner for Outward: spawn any donor-scene creature beside the player as a real vanilla hostile, with full AI, aggro, combat, and vanilla death/loot. Co-op supported. Early software — see README for known issues.SpawnKit changelog
0.6.2 — 2026-09-05
- Static-analysis wave 5: release prep for Beastwhispering 0.2.18 + DangerousRoads 0.1.13
- SpawnKit: materialize a clone's unspent StartingPouchItems weapons at mint (Case B upstream, measured)
- SpawnKit: MeleeSidearm review fixes — right-hand-only, verified swaps, lifetime cap, kill switch
- SpawnKit: MeleeSidearm — a bow-holder stuck in the melee AI state gets its sidearm forced
- SpawnKit: restamp WeaponType after equip and every animator Rebind
- ForgeKit 0.4.13 / DonorKit 0.1.9 / SpawnKit 0.6.1: session-3 fixes
- Merge branch 'worktree-agent-ad0ca166327fb8804' into integrate/session3-fixes
- Merge branch 'worktree-agent-a92939b0a3b7c1d30' into integrate/session3-fixes
- Fix release-blocking version skew: NetKit 0.2.10, Beastwhispering 0.2.17
- SpawnKit: N2 review fixes — mint gates need both authority terms
- DonorKit/ForgeKit: PRE-1/PRE-2 harvest resource census + liveness heartbeat
- SpawnKit: N2 authority reads use raw room-host, not flipped master
- SpawnKit 0.6.0 / DangerousRoads 0.1.10: pre-session follow-up close-out (B1 knobs, quiet-roads visibility, batched warm asks, mode-faithful draw condition)
- Merge branch 'main' into worktree-agent-a5a0898794ba1be15
- SpawnKit+docs: PR3-mode review nits — single mode read in CanEventuallyShowRoomWide; PR3-5 FAIL wording excludes the draw-to-spawn race
- SpawnKit: B1-nopark SpawnOptions.NeverFarPark, the event-owned mark
- SpawnKit+DangerousRoads: PR3-mode draw-time predicate mirrors the spawn gate per RoomWarmMode (dead-end sits out at defaults)
- SpawnKit 0.5.5: MP-hardening sweep close-out (L3b, B5, B8, B12, PR1, PR5, L3c, C2, C1)
- Merge branch 'main' into worktree-agent-a54a97473c5bb76f6
- SpawnKit: PR5 swap leg — the placeholder hot-swaps to the real body, strictly serialized
- … and 23 more (see git history)
0.6.2 — 2026-09-04 (live-tested on legion)
Two live-found P0s off the 2026-09-04 session.
WeaponStamp: spawned armed humanoids swing again. Every SpawnKit-spawned armed humanoid played the unarmed punch clip and dealt zero damage — Cobalt, live: "the bandit was doing the punch animation with the axe drawn and he did no damage to my pet", across multiple species. Outward has no animator override controllers: one humanoid controller picks punch-vs-weapon clip sets from a single animator intWeaponType, written ONLY byCharacter.WeaponChanged(), which is triple-guarded and silently no-ops on guard failure with no retry. Weapon damage ridesHitStartedanimation events on the ARMED clips, and the unarmed fist detector is assigned only whenCurrentWeapon == null— so weapon-bound + unarmed-clip means the blade sweep never arms AND the fist path is locked out. One cause, both symptoms, and every game-state field read correct becauseWeaponTypeis not game state.grep WeaponChanged src/SpawnKitwas zero hits while SpawnKit calledAnimator.Rebind()(which resets every parameter) in three places after equipping. We had already fixed this once — GhostPeer'sGhost.Restamp(GH19, same verbatim symptom); the knowledge never crossed kits. Now re-stamped after the equip and after every Rebind, with a deferral queue so an early stamp waits for the animator instead of being discarded. Boot assertion proves both DonorKit guards' prefixes actually attached — a mismatched Harmony patch neither applies nor errors. Live: punch clips 13 -> 0, armed attack clips 0 -> 13, beasts unregressed.MeleeSidearm: a bow-holder stuck in the melee AI state.AISCombatMelee.UpdateAttackCheckandTryStartAttackPatternare both gated onCurrentWeapon is MeleeWeapon; aProjectileWeaponis not one, so a bound bow in the melee state is frozen forever. Positioning, facing and dodge carry no weapon-class gate — which is exactly why three observed raiders evaded while never swinging, with the merchant's HP flat for minutes. Now detected and swapped to a right-hand melee sidearm, keyed on the observable end state rather than on why the bow is bound (the upstream — a failedEquipRequiredWeaponsswap on a clone — remains inferred and is instrumented, not assumed). Post-equip verification before an attempt is spent, a sticky lifetime cap so an equip tug-of-war cannot recur per episode, vanilla's ownInLocomotion && !IsCastingguard, and inactive candidates skipped (a bag deactivated by the ItemUidGuard leaves inactive weapons visible to the scan).MirrorAuthority(N2): a SplitScene authority flip madeGuestReplicasread the getter SplitScene postfixes and silently clear the host's warm wants; the host then retired the species for the session. Authority reads now use the raw room-host — EXCEPT the two mint gates, which require the conjunction (raw says guest AND vanilla will convert the replica), because raw alone there would mint a live AI copy of the master's enemy on its own viewID.- New:
[Spawner] MeleeSidearmFix(default true, live) gates both the sidearm poll and the mint-time pouch re-registration.spawndumpgainswpnType= offHandType= wpnClass= pouchWpn= pouchReg= sidearm= swapState= tries= lifeswaps=—wpnTypeis the field that had never been read and which root-caused the punch bug. - Retests:
spawnkit-testplan.md(BUG-HUMANOIDPUNCH PASS, CB1-CB8),guest-mirror-harvest-testplan.md.
0.6.0 — 2026-08-30 (built, not live-verified)
The pre-session follow-up wave: three additive surface pieces (hence the MINOR bump).
SpawnOptions.NeverFarPark, surfaced on the handle (public setter — ownership is a phase; the owning event releases the mark in Teardown and on the torn-down early-outs): an event can mark a creature it owns so a consumer's own distance-based park pass leaves it alone. SpawnKit does not park anything itself — it carries the flag for consumers that do (DangerousRoads sets it on patrol troglodytes and stranger mobs; a parked member counted as neither alive nor dead, which is how a patrol could reportalive=0 dead=0). Retest:dr-farcache-testplan.mdFC15.Spawner.CanEventuallyShowRoomWide(key)+ pureSpeciesRoomPolicy.CanEventuallyShow: the draw-time question ("could a spawn ever get past the spawn gate under the operator's RoomWarmMode?") as distinct fromCanMintRoomWide's "cleanly, now". Under the defaultRoomDegradedit refuses only a TERMINAL dead end — exactly what the B12 spawn gate refuses — so a card can no longer be drawn eligible and then refused at spawn. Retest:dangerous-roads-testplan.mdPR3-5.- Batched warm asks bind
RoomWarmRequestCaponce. The cap is per-peer PER CALL, and DR's old per-key loops multiplied it (11 asks past a cap of 3); DR now sends one list per refusal/tick. The truncated tail moves up once the head resolves on that peer. Retest:guest-mirror-harvest-testplan.mdPR2-BATCH1.
0.5.5 — 2026-08-30 (built, not live-verified)
The co-op guest-mirror half of the MP-hardening sweep: degrade, never abort. A guest that cannot yet show a species gets something harmless to stand in for it and a route back to the real body, instead of a refusal that either wedges or deletes.
- The destroyed-cache re-harvest is capped, and the mint says where the body really came from (L3b).
The guest mirror's donor-chain cap was passed as
wasCached ? 0 : cap, and 0 means UNCAPPED inDonorHarvest— so precisely the case that needed the cap most (a cached template destroyed under us, forcing a re-harvest) ran without one. The cap now always applies, and thesrc=label on the mint is decided from the donor cycles actually spent: a re-harvest reports the newcache-lost-reharvestinstead of falsely claimingsrc=cache. Policy inCore.MirrorAcquirePolicy; rowGM29. - An
sk.goneinside the mint dwell no longer fires a bogussk.fail(mint-inactive)(B5). The 2-frame dwell that lets a freshly minted replica settle would report the mint inactive when the master had simply retired the spawn mid-dwell — the very reason this machine had removed the row itself. The resume now checks the registry row first and ends silently in that case (Core.MintResumePolicy). The remaining inactive-within-dwell warnings (GuestReplicas and both EnemySpawner siblings) now state what was actually observed and offer the duplicate-UID guard as a checkable hypothesis rather than a verdict. RowGM30. Audited alongside it:SpawnNet.OnFailcarries no retry or blacklist keyed on failure reason, so the field harm here was log noise and mirror-ledger pollution, never a double-despawn. - A kept cold-unsafe spawn records its re-flush obligation (B8).
SpawnNet.OnFailnow writes the Forgiven-ledger re-flush obligation for EVERY cold-unsafe report that leaves the spawn Alive — degraded and forced spawns included — paired with aRequestRoomWarm.RoomGateOkno longer gates the plumbing, only the forgiveness verdict. The despawn decision is untouched by this change. RowGM31. - The warm-request deadlock: terminal dead ends refuse, and guests replay their own refused spawns
(B12).
RoomDegradedno longer spawns past a TERMINALDeadEndOnPeer— the degraded branch readswarm.ReasonthroughSpeciesRoomPolicy.IsDeadEnd(the verdict is downgraded by design, so the reason is what still carries the truth) and refuses both at preflight and at the broadcast-time re-check. And the new bounded per-uidCore.ColdPendingBook(16 rows) keeps a cold refusal's (uid, species, wire message), so the species' warm completion replays the guest's OWN spawns into the queue with no dependence on the host's Forgiven ledger. A gone forgets its pending uid, a room change clears the book, andIsKnowncounts pending rows so the host's re-flush dedups against them. RowsGM32/GM33. cold-busy— a refusal that means "not right now", not "never" (PR1). New benign wire reason coveringRefuseCombat+RefuseBudget+RefuseCeiling(Core.MirrorFailPolicy.ColdRefusalReason, unit-pinned);RefuseAdmissionand anything unknown stay cold-unsafe and destructive. The master's benign leg records the Forgiven obligation and requests the warm for a cold-busy report. InShouldDespawnColdUnsafethe!roomGateOkclause is GONE: it is now one grace per (uid, actor) for every spawn, degraded included — degraded is precisely the mode that expects cold guests. The 27-line Phase-B rationale beside it was rewritten in the same change to carry that ratified override and to say why once-only still stands; theroomGateOkparameter is retained for the log line and call-site compatibility. RowsGM34/GM35.- Placeholder bodies: a cold refusal mints a stand-in bound to the leased viewID (PR5, degrade leg).
New
[Coop] PlaceholderBodies(default true). Instead of nothing standing where the master says a creature is, the guest mints a CompanionKit ghost stand-in on the leased viewID (Views.Neutralize→pv.viewID = m.ViewId, group restored to 0 so it receives), with the AI stripped inside the INACTIVE window —EditorCharacterAILoadAIdestroyed beforeIsAI's first latching read — aSKPH_UID (soSK_stays free for the swap), the master's faction stamped after activation (ChangeFactionsync:false), andsk.ack(Source="placeholder"). It is harmless by construction:CensusAndEnforceearly-returns on Placeholder rows soEnsureEquippednever fires, and it is NOT Invincible. Requires CompanionKit 0.4.29 (theGhostStandInaccess widening). RowGM36. - …and it hot-swaps to the real body, strictly serialized (PR5, swap leg).
Core.PlaceholderSwapis the pure sequencer (Standing / Swapping / Retired, with aDeferGonelatch where Removed outranks Died; the exhaustive table and every length-4 sequence are pinned inPlaceholderSwapTests). The executorGuestReplicas.SwapRoutineis yield-free and its order is strict — identity-checked deregister →Views.Neutralize→ destroy the placeholder →Mint(template, m, "swap")→ row takeover — because a same-UID newcomer deactivates its predecessor andIsAIlatches on first read. The fresh row keepsPlaceholder=truethrough its 2-frame dwell so a mid-dwell gone routes through the machine; completion drops the flag, acksSource="swap"and runs the normal census/visual tail. A template evicted before the destroy rewinds to Standing and re-wants the species, so the swap is retriable. RowGM37. - The
cold-busyand keep-configured deferrals are BOUNDED. Worth saying out loud, because it deliberately overrides an operator setting: the Forgiven expiry window now bounds — and after ~60 s despawns — spawns kept underDespawnOnMirrorFailure=falseas well as cold-busy deferrals whose warm never lands. The ledger backstop wins over "keep" on purpose, to bound the B10 ghost/scan hazard. Related, underDespawnOnMirrorFailure=falseplus LRU thrash (a non-default combination): a warm→evict→re-flush→refuse ping-pong can cycle for as long as the peer's row keeps genuinely changing. It is paced by real warm completions rather than by a timer; noted beside B11's cache-coupling doc. - Guest scene-batch harvest: k species out of ONE additive load (L3c — the resolution of B12's budget
deadlock). A master-instructed warm on a guest now carries every other wanted-cold species that shares
a donor scene with the one being warmed, charged as ONE donor cycle: the LightProbes crash resource
degrades per load/unload, not per species, and billing needed no change because
WarmRoutinealready chargesDonorHarvest.CyclesThisSessiondeltas. The DangerousRoads patrol roster (~11 species across ~3 scene families) now fits the shippedMirrorHarvestBudget=4, unit-pinned. Batch bound (the B11 coupling): companions per batch = min(wanted, free template-cache slots after the primary,SceneBatchPlan.DefaultMaxBatchSpecies= 12 including the primary) — never build a template the LRU evicts in the same Insert. The serial warm latch, the safety re-checks and per-species failure isolation are all unchanged, and a companion that is not live in the settled scene keeps its own queue entry and chain. New pure policycore/SpawnKit.Core/SceneBatchPlan.cs+SceneBatchPlanTests. RowsL3C1–L3C3. - ADDITIVE:
SpawnTemplates.AcquireScenegains anallowNonMasteroverload mirroringAcquire's, honored for master-instructed warms and carrying the same attribution log; the 2-argument form is unchanged.COMPAT_SINCEstays 0.5.0. skwarmdumptells a quiet peer from a frozen one (C2).WarmMirrortracks a HOST-LOCALlastHeardAtper actor, updated on ANY message received whether or not it decodes, usingTime.unscaledTimeon this machine — never a wire field.PeerRow.Atonly advances on a message we could actually READ, so a peer sending garbled bytes every tick used to read as "frozen" by age alone. The dump now printslanded=besideheard=for Participating peers and a last-heard age forUnmodded/HelloedNoRowrows too; the table is cleared on a room change, because actor ids restart and a stale timestamp would name a stranger. RowC2-LASTHEARDindocs/guest-mirror-harvest-testplan.md.- Comment-only (C1): the warm-set heartbeat's loss-tolerance note now names the real reason it exists — peer-state transitions (late join, resync, room change, a peer not yet scene-ready), not transport loss. PUN RPCs are reliable, ordered and deduped; loss means disconnection. The heartbeat stays.
- Comment-only (L3c review follow-up): the batch trigger comment states the true condition — mirror mints batch too, not only the harvest path.
- Dependency pins raised to the sweep floors (CompanionKit 0.4.29, DonorKit 0.1.8, NetKit 0.2.8).
0.5.4 — 2026-08-29 (built, not live-verified)
- Preflight refuses while the world is not live —
FailReason.GameplayNotLive(14, appended;COMPAT_SINCEstays 0.5.0). ReadsForgeKit.Lifecycle.IsGameplayLive(): a level load in flight OR the loader still holding gameplay paused after it (the "press any key" window), where a spawned body sits inCharacterAI.Update's paused branch and comes up T-posed with an inactiveAIStatesRoot(education/who-poses-the-body.md#8). Distinct fromNoPlayer— the player exists in that window.DescribeAiLiveness's ownpausedreadback is unchanged (it is a readback on an existing body, not a gate).
0.5.3 — 2026-08-29
postrace <name|uid:xxx> [seconds=20] [hz=5]dev verb (PosTrace.cs): the BUG-SPAWNLAUNCH discriminator. Coroutine-sampled[POSTRACE]line per tick on one tracked spawn —y,grounded=<cc.isGrounded>,falling=<m_falling>,totem=<m_charTotemMove>,agent=<on|off>/onMesh/next.y,ragdoll,clip, andoverlaps=every collider inside its CharacterController (self excluded, layer named). Stops on despawn/death/timeout orpostrace off. Requires CompanionKit 0.4.23 (pin only).
0.5.2 — 2026-08-28
- Fix stale dependency pins across the fleet; DonorKit 0.1.7
- release prep: sync version numbers across all four files
- Cross-area pet orphan fix + three combat-staging diagnostics
- Merge feat/guest-voluntary-warm: guest self-warm opt-in + DR late-join re-push
- SpawnKit: guest voluntary warm — review fixes (M1/L1/M2/L2)
- SpawnKit 0.5.1: review nits — MintHeal.Step/SettleState shared by coroutine + replay test, max==base warning, startHp on the settle exit line
- SpawnKit + DR: guest voluntary warm (GM22–GM28, built, not verified)
- SpawnKit 0.5.1: co-op mint heal — refresh the max stat + settle watch (MP-HALFHP)
- F2-V2: raw-gate the PLAN half of the want duty, the forgiven re-flush and skresync
- F2-V1: stamp the guest warm routine with its room, so a stale one cannot free the latch
- AF2-13: additive LruView instead of retyping LruKeys (review CHANGES, ABI removal)
- AF3-2: gate SpawnKit's cross-peer host duties on RAW authority, not the flipped getter
- AF2-13: TemplateStore.LruKeys hands out a read-only view of the live LRU order
- AF2-9: cap the species count a warm row may decode, and refuse an over-long one
- AF2-8: say "sent this sweep" — the count is the whole want book, not my species
- AF2-7: the warm mirror stops mirroring its own row
- AF2-11: an abandoned template acquire still charges the donor cycles it burned
- AF2-10: reset the guest's harvest standing above OnRoomChanged's early-out
- AF2-5: the sk.want handler logs what actually happened, not "queued at head"
- AF2-3: an abandoned (actor, species) pair neither plans nor spends the per-peer cap
- … and 2 more (see git history)
aiLive=on the[SPAWN]mint line and everyspawndumprow — the one field that answers "can this body engage on its own?". Verdicts, most-global first:NoAiGraph,WorldPaused,AiBehaviourDisabled,NotStartInitialized,DistanceCulled,Ticking; anything butTickingprints its own remedy sentence. Classification is pure and unit-tested (core/SpawnKit.Core/AiLiveness.cs,tests/SpawnKit.Tests/AiLivenessTests.cs).charAIDisable=is documented at the call site as PRESENCE, not state. A headless session readcharAIDisable=Trueas "SpawnKit disabled the clone's AI" and concluded, for a whole stretch, that spawned wilds can never engage.CharAIDisableis a DISTANCE CULLER (CharAIDisable. AICloseToPlayerCheck) that only ever acts when no player sits insideDistanceToEnable; it says nothing about liveness. The field key and value are unchanged (grep contract) —aiLive=is the additive answer.- Why it matters: the decisive term is GLOBAL.
NetworkLevelLoader.IsGameplayPausedgatesAICEnemyDetection.Update,AISWander.UpdateandCharAIDisable.AICloseToPlayerCheckalike, so a wedged world-sim pause (which aloadsavecan land in, with every dev verb still answering normally) freezes VANILLA scene creatures exactly as hard as SpawnKit clones.aiLive=WorldPausednow names it and points atunstick fix. - Additive only —
COMPAT_SINCEstays0.5.0.
0.5.1 — 2026-08-24
- Co-op mint heal: spawns in a 2+ player room no longer arrive at half health.
ApplyCoopStatsthrows on a fresh clone before vanilla's ratio heal, and the host read a stale max stat; the mint heal now forcesRefreshVitalMaxStatfirst and runs a ≤1 s settle watch that carries the max delta onto current health (damage taken inside the window is kept). Host-only; solo path unchanged (SpawnKit.Core.MintHeal) - New
[SPAWN] mint-heal …/mint-heal settle …log lines; the mint header andspawndumpprinthp=cur/max base=B - Guest voluntary warm: a guest may Prewarm for itself (menu button /
spawnprewarm) under[Coop] GuestPrewarm; the ask rides the existing safe want queue (out of combat, loader idle) on its own per-room bucket (GuestPrewarmBudget) under a per-session ceiling (GuestDonorCycleCeiling);Core.GuestWarmPolicy+ class-taggedGuestHarvestStanding(built, not live-verified — GM22–GM28) - Published warm-row budget is now
min(mirror-left, ceiling-left)(WarmSetWire.PublishedBudget); the voluntary bucket stays local; no wire change skwarmdump/spawndumpprint both buckets, the session ceiling and the class-tagged queue- New public seam
Spawner.OnPeerSceneReady(master-only) — DangerousRoads re-pushes its room-warm list for late joiners
0.5.0 — 2026-08-22
- Room-wide warm mirror: host-side WantBook ledger, PeerOwned mirror of every peer's mintable species, host gate on party-wide mintability (Phase A observe + Phase B enforce)
- Wire-protocol bump:
sk helloannounces 0.5.0; the compare is exact-ordinal, so a mixed-build room refuses co-op loudly (same-bundle rule) - Version reconciliation: Plugin.cs already announced 0.5.0 since the warm-mirror wire bump; manifest.json / thunderstore.toml / csproj now agree
0.4.4 — 2026-08-19
- SpawnKit: casters attack — BUG-CASTERNOSKILL skill-knowledge repair (live-verified)
- Merge branch 'fix/sa-0815-mpnet' into feature/pet-self-feed
- Review fixes for the SA lane-F3 MP/net wave (H1, M1, M2, L1-L3)
- SpawnKit: review fixes — refusal memo must not shadow a later-valid template
- SA 2026-08-15 lane F3: MP/net teardown, mirror-race + hello-warn fixes
- SpawnKit: TemplateAiGate cluster fixes (SA-0815 F2b, A4-1/2/3/5/6/7/8 + A9-4)
- SpawnKit: refuse brainless templates — BUG-PREBUILTADOPT structural AI-graph gate
- SpawnKit: fix Character.Start never running on far-mint spawns
- spawn <TAB> completes the species roster — SpawnKit annotates spawn/spawnex/spawnprep; explicit choices win in the engine
- 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
0.4.3 — 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
- Merge BUG-CONTROLLERCLOSE fix: controller B via vanilla ControlsInput.MenuCancelSystem() ORed into the same two-stage Esc gate (CI-safe, no Rewired_Core ref; retest owed)
- Merge Ghost root-scale fix (GV-SCALE1): donor's authored (0.02,1,0.02) root localScale rode Instantiate into every mint; RootScaleFix restores degenerate axes at mint time, SK_-uid + UseLegacyVisual=false scoped; retest GV-SCALE1b owed
- BUG-CONTROLLERCLOSE: controller B closes the SpawnKit menu via the same two-stage gate as Esc
- Invisible-Ghost fix: un-crush the donor's root localScale at mint (GV-SCALE1b)
- DangerousRoads relay whitelist gap + ghostdiag DEGENERATE threshold widen
- Fable-review fixes: viewID watermark reads outstanding ids (latched), corpse-release fallback never parks a neutralized view, FarCache InFlight can't leak, visual-pass retries count as busy, Notify header placement
- SpawnKit perf wave: hot-loop allocs removed, adaptive replica enforce, prune throttle, viewID watermark + opt-in corpse release, AI sleep radius, caps 8->12
- SpawnMenu search bar: pure SpeciesFilter (key + resolved donor name), per-frame latched row set, two-stage Esc
- Slope-tilt review wave: pure smoother + hysteresis deadband + per-species axis
- SA-0808 Wave C code half: spawn recovery — census, ghost fix-at-cause, quest-gate clear, TerrainManager guard, abandon quarantine
- Static-analysis 2026-08-08 wave 0: nine P1/P2 fixes
- Merge branch 'feat/pet-health-orb'
- Fix BUG-RAGDOLLJOINTLOSS: the rig re-init pass ate healthy ragdoll joints
- Pet health orb: replace the IMGUI bar with a green Outward-style vitals circle
- Fix BUG-GHOSTINERTSPAWN: re-run the init chain Character.Start already spent
- Merge Lane B: SpawnKit view lifecycle migrated into NetKit.ViewLease (phase 2)
- Phase-2 view-lease migration: SpawnKit's view lifecycle moves into NetKit
- Review fixes: strip the whole VFXPlayed rebind family; pin only when it defends something
- … and 8 more (see git history)
Unreleased
-
Refactor (behavior-preserving; built, NOT live-verified): the PhotonView LIFECYCLE (mint/bind/deferred release/age-out sweep/corpse mute) migrated into
NetKit.ViewLease(phase 2 of the view-hygiene migration).MintNormalizeandEnemySpawnerare thin delegations now; every[SPAWN]log line is byte-stable — though the lease-mechanics lines (deferred/released/aged-out) now carry theNetKitBepInEx source label and follow NetKit's[Diag] LogLevelrather than SpawnKit's (defaults identical; warnings un-gated on both) — and SpawnKit keeps its policy — the master-warn vs mirror-throw no-view asymmetry, the Vanilla-corpse may-persist declaration, and the corpse-worded once-per-entry aged-out notices (supplied to the lease as callbacks). The parked-release ledger + the 300s age-out rule gained their first unit tests (NetKit.Tests/ViewLeaseTests). Template normalization deliberately keeps its bareviewID=0(a dormant template must stay mintable — see the new warning note inSpawnTemplates.Normalize). -
DangerousRoads 0.1.1 + SpawnKit 0.4.2: re-pin off CompanionKit 0.4.2
-
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.4.1 — 2026-08-02
- Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule
