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

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).

Date uploaded 6 days ago
Version 0.4.10
Download link CeruleanCutlass-ForgeKit-0.4.10.zip
Downloads 100
Dependency string CeruleanCutlass-ForgeKit-0.4.10

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

README

ForgeKit

πŸ“– Full documentation: ForgeKit wiki page

Dependency-free dev-tooling library for people writing BepInEx 5 mods for Outward: Definitive Edition. It doesn't add anything a player sees on its own β€” it's the plumbing several other mods (CompanionKit, SpawnKit, and more) build on, and other modders are welcome to build on it too.

Requires: BepInEx 5 (Outward's Mono branch β€” see Compatibility below). No SideLoader, no Harmony, no other mod dependency.

What's in it

Piece What it gives you
CommandChannel + CommandRegistry A file-driven dev command loop: write a verb into BepInEx/config/<yourmod>_cmd.txt, it runs on the next poll (unscaled time, so it still fires while the game is paused). Unknown verb or help lists everything registered. It runs on unscaled time, so it still fires while the game is paused.
SelfTestHarness The [SELFTEST] BEGIN / PASS: / FAIL: / SKIP: / DONE pass= fail= skip= report shape β€” wire up Check(name, condition) / CheckIf(canRun, …) calls and get one consistent, greppable self-test report (skips are unrunnable-here, not failures).
Notify An on-screen info toast for the player, mirrored to the log.
Lifecycle WhenPlayerReady β€” a coroutine that waits past the game's void/staging coordinates before your mod starts touching the player, plus IsSanePosition for the same check inline.
TableLoader<T> / EmbeddedRes Embedded-default-plus-config-override text/texture tables: ship a sane default inside your DLL, let players override it by dropping a file in BepInEx/config/.
CommonVerbs The shared dev-verb pack β€” one CommonVerbs.RegisterAll(registry, Log, opts) call and ~36 verbs answer on your channel. See the table below.
ScriptRunner script / scriptcancel / scriptstatus β€” run a file of verbs as a sequence. Auto-registered into every channel.
Keybinds The cross-mod keybind registry. A mod cannot see another mod's config, so this is the only place a key collision is knowable β€” Claim your keys at boot and a clash gets reported instead of firing both mods.
IdPool The generated id constants for this workspace's allocated range (see docs/id-pool.md). Never type a numeric id.
Suggest / StatusApplyGate Nearest-match hints for a mistyped verb / a guard around status-effect application.
ScriptedBody / ScriptedBodies "This Character belongs to a script β€” do not adopt it." Stamp a body you spawn and drive with ScriptedBodies.Mark(go, "YourMod", "why"); any mod that goes looking for a body to clone/recruit/tame checks ScriptedBodies.IsScripted(character) and skips it, with Describe for the refusal log. It lives here because ForgeKit is the one assembly everybody already depends on, so the stamper and the reader never need a dependency on each other.

The shared verb pack (CommonVerbs)

CommonVerbs.RegisterAll(registry, Log, opts) in your RegisterVerbs gives your mod's own <Mod>_cmd.txt this set β€” no new channel, no relay change, and registries are per-mod so there's no collision (CommandRegistry warns on a twice-registered verb). opts excludes any your mod supersedes.

Domain Verbs
Give give Β· drop Β· useitem Β· givewater Β· equip Β· unequip Β· givemoney
Stage teleport (raw height) Β· walkto Β· standoff (ground-safe) Β· goto Β· moveto Β· face Β· pos Β· settime
Player sethp Β· combatclear Β· killnearest Β· swing Β· lockon Β· lockoff
Skills learnskill Β· unlearnskill Β· resetcooldowns Β· castspell
Status grantstatus Β· removestatus Β· statusdump
Diag scenedump Β· skydump Β· groundprobe Β· combatmgrdump Β· keybinds Β· ragdolldump Β· psdump Β· containerdump Β· containerroll Β· reloadcfg

Log tags are a byte-stable grep contract β€” don't rename them.

Installing (for players)

Drop the ForgeKit folder into BepInEx/plugins/. It has no effect by itself β€” install it because another mod you're using declares it as a dependency.

Using it (for modders)

<!-- your .csproj -->
<ProjectReference Include="path\to\ForgeKit\ForgeKit.csproj" Private="false" />
[BepInPlugin(GUID, NAME, VERSION)]
[BepInDependency(ForgeKit.Plugin.GUID)]
public class Plugin : BaseUnityPlugin
{
    private CommandRegistry _commands;
    private CommandChannel _channel;

    void Awake()
    {
        _commands = new CommandRegistry(Logger);
        _commands.Register("hello", "hello β€” logs a greeting.", args => Logger.LogMessage("Hi!"));
        _channel = new CommandChannel("YourMod_cmd.txt", Logger, _commands);
    }

    void Update() => _channel.Tick();
}

Private="false" matters β€” it stops MSBuild from copying a second ForgeKit.dll into your mod's own output folder. The kit ships from its own BepInEx/plugins/ForgeKit/ folder; your mod just references it and declares the dependency so BepInEx loads it first.

Configuration

ForgeKit has no configuration file of its own β€” it provides the command channel and config-table helpers that consuming mods use. The dev command loop reads a per-consumer file at BepInEx/config/<Mod>_cmd.txt (e.g. BepInEx/config/YourMod_cmd.txt), and TableLoader<T> lets a consumer ship an embedded default table overridable by a file the player drops in BepInEx/config/. Any settings live in the consuming mod's own cobalt.<name>.cfg, not here.

Compatibility

Outward must be on its Mono Steam beta branch, not the default IL2CPP build β€” like every BepInEx 5 mod for this game. If your game runs but no BepInEx mods load and there's no crash log, this is almost always why (Properties β†’ Betas β†’ select mono in Steam).

Mixed kit builds. ForgeKit runs the kit-version handshake at boot (KitContract): every mod reports which kit version it was compiled against, ForgeKit compares that with what is running and logs one [CONTRACT] line per pair, plus a [STAMP] census of every cobalt.* plugin's build. A VERSION SKEW line (and an on-screen notice) means a mod and a kit came from different releases β€” reinstall everything from one bundle/release. Full model: docs/wiki/kits/versioning.md.

License

Apache License 2.0 β€” see LICENSE in the repository root. You may use, modify, and redistribute this kit (including in commercial mods) provided you keep the copyright/license notice; see the license text for the full terms.

CHANGELOG

ForgeKit changelog

0.4.10 β€” 2026-08-28

  • ScriptedBody / ScriptedBodies: "this Character belongs to a script β€” do not adopt it". A one-field marker plus Mark / IsScripted / Describe. Purely additive, so COMPAT_SINCE stays at 0.4.4.

    It lives in ForgeKit for one reason: ForgeKit is the assembly every mod already depends on, so the stamper and the reader cost no new dependency edge and never have to know about each other. StoryKit stamps every NPC body it rigs; Hireling's recruit/re-form scan honours it.

    The bug it closes (live, legion, single-player): Cobalt's saved Hireling follower was named Travelling Merchant, and Hireling's re-form rung matches a live scene Character by DISPLAY NAME. The moment DangerousRoads' road-merchant event spawned a StoryKit NPC of exactly that name, the re-form cloned it into the follower puppet. What Cobalt then had glued to him was a merchant- dressed body with no dialogue prompt (a puppet's dialogue/Merchant machinery is stripped by design), no lock-on name (a puppet has no Character component at all), invisible to every roadsmerchant / roadsstatus / NpcRegistry census β€” and it outlived the real merchant's despawn by ten minutes, because it was cloned before it. Every symptom read as "the road merchant is broken"; none of them was.

    Wider than the merchant, on purpose. StoryKit stamps EVERY NPC it spawns, so this makes every StoryKit NPC β€” Maren, the trainers, both DangerousRoads road events β€” permanently un-recruitable as a Hireling follower, not just the one that caused the bug. A scripted quest/vendor NPC should never have been adoptable; the refusal is now explicit and logged rather than accidental. A follower recruited from a StoryKit NPC on an older build will no longer re-form.

