Jump to content

Grimwar/VMod

From Vellocet Developer Community
(Redirected from Grimwar/Server-side C)
Version target. This guide covers VMod API 5 and addon schema 9. A server rejects packages built for another API or schema.

VMod is Grimwar's server-authoritative C# mod runtime. The server compiles source with Roslyn C# 2.0 and runs the result through dotnow. Grimwar itself remains an IL2CPP build.

A VMod can use the public Grimwar.ModApi services. It cannot reference Unity, FishNet, Grimwar internals, arbitrary files, sockets, reflection, processes, or native code.

What a VMod package contains

The addon folder is the install unit.

community.hello/
  addon.json
  mod.json
  HelloMod.csx

Use .csx below a Unity project's Assets folder so the editor recognizes C# without asking Unity to compile the file. A VMod kept outside Unity can use .cs.

addon.json registers the folder with Grimwar's addon catalog:

{
  "schemaVersion": 9,
  "id": "community.hello",
  "title": "Hello Server",
  "author": "Ada",
  "version": "1.0.0",
  "distributionProfile": "development",
  "buildId": "0123456789abcdef0123456789abcdef",
  "createdUtc": "2026-08-25T00:00:00Z",
  "sdkVersion": "1.0.3",
  "unityVersion": "6000.3.17f1",
  "requires": [],
  "contents": [
    {
      "kind": "mods",
      "id": "community.hello",
      "title": "Hello Server",
      "mod": { "manifest": "mod.json" }
    }
  ]
}

The addon ID, content ID, and VMod ID must resolve to the same normalized ID. requires contains addon IDs. Grimwar enables and loads required addons before the package that names them.

mod.json defines the code entry point:

{
  "id": "community.hello",
  "name": "Hello Server",
  "version": "1.0.0",
  "apiVersion": 5,
  "kind": "addon",
  "entryPoint": "CommunityMods.HelloMod",
  "sourceFiles": ["HelloMod.csx"],
  "dependencies": [],
  "permissions": [],
  "enabled": true,
  "serverOnly": true
}
Field Rule
id Stable ID up to 96 characters. Use lowercase letters, digits, dots, underscores, and hyphens.
apiVersion Must equal the server's current VMod API, currently 5.
kind addon, admin, or gameMode. Administrative host operations require admin.
entryPoint Fully qualified, non-abstract class implementing IMod.
sourceFiles Relative .cs or .csx paths contained by this addon.
dependencies VMod IDs that must load first.
permissions Permission names the VMod may attach to its commands and menu items. This does not grant those permissions to players.
serverOnly Must be true in API 5.

Write the entry class

This mod registers hello for chat and authenticated consoles:

using System;
using Grimwar.ModApi;

namespace CommunityMods
{
    public sealed class HelloMod : IMod
    {
        private IModContext _context;

        public void OnLoad(IModContext context)
        {
            _context = context;
            var result = context.Commands.Register(new ModCommandSpec
            {
                Name = "hello",
                Description = "Say hello to the server.",
                MinimumArguments = 0,
                MaximumArguments = 0,
                AllowChat = true,
                AllowConsole = true,
                ChatAliases = new[] { "hi" }
            }, "OnHello");

            if (!result.Success)
                throw new InvalidOperationException(result.Message);

            context.Log.Write("Hello Server loaded.");
        }

        public void OnUnload()
        {
            _context = null;
        }

        public void OnHello(IModCommandInvocation command)
        {
            var name = command.Caller == null
                ? "server console"
                : command.Caller.DisplayName;
            _context.Messages.Broadcast("Hello from " + name + "!");
        }
    }
}

Registrations take a method name because delegates do not cross the interpreter boundary. Grimwar verifies the method before accepting the registration.

Registration Handler signature
Command One IModCommandInvocation parameter
Event One ModEvent parameter
Timer One ModTimerInvocation parameter
VSig subscription One ModVSigEvent parameter
Menu One ModMenuInvocation parameter

OnUnload must clear references and release any VMod-owned state that is not already scoped through the host. Commands, event subscriptions, timers, effects, and dynamic modes are removed when the package unloads.

Public API services

IModContext supplies the following services:

Service Use
Log Namespaced server log messages.
Commands Chat, local console, server console, and RCON commands with aliases, argument limits, and permissions.
Players Immutable player snapshots and selectors such as exact name, unique partial name, Steam ID, client ID, @me, and @random.
Messages Broadcast or private chat, escaped colored segments, and major or minor HUD messages.
Menus Server-owned numeric menus and shared VMod Admin entries.
Effects Health, damage, slap, slay, timed damage or movement multipliers, weapon restrictions, and screen shake.
Audio Play addon-owned audio globally, for one player, or at a world position.
Admin Permission checks, immunity-aware targeting, kick, ban, and unban for admin packages.
GameMode Read mode state. The active gameMode package can change teams, respawn players, and end rounds or matches.
Events Player, combat, round, match, and mode-change subscriptions.
Scheduler One-shot and repeating callbacks.
Clock and Random Monotonic or Unix time and a per-VMod random source.
Storage Sandboxed typed key/value data under the VMod's state directory.
VSig Emit or subscribe to bounded VSig signals.
Mods Inspect loaded packages. Administrative VMods can request reloads.

