-
Notifications
You must be signed in to change notification settings - Fork 0
CommandRegistry
Namespace: ScavLib.command
Safely register custom commands into the game's native developer console.
Commands are injected at the correct time automatically via a Harmony Postfix
on ConsoleScript.Start() — you do not need to manage injection timing.
Inherit BaseCommand and implement the required members:
using ScavLib.command;
public class HealCommand : BaseCommand
{
public override string Name => "mymod_heal";
public override string Description => "Fully heal the player.";
public override void Execute(string[] args)
{
ScavLib.util.PlayerUtil.HealAll();
ScavLib.util.GameUtil.Log("[HealCommand] Player healed.");
}
}Then register it in your plugin's Awake():
private void Awake()
{
// Recommended (0.4.2+): TryRegister with explicit owner
if (!CommandRegistry.TryRegister(new HealCommand(), "MyMod", out var error))
Logger.LogError($"Failed to register command: {error}");
}| Member | Type | Required | Description |
|---|---|---|---|
Name |
string |
Yes | Command name as typed in the console. No spaces. |
Description |
string |
Yes | Short description shown in console help. |
ArgDescription |
(string, string)[] |
No | Per-argument descriptions. Each entry is (shortDesc, longDesc). |
ArgAutofill |
Dictionary<int, List<string>> |
No | Auto-complete candidates. Key is argument index (0 = first arg after command name). In Execute(string[] args), args[0] is the command name itself, so key 0 corresponds to args[1], key 1 to args[2], and so on. |
SubCommands |
Dictionary<string, BaseCommand> |
No | (0.4.2+) Named child commands for hierarchical routing. Keys must be lowercase. |
Execute(string[] args) |
void |
Yes | Command logic. args[0] is the command name itself. |
The game's Command constructor automatically injects autofill candidates for
certain argument types based on ArgDescription. Do NOT manually list these
in ArgAutofill — the constructor uses Add() rather than [], and a
duplicate key will throw ArgumentException at registration time.
ArgDescription Item1 prefix |
Auto-injected candidates |
|---|---|
"bool" |
true, false
|
"position" |
cursor, player, random, #.#
|
// CORRECT: let the game inject bool candidates automatically
public override (string, string)[] ArgDescription => new (string, string)[]
{
("bool verbose", "Enable verbose output?")
};
// Do NOT add ArgAutofill[0] = ["true", "false"] — it will collide!BaseCommand supports an optional SubCommands dictionary for structured
command routing. This eliminates manual if/else or switch dispatch logic
in Execute().
using ScavLib.command;
using System.Collections.Generic;
public class MyModCommand : BaseCommand
{
public override string Name => "mymod";
public override string Description => "MyMod management commands.";
private readonly Dictionary<string, BaseCommand> _subs =
new Dictionary<string, BaseCommand>
{
{ "heal", new HealSubCommand() },
{ "info", new InfoSubCommand() },
};
public override Dictionary<string, BaseCommand> SubCommands => _subs;
public override void Execute(string[] args)
{
ExecuteSubCommand(args, subArgIndex: 1);
}
}ExecuteSubCommand(string[] args, int subArgIndex) is a protected method on
BaseCommand that handles the full dispatch lifecycle:
- No subcommand provided → prints usage help listing available subcommands
-
help/?/--help→ same usage help - Unknown subcommand → prints error message with the available list
-
Valid subcommand → dispatches to the matching child
BaseCommand
Subcommand keys MUST be lowercase — ExecuteSubCommand lowercases the incoming
arg before lookup, mirroring the console's case-insensitive behavior. For
nested subcommands (e.g. mymod player heal), the inner command's Execute
should call ExecuteSubCommand(args, subArgIndex: 2).
ScavLib automatically merges SubCommands keys into ArgAutofill[0] at
registration time. First-level subcommand names get Tab completion for free.
Limitation: The game console only consults the top-level command's
argAutofill (see ConsoleScript.TryFinishCommandPart). Nested subcommand
names (second level and beyond) cannot be Tab-completed — expose them
via your help output instead.
SubCommands is rejected at registration time if ArgDescription[0]
starts with bool or position, because the game's Command constructor
will already auto-inject candidates for those types and the autofill keys
would collide. Use SubCommands OR a bool/position first arg, not both.
TryRegister is the preferred registration API. It returns explicit
success/failure and records which mod owns each command in an internal ledger,
enabling the scavlib check diagnostic and safe Unregister operations.
private void Awake()
{
// Recommended: use TryRegister with explicit owner
if (!CommandRegistry.TryRegister(new MyCommand(), "MyMod", out var error))
Logger.LogError($"Failed to register command: {error}");
// Legacy: Register() still works but records null as owner
CommandRegistry.Register(new LegacyCommand());
}The owner string should match your ModInfo.Name. It is optional but is the
standard going forward as the mod-session system matures in 0.5+.
Remove a previously-ScavLib-registered command from the game's console. Only commands present in the owner ledger can be removed — game-native commands are protected automatically because they were never added to the ledger. Commands still pending injection (registered before the console existed) can also be removed.
// Remove a command your mod registered
CommandRegistry.Unregister("mymod_temp_command");
// Attempting to unregister a game-native command returns false + logs a warning
bool ok = CommandRegistry.Unregister("heal"); // falseTwo query methods expose the owner ledger for tooling and diagnostics. The
scavlib check command is built on top of them.
| Method | Returns | Description |
|---|---|---|
GetOwner(string name) |
string |
The owner mod name for a ScavLib-registered command, or null if the command is unknown to ScavLib (game-native, or registered without an owner). |
GetAllRegistered() |
IReadOnlyList<(string name, string owner)> |
Snapshot of every command ScavLib has injected, each paired with its owner (or null). |
foreach (var (name, owner) in CommandRegistry.GetAllRegistered())
GameUtil.Log($"{name} (owner: {owner ?? "<no owner>"})");
string who = CommandRegistry.GetOwner("mymod_heal"); // "MyMod"public class SpawnCommand : BaseCommand
{
public override string Name => "mymod_spawn";
public override string Description => "Spawn an item at the player's position.";
public override (string, string)[] ArgDescription => new (string, string)[]
{
("string id", "The resource ID of the item to spawn.")
};
public override Dictionary<int, List<string>> ArgAutofill =>
new Dictionary<int, List<string>>
{
{ 0, new List<string> { "bandage", "rifle", "pistol", "medkit" } }
};
public override void Execute(string[] args)
{
if (args.Length < 2)
{
ScavLib.util.GameUtil.Log("Usage: mymod_spawn <id>");
return;
}
string id = args[1];
var go = ScavLib.util.GameUtil.SpawnAtPlayer(id);
ScavLib.util.GameUtil.Log(go != null ? $"Spawned '{id}'." : $"Unknown item '{id}'.");
}
}Prefix your command name with a short mod identifier to avoid collisions with built-in commands and other mods:
mymod_spawn // OK (recommended)
spawn // NO (collides with a built-in command — rejected)
ScavLib validates command names at registration time and rejects:
- null or empty names
- names containing spaces
- names that collide with the 47 built-in game commands
A soft warning (not a rejection) is emitted for names without an underscore
prefix, encouraging the <modname>_<commandname> convention.
If a command with the same name is already registered, CommandRegistry
logs a warning and skips the duplicate. The warning includes the owner of the
existing command (if registered via TryRegister), helping you identify which
mod is causing the conflict. Check the BepInEx log if your command does not
appear in the console.
Run scavlib check in the developer console for a full diagnostic report,
built on the query API above. It prints three sections:
-
Harmony Patches — every patch ScavLib applies, each marked
[OK]or[FAIL]. A failed patch shows its error message. This reflects patch registration, not runtime activity. -
Commands (via ScavLib) — every command ScavLib injected, paired with
its owner mod name (from
GetAllRegistered()). - Conflicts — a summary line. Because the owner ledger plus the duplicate-name rejection in injection mean a real conflict can't slip through, any rejected registration was already logged at register-time.
Useful for triaging "why isn't my event firing" — if a patch shows [FAIL],
you know the dependent events are silent and why.