Skip to content

CommandRegistry

QinShenYu edited this page Jun 10, 2026 · 3 revisions

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.


Defining a Command

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}");
}

BaseCommand Members

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.

Built-in Argument Hints (Game Behavior)

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!

0.4.2: Hierarchical Subcommands

BaseCommand supports an optional SubCommands dictionary for structured command routing. This eliminates manual if/else or switch dispatch logic in Execute().

Basic Usage

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 Helper

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

Automatic Tab Completion

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.

Conflict with bool / position Args

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.


0.4.2: TryRegister with Owner Ledger

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


0.4.2: Unregister Commands

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"); // false

Query API (0.5.0)

Two 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"

Adding Argument Descriptions and Auto-fill

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}'.");
    }
}

Naming Convention

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.


Duplicate Registration

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.


Diagnostic: scavlib check

Run scavlib check in the developer console for a full diagnostic report, built on the query API above. It prints three sections:

  1. 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.
  2. Commands (via ScavLib) — every command ScavLib injected, paired with its owner mod name (from GetAllRegistered()).
  3. 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.

Clone this wiki locally