Grimwar/VMod
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
- Run
vmod_runtime_pathsthrough an authenticated server console or RCON session. Use the printed addon path instead of guessing a platform-specific data directory. - Copy the complete addon folder into
<data>/addons. - Restart the server, or run
vmod_runtime_reloadwhile no scripted game mode is active. - Run
vmod_runtime_listand confirm the package is loaded and not faulted. - Exercise every chat and console alias with empty, quoted, invalid, and maximum-length arguments.
- Run the test again with a non-admin player when the VMod declares permissions.
- 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.