Grimwar/VMod reference
This page covers VMod services and larger scripting tasks. Start with the working VMod example. New modules target API 15; the current runtime accepts APIs 11 through 15.
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 administration entries. |
Effects |
Health, damage, slap, slay, timed damage, movement, and player-scale effects, weapon restrictions, and screen shake. |
Audio |
Play add-on-owned audio globally, for one listener, at a world position, or from a player's authoritative pawn; apply bounded gain and stop returned playback IDs. |
Content |
Describe declared assets and read supported text/data from owned or required packages. |
Panels |
Create supported game panels through the public API. |
Status |
Set or clear bounded, expiring, per-player HUD slots owned by this VMod. |
Spawn |
Let only the active scripted game mode return a living participant to a host-selected legal team spawn. |
Bots |
Let only the active scripted game mode create, inspect, and remove its own server bots at host-selected legal spawns. |
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, award personal objective points, 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. |
Configuration |
Bounded operator-editable JSON owned by this VMod under <data>/config/mods/<mod-id>.json.
|
Storage |
Sandboxed typed key/value data under the VMod's state directory. |
VSig |
Emit or subscribe to bounded VSig signals and resolve callback-scoped participant snapshots. |
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.
Operator configuration
Use Configuration for operator policy and settings. Use Storage for mutable runtime state that the VMod owns. Each VMod can access only its own configuration file.
Configuration paths contain 1 through 16 dot-separated segments. Values may be strings, finite numbers, Booleans, nulls, or scalar arrays. Read typed values with methods such as GetString, GetInt, and GetBool. Write documented defaults with SetString, SetInt, or SetBool, then call Save.
Validate settings before applying them. If Reload fails, keep the last valid values and report LastError. Do not duplicate operator settings in mod.json or VMod source.
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);
}
Scheduler reference
| Method | Contract |
|---|---|
Scheduler.Once(delaySeconds, handlerMethod, state = null) |
Schedule one callback and return its opaque timer ID. |
Scheduler.Repeat(intervalSeconds, handlerMethod, state = null) |
Schedule callbacks until cancellation or gameplay-scope cleanup and return the timer ID. |
Scheduler.Cancel(timerId) |
Remove an active timer. Returns false for an empty, unknown, completed, or already-cancelled ID.
|
The handler must be a public invokable method with exactly one ModTimerInvocation parameter. An empty timer ID means scheduling failed because the gameplay scope is inactive, the handler signature is invalid, the delay is not finite, or the VMod already owns 256 timers. Store and test the returned ID when later cancellation matters.
ModTimerInvocation.TimerId is the same opaque ID returned by the scheduler. State is the optional string captured when the timer was created; the host truncates it to 4,096 characters. InvocationCount starts at 1 and increases for each repeat callback.
Delays and intervals are clamped to 50 ms through 24 hours. Repeating timers deliver at most one callback when checked and schedule their next callback at least 50 ms in the future, so a stalled server does not dispatch a burst of catch-up invocations. Cancel can be called from the repeating timer's own handler:
private string _pulseTimerId;
public void StartPulse()
{
_pulseTimerId = _context.Scheduler.Repeat(1f, "OnPulse", "round-pulse");
if (_pulseTimerId.Length == 0)
_context.Log.Warn("Could not start the pulse timer.");
}
public void OnPulse(ModTimerInvocation value)
{
_context.Messages.Broadcast("Pulse " + value.InvocationCount);
if (value.InvocationCount >= 10)
{
_context.Scheduler.Cancel(value.TimerId);
_pulseTimerId = null;
}
}
Combat events put the victim in Player and a connected attacker or killer in OtherPlayer. Read Amount, HealthAfter, WeaponId, AbilityId, DamageSourceKind, DamageSourceId, WasHeadshot, WasCritical, WasAirShot, WasDomination, WasRevenge, WasMelee, and WasSuicide for event data.
MatchStarted fires when the host creates a match. RoundPreparing fires on entry to pre-round. RoundStarted fires when play becomes live and fires again for overtime. Round and match end events include WinningTeam.
The host clears timers when the gameplay scope deactivates or the VMod unloads. Cancel repeating timers earlier when their owning round, objective, or activity ends.
Gameplay presentation and provenance
Status.Set(playerId, slotId, text, duration) creates or replaces a VMod-owned logical HUD slot for one connected client and returns a clearable lease ID. The host sanitizes text, bounds duration and slot counts, expires state on both sides, and removes it on disconnect or unload.
Spawn.ReturnToSpawn(playerId) is deliberately narrower than transform access. Only the active scripted game-mode owner may call it, only for a living participant, and the native mode chooses a legal spawn through its spawn policy. The host clears carried velocity, rate-limits each participant, and removes cooldown state on disconnect.
Audio.PlayFromPlayer resolves the named player's current authoritative pawn position instead of accepting coordinates from interpreted code. Optional playback gain is bounded from 0 through 1 and multiplies the cue's catalog gain.
Scripted bots
Only the VMod that owns the active scripted game mode can spawn bots. The host validates the team and class, chooses an enabled spawn, and returns the stable bot player ID in ModOperationResult.Value. VMods cannot provide transforms or client-authored coordinates.
var result = _context.Bots.Spawn(new ModBotSpawnSpec
{
Team = "Union",
ClassId = "scamp",
DisplayName = "Hunter",
Behavior = ModBotBehavior.HuntEnemies
});
if (!result.Success)
_context.Log.Warn(result.Message);
Standard uses the normal objective-aware bot policy. HuntEnemies pursues enemy participants across the map and does not retreat because of low health.
Use Bots.GetOwned() to read current snapshots, Bots.Despawn(playerId) to remove one owned bot, and Bots.DespawnAll() to remove all bots owned by the VMod. A VMod cannot inspect or remove bots owned by the host or another package.
One VMod can own up to 64 bots, with a server-wide limit of 128.
The first scripted spawn disables the automatic bot quota and removes automatic quota bots for that gameplay scope. An active scripted game mode may call Bots.DespawnAll() during setup to claim the scope before its first spawn. This prevents automatic bots from appearing during pre-round or intermission phases.
Grimwar removes scripted bots and restores the previous quota when the mode changes, the gameplay scope ends, or the package unloads. If a spawn claimed the scope and then failed, Grimwar restores the previous quota. A prior Bots.DespawnAll() claim remains active until the gameplay scope ends.
Define a game mode
A gameMode package declares its mode in mod.json. The mode ID must match the VMod ID.
{
"gameMode": {
"id": "community.outbreak",
"name": "Outbreak",
"mapPrefix": "ob",
"aliases": ["outbreak"],
"teamNames": {
"union": "INFECTED",
"order": "SURVIVORS"
},
"teamWeaponSlots": {
"union": "melee",
"order": "all"
},
"disabledMovementFeatures": "grapple",
"defaultClassId": "klepper",
"automaticTeamBalance": false,
"infiniteReserveAmmo": false,
"warmupSeconds": 15,
"preRoundSeconds": 5,
"roundSeconds": 600,
"overtimeSeconds": 120,
"postRoundSeconds": 10,
"respawnWaveSeconds": 10,
"minimumPlayers": 2,
"winLimit": 1,
"maximumRounds": 0
}
}
The native mode host owns phase timing, readiness, scores, respawn waves, and synchronized match state. The active VMod can assign teams, respawn players, award personal objective points, and end rounds or matches through GameMode. Other VMods cannot use those mutations.
The entry class can also implement OnGameModeStart(), OnGameModeTick(float), OnGameModePhaseChanged(string, float), OnGameModeTimeExpired(), OnGameModeDamage(ModDamageRequest), and OnGameModeDispose(). Listen servers and community dedicated servers support scripted modes. Official servers do not.
OnGameModeDamage receives a mutable request before the host applies damage. The mode can change Amount, KnockbackScale, or MaximumKnockback, or set Cancelled to true.
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.
An administration VMod reads admin-groups.json and admins.json from <data>/config/mods. The server creates standard groups and an empty user list when those files do not exist. 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 add-on configuration.
Add custom audio and data
Use the VMod's Content controls to import owned media or reference a shared pack. API 15 provides Content.Describe and Content.TryReadText for declared text/data, alongside Audio playback. Custom content includes examples and dependency rules.
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 and resolve the participant snapshot emitted by trigger_multiple.executed:
context.VSig.Subscribe("extraction_zone", "executed", "OnExtraction");
public void OnExtraction(ModVSigEvent value)
{
if (value.Values.Length == 0)
return;
var token = value.Values[0] as string;
var occupants = _context.VSig.GetParticipantSnapshot(token);
var playerIds = new string[occupants.Length];
for (var i = 0; i < occupants.Length; i++)
playerIds[i] = occupants[i].Id;
if (playerIds.Length > 0)
_context.GameMode.AwardObjective("extraction", playerIds);
}
See Signals shared with VMod for the map side.
trigger_multiple.execute captures the participants currently inside the volume and emits an opaque token. VSig.GetParticipantSnapshot resolves that token only during the matching synchronous callback. Expired, malformed, and unknown tokens return an empty array. The host deduplicates participants, limits a snapshot to 256 entries, and exposes immutable ModPlayer data instead of colliders or live Unity objects.
GameMode.AwardObjective(objectiveId, playerIds) gives one personal objective point to each distinct resolved participant without changing team score. The call accepts at most 256 IDs, rejects the complete request if any ID is unknown, requires an active round, and is restricted to the VMod that owns the active scripted game mode.
Entity activator, caller, and self handles remain inside the host. Other VMod subscriptions receive only validated scalar ModVSigEvent data.
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, resource-scoped using statements, 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 documented public API.
Runtime limits
A server accepts at most 128 VMods. Each package can list up to 64 source files, 64 dependencies, and 256 declared permissions. One source file can be up to 512 KiB; combined source can be up to 2 MiB.
Each callback has a 100 ms time limit and a 100,000-iteration budget. Five callback failures fault the package and unload its dependents. Storage accepts up to 1,024 keys with 65,536 characters in each value. VMods still run inside the server process, so install code only from sources you trust.
Install and test a VMod
- Build a Development package through VSDK's standalone resource controls.
- Point
--addon-root,GRIMWAR_ADDON_ROOT, or the Unity Editor Add-on Overlay at the folder that contains your package folders. - Run
vmod_runtime_pathsthrough an authenticated server console or RCON session. Use the printed add-on path instead of guessing a platform-specific data directory. - For a manual install, copy the complete add-on folder into
<data>/addons. - Restart the server, or run
vmod_runtime_reloadwhile no scripted game mode or map-bound VMod 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 add-ons and permissions, recompiles changed source, and reloads in dependency order. |
vmod_runtime_reload_host_config |
Reloads admin groups, users, and VMod configuration without unloading VMods. |
vmod_runtime_paths |
Prints add-on, 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 or map-bound VMod is active. Unload that map and change to a native mode first.
Distribute a VMod
In VSDK, enable Release package before building a standalone VMod. Set thumbnail in mod.json to a relative 1920 × 1080 PNG. Install the complete generated add-on folder on the server or publish it as VMod through Grimwar's Addons screen.
Publish map-bound scripts as part of the map's Release package using the Map upload type. The source snapshot stays inside the scene and is covered by its compilation receipt. Clients carry those bytes without executing them. Rebuild and republish the map whenever its attached source changes.
See Steam Workshop publishing and Dedicated servers for installation and dependencies.