ForgeKit — shared dev-tooling for Outward mods¶
ForgeKit is a kit (reusable library) for Outward that gives mod authors a common set of development tools: a file-driven dev-command loop, a self-test harness, on-screen toasts, a player-ready lifecycle wait, config-backed data tables, a cross-mod keybind registry, and a pack of ready-made dev verbs. It is the bottom layer every other kit and mod in this family rests on. It has no player-facing surface of its own.
At a glance
- Type: reusable library (kit)
- GUID: cobalt.forgekit
- Requires: BepInEx 5 (Outward's Mono branch)
- Dependencies: none (no SideLoader, no Harmony, no other kit)
- Config: none
- Commands: none of its own — it provides the command-channel machinery other mods use
For players¶
You won't interact with ForgeKit directly. It ships as a dependency of the mods you install and does nothing on its own — install it because another mod declares it as a requirement.
What's in it¶
| Piece | What it gives a modder |
|---|---|
CommandChannel + CommandRegistry |
A file-driven dev-command loop: write a verb into BepInEx/config/<yourmod>_cmd.txt and it runs on the next poll. The poll runs on unscaled time, so it still fires while the game is paused. Unknown verb (or help) prints every registered verb with its help text. |
VerbHost / VerbContext |
A thin wrapper over the registry that adds a per-verb prologue: resolve the local player, an optional Photon master-only gate, and a per-verb error tag. |
SelfTestHarness |
The [SELFTEST] BEGIN / PASS: / FAIL: / DONE pass= fail= report shape — wire up a handful of Check(name, condition) calls and get one consistent, greppable self-test report. |
Notify |
An on-screen info toast for the player, mirrored to the log so log-only sessions see it too. |
Lifecycle |
WhenPlayerReady — a coroutine that waits until the player is actually placed on a gameplay scene (past the game's void/staging coordinates) before your mod starts touching it, plus IsSanePosition for the same check inline. |
TableLoader<T> / EmbeddedRes |
Embedded-default-plus-config-override data tables: ship a sane default table inside your DLL, and let players override it by dropping a same-format file into BepInEx/config/. |
Keybinds |
A cross-mod keybind registry: each mod claims its keys, and if two mods claim the same key ForgeKit logs a warning naming both claimants and where to rebind. |
CommonVerbs |
A shared pack of generic dev verbs (item spawning, world/combat staging, engine diagnostics) any mod can register onto its own command channel. |
The command channel¶
The command channel is the in-game iteration loop. A consuming mod constructs a CommandChannel
pointed at a file name (e.g. YourMod_cmd.txt) and calls Tick() from its Update. Writing a line
into BepInEx/config/YourMod_cmd.txt runs that verb on the next poll:
- The poll cadence is on unscaled time, so verbs fire even while the game is paused (a menu open,
the game at
timeScale 0). - A command file may batch multiple lines — each non-blank, non-
#line runs in order. - A verb that throws is caught and logged, so one bad line in a batch doesn't kill the rest.
- Unknown verb, empty input, or the literal
helpprints the full registered verb list with help text.
Keybinds can drive the same verbs directly (channel.Run("somedump")), so a mod's F-keys and its
command file share one code path.
The shared verb pack (CommonVerbs)¶
CommonVerbs.RegisterAll(host, log) adds a batch of generic dev verbs to a mod's own command channel
(they answer on that mod's <mod>_cmd.txt). Verbs are grouped into domains a consumer can toggle or
opt out of individually.
| Domain | Verbs |
|---|---|
| Items | give, drop, useitem, givewater, equip, unequip |
| World | teleport, goto, settime, givemoney |
| Combat | sethp, combatclear, killnearest |
| Skills | learnskill, unlearnskill, resetcooldowns |
| Engine diagnostics | statusdump, scenedump, skydump, groundprobe, combatmgrdump, keybinds, ragdolldump, psdump |
A few highlights:
give [pouch\|bag\|ground] [qty] <name-or-ItemID>/drop [qty] <name-or-ItemID>— spawn any item by name or numeric ItemID; an unresolved name logs did-you-mean suggestions.givewater [clean\|river\|salt\|rancid\|leyline\|sparkling\|healing]— spawn a pre-filled Waterskin.goto <scene> [spawnPoint]/teleport <x> <y> <z>/settime <hour>— world staging.gotoandsettimeare host-only (a guest driving them would desync a co-op party).learnskill <name-or-ItemID>/unlearnskill <name-or-ItemID\|all>— teach or forget any skill, so a test save doesn't need a cheat menu.keybinds— report every key claimed by every mod in the family, grouped by key, with conflicts flagged (see the keybind registry below).
Each mod chooses which domains it takes; a mod that ships its own richer version of a verb excludes the pack's copy and keeps its own.
Settings¶
ForgeKit has no configuration file of its own — it provides the command channel and config-table
helpers that consuming mods use. The pieces it ships (TableLoader, CommandChannel, Keybinds)
read the consuming mod's files and config, always under that mod's logger. Each consumer gets its
own command file at BepInEx/config/<Mod>_cmd.txt; ForgeKit itself neither writes a .cfg nor polls
a command file.
Commands¶
ForgeKit runs no command channel itself — it provides the channel machinery other mods use, and the
CommonVerbs pack they register onto their own channels. See the tables above.
For modders¶
Depend on it¶
Reference the kit and declare the dependency so BepInEx loads it first:
<!-- your .csproj -->
<ProjectReference Include="path\to\ForgeKit\ForgeKit.csproj" Private="false" />
Private="false" matters — it stops MSBuild from copying a second ForgeKit.dll into your mod's
output folder. The kit ships from its own BepInEx/plugins/ForgeKit/ folder; your mod just
references it and declares the dependency.
Wire up a command channel and a verb¶
[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!"));
// Optional: add the shared dev-verb pack, resolving the local player yourself.
CommonVerbs.RegisterAll(_commands, Logger, () => CharacterManager.Instance?.GetFirstLocalCharacter());
_channel = new CommandChannel("YourMod_cmd.txt", Logger, _commands);
}
void Update() => _channel.Tick();
}
Everything takes the consumer's ManualLogSource, so every line lands under your mod's name in the
log.
Claim your keybinds¶
A mod cannot see another mod's config, so a key collision is only knowable in a shared place. Each mod claims its keys at boot; ForgeKit reports a clash naming both claimants and where to rebind:
Keybinds.Claim(NAME, "tame the targeted animal", MyConfig.TameKey); // ConfigEntry<KeyboardShortcut>
Re-claiming (a live rebind, a config reload) replaces the old claim, so retuning keys never spawns
phantom conflicts. The keybinds verb dumps the whole registry on demand.
Extension points and traps¶
CommandChannelcadence. The constructor takespollSeconds, anallLinesflag (run every line vs. the first non-blank line only), andprimeStamp(skip commands already sitting in the file at boot). Defaults match the common case; the flags exist to preserve each consumer's historical behavior.Lifecycle.WhenPlayerReadymust be started via your plugin'sStartCoroutine(Unity coroutines need a MonoBehaviour host). Pass a stablewaitKey(e.g.this) to auto-supersede an older in-flight wait when two scene loads land in quick succession — otherwise a door-to-door transition could double-fire your per-scene setup.TableLoader<T>/EmbeddedResload resources from the consumer's assembly, not ForgeKit's — pass your ownAssemblyso the loader finds your embedded default. A missing default is always logged, never silent.Notify.Logis a settable static; assign it (Notify.Log = Logger) in yourAwakeso toasts are mirrored under your mod.
See also¶
- Kits index
- Installing
- Kits that build on ForgeKit: AggroKit, NetKit, SkillKit, StoryKit, CompanionKit, SpawnKit
- Wiki home