0.4.9 β€” 2026-08-25

  • The world-pause wedge, named: it is the ProloguePanel, and unstick fix now presses the key it is waiting for. A long live session lost at least three results to a session whose world sim was pinned while every dev verb answered normally (the command channel polls Time.unscaledTime by design). It is intermittent and self-clearing, not a permanent latch β€” a later same-moment reading showed paused=False pausedBy=[] with the pet sim's ticks= climbing and pausedTicks= frozen β€” but a bad window ran past a minute (six consecutive polls reading paused=True), which is long enough to invalidate every timed observation taken inside it.

    Why recovery took so long, which is the practically important half. The ladder applies ONE rung per invocation, deliberately. On THIS wedge rung 1 (gate) is a no-op β€” it sets a latch the coroutine has not reached β€” so unstick fix had to walk down to rung 4 (forceunpause), which clears m_gameplayPausedBy wholesale (the Prologue key with it) and so releases the WORLD SIM even though the load coroutine stays parked. At roughly one invocation per poll that is the observed t+0 … t+61s staircase, and it is why the latency varied with how many rungs a given attempt got through. With rung 0 in front, the first unstick fix should be the one that works.

    The mechanism, from the decompile:

    • ProloguePanel.Show pushes PauseGameplay("Prologue") (ProloguePanel.cs:27). It is released ONLY by ProloguePanel.OnHide β†’ UnPauseGameplay("Prologue") (:63-70), reached only when GoToNextPage walks past the last screen (:34-60). The sole vanilla caller of MenuManager.GoToNextProloguePage is LocalCharacterControl.cs:185-189, behind ControlsInput.QuickDialogueUp β€” a real keypress on an OS-focused window. Unfocused or headless, that press never happens.
    • NetworkLevelLoader.FinishLoadLevel parks on while (MenuManager.Instance.IsProloguePanelDisplayed) (:1523-1526), which sits BEFORE the while (!m_continueAfterLoading) gate (:1538) and before the SendReadyToContinue RPC (:1541). So readyIds stays empty, AllPlayerReadyToContinue stays false, and UnPauseGameplay("Loading") (:1581) is never reached β€” which is why the pause stack read [Loading,Prologue] with allDone=True allReady=False.
    • By that point m_gameplayLoading (:1461), m_loadingLevel (:1215) and m_waitingForOtherPlayers (:1490) have all been cleared, so IsOverallLoadingDone reads TRUE while the sim is pinned. That is the whole "the load is entirely finished yet the world is paused" shape, and it is why the [LOADGATE] classifier answered done about it.
    • The gate rung was therefore a no-op on this wedge: it set a latch the coroutine had not reached. It was not being "overwritten" β€” m_continueAfterLoading has exactly two writers, SetContinueAfterLoading (:970) and BaseLoadLevel (:870), and every observed revert to False followed a fresh load.

    What changed:

    • New rung 0, prologue, tried FIRST by unstick fix: call MenuManager.GoToNextProloguePage() until the panel hides (bounded at 64 pages), then re-assert up to twice more in case a further context screen was queued behind the first. This is the cure, not a workaround β€” it is the same call vanilla's keypress makes.
    • LoadSnapshot gained ProloguePanelUp; LoadPhase gained the prologue phase, classified ABOVE done (an IsOverallLoadingDone snapshot is not proof the sim is running); and GateGuardHolds gained !ProloguePanelUp, so neither the watchdog nor the gate rung can claim "passed the continue gate β€” no keypress needed" about a coroutine parked earlier.
    • The auto ladder's ORDER is now pure and unit-tested (DevUnstick.ChooseAuto), including the property that Auto can never reach forceready.
    • A rung no longer grades itself. The same-frame re-dump can be stale β€” the loader settles its pause state in NetworkLevelLoader.Update β†’ UpdateGameplayPaused (:352, :382-406), which is exactly why applied forceunpause was followed by paused=True otherPlayerPaused=True and then by False on the next poll. Every applied rung now schedules a deferred [UNSTICK] verify step=… pausedBefore=… pausedNow=… β†’ HELD | REVERTED | NO-CHANGE line 0.75 s (realtime) later, and re-dumps under it. NO-CHANGE exists so a rung applied to a session that was never paused cannot read as a repair.
    • The dump gained [UNSTICK] prologue: panelUp=… and [LOADGATE]'s gate line gained prologuePanel=… β€” the field whose absence made the wedge unreadable.
  • On the otherPlayerPaused=True with peers=0 lead. Real, and explained: when the pause stack goes 0β†’1, OnReceivePauseGameplay (:1976-1999) broadcasts SendPauseStatus(true), which in an offline room lands locally and puts the LOCAL player's own id into m_playerCurrentlyInPause; UpdateGameplayPaused (:401) then reports it as m_otherPlayerPaused. It clears only when the stack drains to 0 (:2009-2017) β€” impossible while Prologue is pinned. It is a SYMPTOM of the stuck stack, not a second cause, and it is not player-facing: a real peer's id is removed by OnPhotonPlayerDisconnected (:1797-1802), and in a normal session the stack always drains. The forceunpause rung's raw Clear() does bypass the SendPauseStatus(false) broadcast, which is why the phantom reappears on the next pause push β€” documented, unchanged, and now visible in the deferred verify line.