ModPlayer and other API objects are snapshots. Keep the stable player ID and call Players.Find again when current state matters.

Events and timers

public void OnLoad(IModContext context)
{
    _context = context;

    var subscription = context.Events.Subscribe(
        ModEventKind.PlayerSpawned,
        "OnPlayerSpawned");
    if (!subscription.Success)
        throw new InvalidOperationException(subscription.Message);

    context.Scheduler.Once(5f, "OnTimer", "startup");
}

public void OnPlayerSpawned(ModEvent value)
{
    if (value.Player != null)
        _context.Messages.Tell(value.Player.Id, "Welcome back.");
}

public void OnTimer(ModTimerInvocation value)
{
    _context.Log.Write(value.TimerId + " fired with state " + value.State);
}

Combat events put the victim in Player and a connected attacker or killer in OtherPlayer. Read Amount, HealthAfter, WeaponId, AbilityId, WasHeadshot, WasCritical, and WasSuicide for scalar event data.

Timers have a 50 ms minimum interval, a 24-hour maximum delay, and a limit of 256 timers per VMod. Cancel repeating timers when their owning activity ends.

Permissions

A permission on ModCommandSpec.Permission must also appear in mod.json. The manifest declaration allows the VMod to ask for the permission; server admin configuration decides who has it.

VMod Admin reads groups and users from <data>/config/mods. A wildcard such as admin.* grants one namespace. A leading minus sign is an explicit deny and wins over grants. Immunity prevents one administrator from targeting another administrator with the same or higher value.

Do not put an RCON password or another secret in VMod source, manifests, or addon configuration.

Send signals to VSig

Every VMod owns a source alias named mod:<mod-id>.

_context.VSig.Emit("round_started", _context.GameMode.ActiveId);
_context.VSig.EmitFrom(
    "mod:community.hello.stage",
    "opened",
    3,
    "sewer_gate");

A custom source must be the owned alias or a child below it. Payloads can contain at most eight null, string, Boolean, or numeric values. Send a 64-bit identifier such as a Steam ID as a string.

A VMod can subscribe to a map signal:

context.VSig.Subscribe("escape_trigger", "on_executed", "OnEscape");

public void OnEscape(ModVSigEvent value)
{
    _context.Log.Write(value.SourceAlias + "." + value.OutputName);
}

See Signals shared with VMod for the map side.

Supported C# subset

VMods can use ordinary control flow, classes, structs, enums, arrays, strings, StringBuilder, and approved System.Collections and System.Collections.Generic shapes.

The validator rejects file and network I/O, reflection, processes, console or environment access, threads, tasks, Unity or Grimwar internals, native interop, unsafe code, typeof, async methods, iterators, lambdas, LINQ queries, try/catch/finally, using, lock, foreach, precompiled DLLs, and extra assembly references. Use indexed for loops and explicit cleanup.

Compilation alone does not guarantee that an arbitrary generic combination exists in the IL2CPP host. Start with arrays, List<T>, Dictionary<string, T>, and HashSet<string> using types present in the public API and bundled examples.

Install and test a VMod

  1. Run vmod_runtime_paths through an authenticated server console or RCON session. Use the printed addon path instead of guessing a platform-specific data directory.
  2. Copy the complete addon folder into <data>/addons.
  3. Restart the server, or run vmod_runtime_reload while no scripted game mode is active.
  4. Run vmod_runtime_list and confirm the package is loaded and not faulted.
  5. Exercise every chat and console alias with empty, quoted, invalid, and maximum-length arguments.
  6. Run the test again with a non-admin player when the VMod declares permissions.
  7. Reload it more than once and check that handlers, timers, commands, and effects do not duplicate.

Useful server commands:

Command Result
vmod_runtime_list Lists loaded and faulted VMods.
vmod_runtime_reload Refreshes addons and permissions, recompiles changed source, and reloads in dependency order.
vmod_runtime_reload_admins Reloads admin groups and users without unloading VMods.
vmod_runtime_paths Prints addon, config, state, and compiler-cache roots.
sv_vmod_enabled 0 Disables the VMod runtime at startup.

The server refuses vmod_runtime_reload while a scripted game mode is active. Change to a native mode first.

Distribution rules

VMod-only packages are currently installed manually or through the development overlay. Steam Workshop publishing accepts map-only Release packages and rejects packages that contain VMods. Server-only VMod source is not installed on remote players.

Use --addon-root <absolute-folder> or GRIMWAR_ADDON_ROOT during development to load a package directly. For ordinary installation, copy the complete folder containing addon.json; a loose .csx file is never an installable VMod.

Bundled examples are available in Grimwar's StreamingAssets/Grimwar/Addons source tree. grimwar.rtd is the smallest command and effect example. grimwar.admin covers permissions and menus. grimwar.zombie_mod is a scripted mode. grimwar.quake_sounds combines server logic with addon-owned audio.