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
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 plusMark/IsScripted/Describe. Purely additive, soCOMPAT_SINCEstays 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/Merchantmachinery is stripped by design), no lock-on name (a puppet has noCharactercomponent at all), invisible to everyroadsmerchant/roadsstatus/NpcRegistrycensus β 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, andunstick fixnow 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 pollsTime.unscaledTimeby design). It is intermittent and self-clearing, not a permanent latch β a later same-moment reading showedpaused=False pausedBy=[]with the pet sim'sticks=climbing andpausedTicks=frozen β but a bad window ran past a minute (six consecutive polls readingpaused=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 β sounstick fixhad to walk down to rung 4 (forceunpause), which clearsm_gameplayPausedBywholesale (theProloguekey with it) and so releases the WORLD SIM even though the load coroutine stays parked. At roughly one invocation per poll that is the observedt+0 β¦ t+61sstaircase, and it is why the latency varied with how many rungs a given attempt got through. With rung 0 in front, the firstunstick fixshould be the one that works.The mechanism, from the decompile:
ProloguePanel.ShowpushesPauseGameplay("Prologue")(ProloguePanel.cs:27). It is released ONLY byProloguePanel.OnHideβUnPauseGameplay("Prologue")(:63-70), reached only whenGoToNextPagewalks past the last screen (:34-60). The sole vanilla caller ofMenuManager.GoToNextProloguePageisLocalCharacterControl.cs:185-189, behindControlsInput.QuickDialogueUpβ a real keypress on an OS-focused window. Unfocused or headless, that press never happens.NetworkLevelLoader.FinishLoadLevelparks onwhile (MenuManager.Instance.IsProloguePanelDisplayed)(:1523-1526), which sits BEFORE thewhile (!m_continueAfterLoading)gate (:1538) and before theSendReadyToContinueRPC (:1541). SoreadyIdsstays empty,AllPlayerReadyToContinuestays false, andUnPauseGameplay("Loading")(:1581) is never reached β which is why the pause stack read[Loading,Prologue]withallDone=True allReady=False.- By that point
m_gameplayLoading(:1461),m_loadingLevel(:1215) andm_waitingForOtherPlayers(:1490) have all been cleared, soIsOverallLoadingDonereads 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 answereddoneabout it. - The
gaterung was therefore a no-op on this wedge: it set a latch the coroutine had not reached. It was not being "overwritten" βm_continueAfterLoadinghas exactly two writers,SetContinueAfterLoading(:970) andBaseLoadLevel(:870), and every observed revert toFalsefollowed a fresh load.
What changed:
- New rung 0,
prologue, tried FIRST byunstick fix: callMenuManager.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. LoadSnapshotgainedProloguePanelUp;LoadPhasegained theprologuephase, classified ABOVEdone(anIsOverallLoadingDonesnapshot is not proof the sim is running); andGateGuardHoldsgained!ProloguePanelUp, so neither the watchdog nor thegaterung 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 reachforceready. - 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 whyapplied forceunpausewas followed bypaused=True otherPlayerPaused=Trueand then byFalseon the next poll. Every applied rung now schedules a deferred[UNSTICK] verify step=β¦ pausedBefore=β¦ pausedNow=β¦ β HELD | REVERTED | NO-CHANGEline 0.75 s (realtime) later, and re-dumps under it.NO-CHANGEexists 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 gainedprologuePanel=β¦β the field whose absence made the wedge unreadable.
-
On the
otherPlayerPaused=Truewithpeers=0lead. Real, and explained: when the pause stack goes 0β1,OnReceivePauseGameplay(:1976-1999) broadcastsSendPauseStatus(true), which in an offline room lands locally and puts the LOCAL player's own id intom_playerCurrentlyInPause;UpdateGameplayPaused(:401) then reports it asm_otherPlayerPaused. It clears only when the stack drains to 0 (:2009-2017) β impossible whilePrologueis 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 byOnPhotonPlayerDisconnected(:1797-1802), and in a normal session the stack always drains. Theforceunpauserung's rawClear()does bypass theSendPauseStatus(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
-
swingnow 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/100followed byAttackInput(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) ism_inLocomotion && m_nextIsLocomotion && !Blocking && !LocomotionAction && !Sheathing && !InChargeCancelCooldown && !m_cancelChargingSentβ soinLocomotion=FalseALONE 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
castspellitemiser convention (SkillVerbs.WhyNotReady):- the state line carries the whole gate now, plus a
gate=OPEN|CLOSEDverdict 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'selse 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; swingWAITS 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 namesunstick.Additive:
DevSwingis new public surface, nothing existing is re-signed, soCOMPAT_SINCEstays 0.4.4. Pinned bytests/ForgeKit.Tests/DevSwingTests.cs. Live-verify owed. - the state line carries the whole gate now, plus a
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 exactlyy = -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 downwardPhysics.Raycastreturns 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 saidno navmesh hereand it placed anyway. That fallback is gone: only aNavMesh.SamplePositionhit (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 thanstandoff's 12 mRoofSanityMetersbecausewalktohas 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
ColliderColumnReportmethod, so the placement path provably contains no raycast; the[WALKTO] REFUSEDline names the probe heights and reports what collider is in the column, flaggedNOT used. - Why it mattered beyond the verb: the off-mesh character made the pet report
CantReach, put the anchor off-mesh, and madestandoffβ 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.offextinguishes it in place (recovery can then be watched),removedestroys it,statusprints the campfire's liveTemperatureSourcebands and the step predicted at the player's distance. Host-only. - Why the verb had to exist:
useitem Campfire KitanswersTryUse -> Trueand deploys nothing. A deployable'sUsedoes not deploy β it opens an interactive placement mode (BasicDeployable.OnItemUse->DeployablePlacer.StartPlacement->Character.StartDeploy) that idles untilCharacter.DeployInputis driven by a real keypress, then round-trips an RPC and aSetupGroundcast animation beforeDeployable.DeployableCastfinally instantiates anything. A command channel has no input frames, so the chain can never complete from a verb;firecampdoesDeployableCast's own work directly. Lighting is mandatory, not cosmetic:FueledContainer.StartInitDISABLES the campfire'sTemperatureSourceand onlyKindle()re-enables it, andEnvironmentConditionsskips any source that is notisActiveAndEnabledβ an unlit campfire radiates nothing. - Additive only: new public
ForgeKit.DevFire(grammar + the distanceβstep lookup), newFireAction/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>]andwalkto <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.standoffreads its pet through the existingCommonVerbsOptions.PetTargetseam, so a pet-owning consumer getstarget=petwith no registration change. teleportunchanged 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 toBepInEx/screenshots/and prints[SHOT] saved <path> <bytes> bytesonce the file is size-stable, so a driving agent can retrieve and Read the PNG for visual analysis (scripts/game-shot.shis the dispatch+retrieval wrapper). Pure grammar inDevShot(unit-tested). -
New (built, NOT live-verified):
removeitem [qty 1-999|all] <name-or-ItemID>CommonVerbs verb (Items domain,[REMOVE]tag) β the reverse ofgive, built onInventories.ConsumeOneso guest-side destruction rides vanilla's compensation. Refuses ambiguous name matches, reports observedTOTAL QTY before -> after, never touches equipped gear. Pure grammar + stack allocation inDevRemove(unit-tested). -
New (built, NOT live-verified):
Inventories.ConsumeOneβ authoritative item consumption on any Photon role. A bareItem.RemoveQuantityis 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/AllByContainernow skip destroy-wanted items (the vanillaItemContainer.ItemStackCountprecedent), so a guest's just-spent item is no longer enumerable β or counted byinvdumpβ 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:
unstickverb,[LOADGATE]watchdog, hardenedgoto - Lane 3: container + caravan queue-unblocker verbs
- Add
scriptrunner: one command line, several verbs, real time between steps - Hyena/Pearlbird tuning wave: HAO taunt, gifts, bone relic, feed rule