0.4.8 β€” 2026-08-25

  • swing now NAMES its refusal, and waits for the gate instead of refusing instantly. A live headless session lost five attacks in a row to a state line that read as if nothing were wrong: weapon='Brand' type=Sword_1H attackOnRelease=False sheathed=False inLocomotion=False blocking=False stamina=100/100 followed by AttackInput(0,0) -> False. (preconditions failed β€” see state line…). The line printed the deciding fact and gave no polarity, so the deciding fact read as an idle detail. Vanilla's gate (Character.AttackInput, Character.cs:5749) is m_inLocomotion && m_nextIsLocomotion && !Blocking && !LocomotionAction && !Sheathing && !InChargeCancelCooldown && !m_cancelChargingSent β€” so inLocomotion=False ALONE refuses every attack. And "locomotion" is not movement: it is the animator's Locomotion TAG (the neutral stand/walk/run state), so a character mid-animation β€” an item use, a stagger, a knockback, the tail of a dodge, sitting β€” is out of it while standing perfectly still. That makes the refusal both ORDINARY and TRANSIENT.

    Three changes, mirroring the castspell itemiser convention (SkillVerbs.WhyNotReady):

    • the state line carries the whole gate now, plus a gate=OPEN|CLOSED verdict and the five flags it never printed (nextIsLocomotion, locomotionAction, sheathing, chargeCancelCd, cancelChargingSent, nextAtkAllowed);
    • a refusal is itemised by the new pure ForgeKit.DevSwing (FailingConditions / Explain), which names every failing conjunct AND the remedy, and is logged as a Warning β€” except a QUEUED press (vanilla's else if (m_nextAttackAllowed > 0) branch banks a combo/charge step and still returns false), which is reported as Info because it is not a failure at all;
    • swing WAITS up to 2 s for the gate to open before striking, exactly as the sheathed path has always waited for the draw animation. Refusing instantly there while waiting here was the inconsistency that made the verb read as broken. If the wait does not pay off it says so and does not attack.

    Cross-finding, same session: those five refusals almost certainly sat inside a WEDGED PAUSE ([UNSTICK] paused=True pausedBy=[Loading,Prologue] pauseScreenOpen=True), which independently stopped every AI detecting β€” and the command channel keeps answering throughout, because it polls unscaled time. A gate that stays closed for a full 2 s is that shape, so the timeout line now says so and names unstick.

    Additive: DevSwing is new public surface, nothing existing is re-signed, so COMPAT_SINCE stays 0.4.4. Pinned by tests/ForgeKit.Tests/DevSwingTests.cs. Live-verify owed.

0.4.7 β€” 2026-08-25

  • walkto <x> <z> rebuilt: navmesh or refusal. As shipped in 0.4.5/0.4.6 the verb placed the character at a fixed altitude instead of on the ground β€” five live calls across ~250 m of map all landed at exactly y = -1465.0, and a control call on the character's OWN x/z lifted them 23.9 m into the air. Root cause was the collider fallback, which was a design error rather than a tuning bug: a downward Physics.Raycast returns the FIRST collider it meets β€” as likely a roof or a bounding volume as the floor β€” and it was reached exactly when the navmesh check had already failed, so the verb surrendered its one safety property precisely where it was needed. Its own log line said no navmesh here and it placed anyway. That fallback is gone: only a NavMesh.SamplePosition hit (settled onto the collider surface beneath it) may set the height, and a column with no navmesh is a flat, loud refusal β€” standoff's rule, which has never dropped anyone.
  • Two supporting fixes for the same root cause. The sample centre now sweeps a downward-first ladder (DevStandoff.PlanColumn: 0, βˆ’8, βˆ’25, βˆ’60, βˆ’150, βˆ’400, +8, +25 m) instead of sitting at the caller's own y, so a call made from an already-wrong elevation recovers rather than compounds β€” 0.4.5 re-cast from the new elevated position and could never come back down. And a rise gate (DevStandoff.RiseOk / WalkToMaxRiseMeters = 3 m, tighter than standoff's 12 m RoofSanityMeters because walkto has no reference body) means a landing above the caller is taken only when no lower rung had navmesh at all, and is announced before the write.
  • The collider probe survives as diagnosis only, in its own ColliderColumnReport method, so the placement path provably contains no raycast; the [WALKTO] REFUSED line names the probe heights and reports what collider is in the column, flagged NOT used.
  • Why it mattered beyond the verb: the off-mesh character made the pet report CantReach, put the anchor off-mesh, and made standoff β€” correctly β€” refuse every subsequent placement. One bad verb disabled the good one and parked the world on a navmesh-free plane.
  • Additive only: new public DevStandoff.PlanColumn / MaxColumnProbes / WalkToColumnOffsets / RiseOk / WalkToMaxRiseMeters. walkto's help text now states the navmesh contract it actually keeps. COMPAT_SINCE stays 0.4.4 β€” nothing was removed or re-signed.

