HelloOutward — the starter mod to copy¶
HelloOutward is a minimal example mod for Outward that does almost nothing on its own — it exists to be copied. It is the template every other mod in this family is forked from, wired up with the shared dev tooling so a new mod builds, deploys, and has a working in-game command loop from the first line you write. It's for modders; there's nothing here to play with.
At a glance
- Type: gameplay mod (starter template)
- GUID: cobalt.hellooutward
- Requires: BepInEx 5 (Outward's Mono branch), ForgeKit
- Config: BepInEx/config/cobalt.hellooutward.cfg
- Commands: BepInEx/config/HelloOutward_cmd.txt
For players¶
There's nothing to play. HelloOutward logs a greeting when it loads and again on each area load; it's a scaffold for building your own mod, not a feature.
What you get out of the box¶
Copy the template and you inherit, already wired:
- A ForgeKit command channel — write a verb into
BepInEx/config/HelloOutward_cmd.txtand it runs on the next poll.helplists everything;selftestruns the template's[SELFTEST]report. The poll is on unscaled time, so verbs fire even while the game is paused. The channel adopts the file's state at boot, so a verb left over from the previous session does not replay at startup. - ForgeKit's shared dev-verb pack, already registered on that channel — see Commands.
- Config binding — one example
Config.Bind(...)entry, so the mod generates its ownBepInEx/config/<guid>.cfg. - The keybind Claim pattern — the copy-ready (commented) pair for binding a key and registering it with ForgeKit's cross-mod keybind registry, plus a self-test assert that fails loudly if two mods claim the same key.
- A smoke-test Harmony patch that confirms Harmony and the game-library references resolve at runtime.
Example configuration¶
BepInEx/config/cobalt.hellooutward.cfg — created on first launch. The template binds one key, so a
fresh fork's config is a single section (this doubles as the template for a new mod's config):
The [Keys] GreetKey pair (see Adding a keybind) ships commented out and bound
to KeyCode.None, so it produces no config section until you wire a real key.
Commands¶
Write a verb into BepInEx/config/HelloOutward_cmd.txt and it runs on the next poll (results go to
the log). Write help to list them all.
| Command | What it does |
|---|---|
selftest |
Run the template's checks and log [SELFTEST] PASS/FAIL … DONE. Runs at the main menu too. |
Most of what answers on this channel comes from ForgeKit's shared dev-verb pack, which the
template registers for you — items and inventory (give, drop, useitem, equip, unequip,
givewater, givemoney, containerdump, containerroll), world and staging (teleport, goto,
moveto, pos, face, settime, reloadcfg), combat (sethp, swing, castspell, lockon,
lockoff, combatclear, killnearest, grantstatus, removestatus), skills (learnskill,
unlearnskill, resetcooldowns), the engine dumps (statusdump, scenedump, skydump,
groundprobe, combatmgrdump, keybinds, ragdolldump, psdump) and unstick (alias unwedge).
Registries are per-mod, so this copy answers only on HelloOutward_cmd.txt and never collides with
another mod's. See ForgeKit for the full pack and for turning domains off.
help and the scripting verbs (script, scriptstatus, scriptcancel) come from the command
channel itself, so every mod in this family has them whether or not it takes the pack.
reloadcfg works because the template passes a ConfigSource — BepInEx 5 has no config file
watcher, so without it a hand-edited .cfg does nothing until a relaunch.
Scaffold a new mod¶
scripts/new-mod.sh <ModName> clones HelloOutward into a ready-to-build project. It:
- copies
src/HelloOutward/tosrc/<ModName>/(dropping any oldbin/obj), - renames
HelloOutward.csprojto<ModName>.csproj, - rewrites the identifiers in every
.cs/.csproj— GUIDcobalt.hellooutward→cobalt.<modname>(lowercased), and bothHelloOutwardandHello Outward→<ModName>(so the namespace,AssemblyName, class names,NAME, and the command-channel filename all follow), and - adds the new project to the solution (
dotnet sln Outward.slnx add ...).
<ModName> must be PascalCase letters and digits only — no spaces or dots.
Then:
- Edit
NAME/VERSIONinsrc/<ModName>/Plugin.cs. dotnet build Outward.slnx -c Release(output lands indist/<ModName>/).- Deploy it to the game machine — set your destination once, then
./scripts/deploy.sh:
echo 'user@gamebox:/path/to/Outward/BepInEx/plugins' > .deploy-target
dotnet build Outward.slnx -c Release
./scripts/deploy.sh
The new mod inherits ForgeKit's dev loop from the start: write verbs into
BepInEx/config/<ModName>_cmd.txt (help lists them, selftest runs the [SELFTEST] template).
If you'd rather not use the script, the manual steps are the same: cp -r src/HelloOutward
src/MyMod, rename the .csproj and its AssemblyName, change the GUID/NAME/namespace in
Plugin.cs, and dotnet sln Outward.slnx add src/MyMod/MyMod.csproj.
The minimal Plugin.cs¶
The whole shape of a mod fits in one class. The essentials:
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using ForgeKit;
namespace HelloOutward
{
[BepInPlugin(GUID, NAME, VERSION)]
[BepInDependency(ForgeKit.Plugin.GUID, ForgeKit.Plugin.VERSION)] // VERSION = the min-version floor, see kits/versioning.md
public class Plugin : BaseUnityPlugin
{
public const string GUID = "cobalt.hellooutward"; // change these when you fork
public const string NAME = "Hello Outward";
public const string VERSION = "1.0.0";
internal static ManualLogSource Log;
public static ConfigEntry<bool> GreetOnEachAreaLoad;
private CommandRegistry _commands;
private VerbHost _verbs;
private CommandChannel _channel;
internal void Awake()
{
Log = Logger;
GreetOnEachAreaLoad = Config.Bind(
"General", "GreetOnEachAreaLoad", true,
"Log a greeting every time the game loads prefab resources.");
RegisterVerbs();
// primeStamp: adopt the command file's current state, so a verb left over from
// the previous session does not replay at startup.
_channel = new CommandChannel("HelloOutward_cmd.txt", Log, _commands, primeStamp: true);
new Harmony(GUID).PatchAll(); // applies every [HarmonyPatch] in this assembly
}
internal void Update() => _channel.Tick();
private static Character LocalPlayer => CharacterManager.Instance?.GetFirstLocalCharacter();
private void RegisterVerbs()
{
_commands = new CommandRegistry(Log);
_verbs = new VerbHost(_commands, Log, () => LocalPlayer);
_verbs.Register("selftest", "Run the template self-test.",
_ => SelfTest(), tag: "[HelloOutward]", needsPlayer: false);
// ForgeKit's shared dev-verb pack, on THIS mod's own channel. ConfigSource is what
// wires up `reloadcfg` (it's a getter because BaseUnityPlugin.Config is protected).
CommonVerbs.RegisterAll(_verbs, Log,
new CommonVerbsOptions { ConfigSource = () => Config });
}
}
}
Notes on the pieces:
[BepInDependency]on ForgeKit makes BepInEx load ForgeKit first — without it your command channel might construct before the kit is ready.- Verbs register through
VerbHost, not the raw registry.VerbHostwraps each verb with a shared prologue: a local-player guard (a player-guarded verb body never has to null-checkctx.Player), an optionalmasterOnly:truePhoton gate for anything that mutates networked state, and a tagged try/catch.needsPlayer:falseopts a verb out of the player guard (asselftestdoes, so it runs at the main menu).ctx.Arg(n)/ctx.Tail()read arguments. _channel.Tick()inUpdateis what polls the command file each frame.
Adding a keybind¶
Keys are a cross-mod resource — a mod can't see another mod's config, so
ForgeKit's Keybinds registry is the only place a collision is knowable. Bind
your key and Claim it:
GreetKey = Config.Bind("Keys", "GreetKey",
new KeyboardShortcut(KeyCode.None), "Say hello.");
ForgeKit.Keybinds.Claim(NAME, "say hello", GreetKey);
The template ships with this pair commented out and bound to KeyCode.None on purpose, so a fresh
fork can't instantly collide — pick a genuinely free key when you wire a real bind. Keep the
!Keybinds.HasConflicts() check in the self-test; it's what makes a clash fail loudly at boot or when
you run selftest.
Growing beyond the template¶
- Add a
core/<Mod>.Core(netstandard2.0) project plus aProjectReferenceif the mod grows pure logic worth unit-testing off the game. - Add a SideLoader reference only if the mod registers custom SideLoader content (items, recipes, skills).
- Everything else — build settings, game references — is inherited from the root
Directory.Build.props, so the.csprojstays a few lines.
See also¶
- ForgeKit — the dev tooling HelloOutward inherits (command channel, self-test harness, keybind registry, shared verb pack)
- Kits index
- Mods index
- Wiki home