0.4.6 β€” 2026-08-25

  • New World-domain verb firecamp [on|off|remove|status] [distance 0.5-20]: places a REAL vanilla campfire on the ground ahead of the player and LIGHTS it, so a session can sample genuine ambient temperature instead of narrowing a band to fake the arithmetic. off extinguishes it in place (recovery can then be watched), remove destroys it, status prints the campfire's live TemperatureSource bands and the step predicted at the player's distance. Host-only.
  • Why the verb had to exist: useitem Campfire Kit answers TryUse -> True and deploys nothing. A deployable's Use does not deploy β€” it opens an interactive placement mode (BasicDeployable.OnItemUse -> DeployablePlacer.StartPlacement -> Character.StartDeploy) that idles until Character.DeployInput is driven by a real keypress, then round-trips an RPC and a SetupGround cast animation before Deployable.DeployableCast finally instantiates anything. A command channel has no input frames, so the chain can never complete from a verb; firecamp does DeployableCast's own work directly. Lighting is mandatory, not cosmetic: FueledContainer.StartInit DISABLES the campfire's TemperatureSource and only Kindle() re-enables it, and EnvironmentConditions skips any source that is not isActiveAndEnabled β€” an unlit campfire radiates nothing.
  • Additive only: new public ForgeKit.DevFire (grammar + the distanceβ†’step lookup), new FireAction/FireArgs, one new verb in the World domain. COMPAT_SINCE stays 0.4.4 β€” nothing was removed or re-signed.

0.4.5 β€” 2026-08-25

  • Ground-safe placement pair: standoff <metres> [bearing=<deg>] [target=pet|nearest|<species>] and walkto <x> <z>. Both probe real ground (navmesh sample, collider settle, path-connectivity check from the reference body's own polygon) and REFUSE rather than place the character in the air. standoff reads its pet through the existing CommonVerbsOptions.PetTarget seam, so a pet-owning consumer gets target=pet with no registration change.
  • teleport unchanged in behaviour (it still writes the RAW height it is given) but now warns BEFORE the write when the destination is more than 3 m above the ground, naming walkto/standoff.
  • Additive only: new public ForgeKit.DevStandoff (grammar + landing-search plan), two new verbs in the World domain. COMPAT_SINCE stays 0.4.4 β€” nothing was removed or re-signed.

0.4.4 β€” 2026-08-19

  • ForgeKit: restore the 6-arg CommandChannel ctor as a binary-compat overload

0.4.4 β€” 2026-08-19

  • ForgeKit: restore the 6-arg CommandChannel ctor as a binary-compat overload

0.4.3 β€” 2026-08-19

  • Cheat verbs: ForgeKit Cheats domain drives the game's own Debug Mode
  • Merge V6b fix (log-level gate: 14 unwrapped sinks retyped + requested-output exemption), peer-reviewed
  • SF11 review fixes: vanilla over-capacity gate, sibling cache reads, durable host
  • Fix V6b: the Quiet-tier exemption was inverted (ambient ungated, verbs gated)
  • SF11 fix A3b: petinvgive reports a SETTLED weight, inside the forge capture
  • Merge A1+A2+A5 fix (removeitem/unequip deferred settled reports via DeferredReport + script-control verbs unblocked), peer-reviewed
  • Merge B1+B2 fix (forge shell stale-cache self-heal + script response window), peer-reviewed
  • forge B1+B2 review follow-ups: keep-in-sync back-ref, skew-proof cache stamps, lease-timeout note
  • Review fixes: deferred verb reports stay inside the forge response capture
  • ForgeKit fix batch A (A1/A2/A5): settled readbacks + exact-verb script guard
  • Merge A6+B3 fix (SaveVerbs refusal order + consumed-savelist message), peer-reviewed
  • SaveVerbs A6+B3: honest post-select savelist message + in-world refusal reordered
  • Merge branch 'fix/sa-0815-leafkits' into feature/pet-self-feed
  • Review follow-ups on the F3c leaf-kit wave (M1, M2, L3, L5, L6)
  • Static-analysis wave F3c: leaf-kit fixes (A7-1/2/4/8/9/13/14/15/16/20/23/24/25/27)
  • A10 verb re-homing: photondump body -> NetKit.ViewRegistryDump; waiver docs; V28-V31
  • Coercion wave: every ForgeKit integration completes cleanly through the TUI seam
  • cfgdump filter accepts a full Section.Key (interface-sweep note) + sweep results recorded
  • Catalog-audit fixes: comment/noise rows out of completion, real skill completion, placeholder-aware usage grammar
  • UsageSpec: derive arg specs from help-string usage clauses β€” completion for the whole verb corpus with zero mod edits
  • … and 2 more (see git history)

0.4.2 β€” 2026-08-11

  • ForgeKit agent test-automation verbs: screenshot pipeline + removeitem
  • Merge branch 'main' into fix/sa-0808-kits
  • Static-analysis 2026-08-08 wave D-kits: P2-13/P2-15 + the kit P3 group
  • Static-analysis 2026-08-08 wave E: close the test-debt list (Β§4)
  • Static-analysis 2026-08-08 wave 0: nine P1/P2 fixes
  • Fix BUG-RAGDOLLJOINTLOSS: the rig re-init pass ate healthy ragdoll joints
  • Review fixes for lane B: drop the dead destroy companions, close the destroy-wanted ghost window, honest wording
  • Fix BUG-CHOWNOTCONSUMED family: authoritative guest consumption via ForgeKit.Inventories.ConsumeOne
  • invdump: read the player's inventory, and answer "how many X" with a number
  • loadsave: a session can now start its own world (live-verified)
  • W4: fixes for everything D1 found, plus the join-race P1
  • Log levels: a per-mod [Diag] LogLevel, gating at the source
  • Stuck-in-combat hardening: wedge-proof the vanilla hostility checker
  • ForgeKit: fake-null sweep in GiveVerbs equip/unequip (UNT0008)

Unreleased

  • New (built, NOT live-verified): screenshot [supersize 1-4] [label] CommonVerbs verb (EngineDiag domain, [SHOT] tag, works at the main menu) β€” captures the frame to BepInEx/screenshots/ and prints [SHOT] saved <path> <bytes> bytes once the file is size-stable, so a driving agent can retrieve and Read the PNG for visual analysis (scripts/game-shot.sh is the dispatch+retrieval wrapper). Pure grammar in DevShot (unit-tested).

  • New (built, NOT live-verified): removeitem [qty 1-999|all] <name-or-ItemID> CommonVerbs verb (Items domain, [REMOVE] tag) β€” the reverse of give, built on Inventories.ConsumeOne so guest-side destruction rides vanilla's compensation. Refuses ambiguous name matches, reports observed TOTAL QTY before -> after, never touches equipped gear. Pure grammar + stack allocation in DevRemove (unit-tested).

  • New (built, NOT live-verified): Inventories.ConsumeOne β€” authoritative item consumption on any Photon role. A bare Item.RemoveQuantity is a no-op for the last unit on a co-op guest (item destruction is master-gated in vanilla), which let guests feed/tame/bandage for free in consumer mods. The helper mirrors vanilla's own compensation (CharacterInventory.RemoveItem: destroy RPC to the master + local hide) and returns read-back facts (before/after/shortfall/ destroy-requested) so callers log from observation, not intent. With it, Inventories.All/AllByContainer now skip destroy-wanted items (the vanilla ItemContainer.ItemStackCount precedent), so a guest's just-spent item is no longer enumerable β€” or counted by invdump β€” during the destroy round-trip window.

0.4.1 β€” 2026-08-02

  • Cleanup: collapse three repeated verb preambles into helpers that already had a home
  • ForgeKit/SkillKit/StoryKit: fix five guards that could never fire, and stop the recovery verb lying
  • ForgeKit: one numeric-token parser for every dev verb, and it refuses NaN
  • Docs sweep: archive, condense, and validate the whole documentation tree

0.4.0 β€” 2026-07-30

  • Cap all-status build-up resistance below the engine's refusal sentinel
  • Review fixes: the shared kits must not enforce OUR id allocation
  • Draw every custom id from the community-allocated pool (87000-87999)
  • Wire the test-automation tooling wave: Containers+Resilience CommonVerbs domains, caravanreroll rows, ForgeKit 0.4.0, ledger/STATUS, limits survey
  • Merge lane 3: container/caravan queue-unblocker verbs + review fixes (scavengesim reachability honesty, quest-event disclosure, reopen gate typing, pristine-state skip)
  • Lane 3 review fixes: honesty gates on the container + scavenge-sim verbs
  • Merge lane 2: session resilience (unstick/LoadGate/goto hardening) + review fixes (Done precedence, sawLoading acceptance, latch decoupling)
  • Merge lane 1: ForgeKit command-script runner (script/scriptcancel/scriptstatus) + review fixes (waitloaded stale-flag acceptance rule, NaN rejection, pump-gap wording)
  • Review fixes F2/F3/F4-F5/F6: watchdog pre-load window, granular refusal, comment truth, latch ownership
  • Review fixes: waitloaded stale-TRUE trap, NaN waits, pump-gap misblame
  • Concurrent-session work committed as-is: grantstatus [force] + StatusApplyGate (DoT thread) + 2026-07-29/30 live-session testplan results
  • Session resilience: unstick verb, [LOADGATE] watchdog, hardened goto
  • Lane 3: container + caravan queue-unblocker verbs
  • Add script runner: one command line, several verbs, real time between steps
  • Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule