<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://developer.vellocetsoftware.com/index.php?action=history&amp;feed=atom&amp;title=Grimwar%2FVMod_reference</id>
	<title>Grimwar/VMod reference - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://developer.vellocetsoftware.com/index.php?action=history&amp;feed=atom&amp;title=Grimwar%2FVMod_reference"/>
	<link rel="alternate" type="text/html" href="https://developer.vellocetsoftware.com/w/index.php?title=Grimwar/VMod_reference&amp;action=history"/>
	<updated>2026-10-21T20:37:03Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://developer.vellocetsoftware.com/w/index.php?title=Grimwar/VMod_reference&amp;diff=153&amp;oldid=prev</id>
		<title>DocumentationBot: Version 1</title>
		<link rel="alternate" type="text/html" href="https://developer.vellocetsoftware.com/w/index.php?title=Grimwar/VMod_reference&amp;diff=153&amp;oldid=prev"/>
		<updated>2026-10-01T04:13:05Z</updated>

		<summary type="html">&lt;p&gt;Version 1&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{GameNav}}&lt;br /&gt;
&lt;br /&gt;
This page covers VMod services and larger scripting tasks. Start with [[Grimwar/VMod|the working VMod example]]. New modules target API 15; the current runtime accepts APIs 11 through 15.&lt;br /&gt;
&lt;br /&gt;
== Public API services ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;IModContext&amp;lt;/code&amp;gt; supplies the following services:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Service !! Use&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Log&amp;lt;/code&amp;gt; || Namespaced server log messages.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Commands&amp;lt;/code&amp;gt; || Chat, local console, server console, and RCON commands with aliases, argument limits, and permissions.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Players&amp;lt;/code&amp;gt; || Immutable player snapshots and selectors such as exact name, unique partial name, Steam ID, client ID, &amp;lt;code&amp;gt;@me&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;@random&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Messages&amp;lt;/code&amp;gt; || Broadcast or private chat, escaped colored segments, and major or minor HUD messages.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Menus&amp;lt;/code&amp;gt; || Server-owned numeric menus and shared administration entries.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Effects&amp;lt;/code&amp;gt; || Health, damage, slap, slay, timed damage, movement, and player-scale effects, weapon restrictions, and screen shake.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Audio&amp;lt;/code&amp;gt; || Play add-on-owned audio globally, for one listener, at a world position, or from a player&amp;#039;s authoritative pawn; apply bounded gain and stop returned playback IDs.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content&amp;lt;/code&amp;gt; || Describe declared assets and read supported text/data from owned or required packages.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Panels&amp;lt;/code&amp;gt; || Create supported game panels through the public API.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Status&amp;lt;/code&amp;gt; || Set or clear bounded, expiring, per-player HUD slots owned by this VMod.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Spawn&amp;lt;/code&amp;gt; || Let only the active scripted game mode return a living participant to a host-selected legal team spawn.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Bots&amp;lt;/code&amp;gt; || Let only the active scripted game mode create, inspect, and remove its own server bots at host-selected legal spawns.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Admin&amp;lt;/code&amp;gt; || Permission checks, immunity-aware targeting, kick, ban, and unban for &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; packages.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GameMode&amp;lt;/code&amp;gt; || Read mode state. The active &amp;lt;code&amp;gt;gameMode&amp;lt;/code&amp;gt; package can change teams, respawn players, award personal objective points, and end rounds or matches.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Events&amp;lt;/code&amp;gt; || Player, combat, round, match, and mode-change subscriptions.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Scheduler&amp;lt;/code&amp;gt; || One-shot and repeating callbacks.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Clock&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Random&amp;lt;/code&amp;gt; || Monotonic or Unix time and a per-VMod random source.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Configuration&amp;lt;/code&amp;gt; || Bounded operator-editable JSON owned by this VMod under &amp;lt;code&amp;gt;&amp;amp;lt;data&amp;amp;gt;/config/mods/&amp;amp;lt;mod-id&amp;amp;gt;.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Storage&amp;lt;/code&amp;gt; || Sandboxed typed key/value data under the VMod&amp;#039;s state directory.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;VSig&amp;lt;/code&amp;gt; || Emit or subscribe to bounded VSig signals and resolve callback-scoped participant snapshots.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Mods&amp;lt;/code&amp;gt; || Inspect loaded packages. Administrative VMods can request reloads.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;ModPlayer&amp;lt;/code&amp;gt; and other API objects are snapshots. Keep the stable player ID and call &amp;lt;code&amp;gt;Players.Find&amp;lt;/code&amp;gt; again when current state matters.&lt;br /&gt;
&lt;br /&gt;
== Operator configuration ==&lt;br /&gt;
&lt;br /&gt;
Use &amp;lt;code&amp;gt;Configuration&amp;lt;/code&amp;gt; for operator policy and settings. Use &amp;lt;code&amp;gt;Storage&amp;lt;/code&amp;gt; for mutable runtime state that the VMod owns. Each VMod can access only its own configuration file.&lt;br /&gt;
&lt;br /&gt;
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 &amp;lt;code&amp;gt;GetString&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;GetInt&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;GetBool&amp;lt;/code&amp;gt;. Write documented defaults with &amp;lt;code&amp;gt;SetString&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SetInt&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;SetBool&amp;lt;/code&amp;gt;, then call &amp;lt;code&amp;gt;Save&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Validate settings before applying them. If &amp;lt;code&amp;gt;Reload&amp;lt;/code&amp;gt; fails, keep the last valid values and report &amp;lt;code&amp;gt;LastError&amp;lt;/code&amp;gt;. Do not duplicate operator settings in &amp;lt;code&amp;gt;mod.json&amp;lt;/code&amp;gt; or VMod source.&lt;br /&gt;
&lt;br /&gt;
== Events and timers ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
public void OnLoad(IModContext context)&lt;br /&gt;
{&lt;br /&gt;
    _context = context;&lt;br /&gt;
&lt;br /&gt;
    var subscription = context.Events.Subscribe(&lt;br /&gt;
        ModEventKind.PlayerSpawned,&lt;br /&gt;
        &amp;quot;OnPlayerSpawned&amp;quot;);&lt;br /&gt;
    if (!subscription.Success)&lt;br /&gt;
        throw new InvalidOperationException(subscription.Message);&lt;br /&gt;
&lt;br /&gt;
    context.Scheduler.Once(5f, &amp;quot;OnTimer&amp;quot;, &amp;quot;startup&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public void OnPlayerSpawned(ModEvent value)&lt;br /&gt;
{&lt;br /&gt;
    if (value.Player != null)&lt;br /&gt;
        _context.Messages.Tell(value.Player.Id, &amp;quot;Welcome back.&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public void OnTimer(ModTimerInvocation value)&lt;br /&gt;
{&lt;br /&gt;
    _context.Log.Write(value.TimerId + &amp;quot; fired with state &amp;quot; + value.State);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Scheduler reference ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Method !! Contract&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Scheduler.Once(delaySeconds, handlerMethod, state = null)&amp;lt;/code&amp;gt; || Schedule one callback and return its opaque timer ID.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Scheduler.Repeat(intervalSeconds, handlerMethod, state = null)&amp;lt;/code&amp;gt; || Schedule callbacks until cancellation or gameplay-scope cleanup and return the timer ID.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Scheduler.Cancel(timerId)&amp;lt;/code&amp;gt; || Remove an active timer. Returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; for an empty, unknown, completed, or already-cancelled ID.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The handler must be a public invokable method with exactly one &amp;lt;code&amp;gt;ModTimerInvocation&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;ModTimerInvocation.TimerId&amp;lt;/code&amp;gt; is the same opaque ID returned by the scheduler. &amp;lt;code&amp;gt;State&amp;lt;/code&amp;gt; is the optional string captured when the timer was created; the host truncates it to 4,096 characters. &amp;lt;code&amp;gt;InvocationCount&amp;lt;/code&amp;gt; starts at 1 and increases for each repeat callback.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;code&amp;gt;Cancel&amp;lt;/code&amp;gt; can be called from the repeating timer&amp;#039;s own handler:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
private string _pulseTimerId;&lt;br /&gt;
&lt;br /&gt;
public void StartPulse()&lt;br /&gt;
{&lt;br /&gt;
    _pulseTimerId = _context.Scheduler.Repeat(1f, &amp;quot;OnPulse&amp;quot;, &amp;quot;round-pulse&amp;quot;);&lt;br /&gt;
    if (_pulseTimerId.Length == 0)&lt;br /&gt;
        _context.Log.Warn(&amp;quot;Could not start the pulse timer.&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
public void OnPulse(ModTimerInvocation value)&lt;br /&gt;
{&lt;br /&gt;
    _context.Messages.Broadcast(&amp;quot;Pulse &amp;quot; + value.InvocationCount);&lt;br /&gt;
    if (value.InvocationCount &amp;gt;= 10)&lt;br /&gt;
    {&lt;br /&gt;
        _context.Scheduler.Cancel(value.TimerId);&lt;br /&gt;
        _pulseTimerId = null;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Combat events put the victim in &amp;lt;code&amp;gt;Player&amp;lt;/code&amp;gt; and a connected attacker or killer in &amp;lt;code&amp;gt;OtherPlayer&amp;lt;/code&amp;gt;. Read &amp;lt;code&amp;gt;Amount&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;HealthAfter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WeaponId&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AbilityId&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DamageSourceKind&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DamageSourceId&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasHeadshot&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasCritical&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasAirShot&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasDomination&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasRevenge&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WasMelee&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;WasSuicide&amp;lt;/code&amp;gt; for event data.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;MatchStarted&amp;lt;/code&amp;gt; fires when the host creates a match. &amp;lt;code&amp;gt;RoundPreparing&amp;lt;/code&amp;gt; fires on entry to pre-round. &amp;lt;code&amp;gt;RoundStarted&amp;lt;/code&amp;gt; fires when play becomes live and fires again for overtime. Round and match end events include &amp;lt;code&amp;gt;WinningTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Gameplay presentation and provenance ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Status.Set(playerId, slotId, text, duration)&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Spawn.ReturnToSpawn(playerId)&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Audio.PlayFromPlayer&amp;lt;/code&amp;gt; resolves the named player&amp;#039;s current authoritative pawn position instead of accepting coordinates from interpreted code. Optional playback gain is bounded from &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; and multiplies the cue&amp;#039;s catalog gain.&lt;br /&gt;
&lt;br /&gt;
== Scripted bots ==&lt;br /&gt;
&lt;br /&gt;
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 &amp;lt;code&amp;gt;ModOperationResult.Value&amp;lt;/code&amp;gt;. VMods cannot provide transforms or client-authored coordinates.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
var result = _context.Bots.Spawn(new ModBotSpawnSpec&lt;br /&gt;
{&lt;br /&gt;
    Team = &amp;quot;Union&amp;quot;,&lt;br /&gt;
    ClassId = &amp;quot;scamp&amp;quot;,&lt;br /&gt;
    DisplayName = &amp;quot;Hunter&amp;quot;,&lt;br /&gt;
    Behavior = ModBotBehavior.HuntEnemies&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
if (!result.Success)&lt;br /&gt;
    _context.Log.Warn(result.Message);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Standard&amp;lt;/code&amp;gt; uses the normal objective-aware bot policy. &amp;lt;code&amp;gt;HuntEnemies&amp;lt;/code&amp;gt; pursues enemy participants across the map and does not retreat because of low health.&lt;br /&gt;
&lt;br /&gt;
Use &amp;lt;code&amp;gt;Bots.GetOwned()&amp;lt;/code&amp;gt; to read current snapshots, &amp;lt;code&amp;gt;Bots.Despawn(playerId)&amp;lt;/code&amp;gt; to remove one owned bot, and &amp;lt;code&amp;gt;Bots.DespawnAll()&amp;lt;/code&amp;gt; to remove all bots owned by the VMod. A VMod cannot inspect or remove bots owned by the host or another package.&lt;br /&gt;
&lt;br /&gt;
One VMod can own up to 64 bots, with a server-wide limit of 128.&lt;br /&gt;
&lt;br /&gt;
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 &amp;lt;code&amp;gt;Bots.DespawnAll()&amp;lt;/code&amp;gt; during setup to claim the scope before its first spawn. This prevents automatic bots from appearing during pre-round or intermission phases.&lt;br /&gt;
&lt;br /&gt;
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 &amp;lt;code&amp;gt;Bots.DespawnAll()&amp;lt;/code&amp;gt; claim remains active until the gameplay scope ends.&lt;br /&gt;
&lt;br /&gt;
== Define a game mode ==&lt;br /&gt;
&lt;br /&gt;
A &amp;lt;code&amp;gt;gameMode&amp;lt;/code&amp;gt; package declares its mode in &amp;lt;code&amp;gt;mod.json&amp;lt;/code&amp;gt;. The mode ID must match the VMod ID.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;gameMode&amp;quot;: {&lt;br /&gt;
    &amp;quot;id&amp;quot;: &amp;quot;community.outbreak&amp;quot;,&lt;br /&gt;
    &amp;quot;name&amp;quot;: &amp;quot;Outbreak&amp;quot;,&lt;br /&gt;
    &amp;quot;mapPrefix&amp;quot;: &amp;quot;ob&amp;quot;,&lt;br /&gt;
    &amp;quot;aliases&amp;quot;: [&amp;quot;outbreak&amp;quot;],&lt;br /&gt;
    &amp;quot;teamNames&amp;quot;: {&lt;br /&gt;
      &amp;quot;union&amp;quot;: &amp;quot;INFECTED&amp;quot;,&lt;br /&gt;
      &amp;quot;order&amp;quot;: &amp;quot;SURVIVORS&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
    &amp;quot;teamWeaponSlots&amp;quot;: {&lt;br /&gt;
      &amp;quot;union&amp;quot;: &amp;quot;melee&amp;quot;,&lt;br /&gt;
      &amp;quot;order&amp;quot;: &amp;quot;all&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
    &amp;quot;disabledMovementFeatures&amp;quot;: &amp;quot;grapple&amp;quot;,&lt;br /&gt;
    &amp;quot;defaultClassId&amp;quot;: &amp;quot;klepper&amp;quot;,&lt;br /&gt;
    &amp;quot;automaticTeamBalance&amp;quot;: false,&lt;br /&gt;
    &amp;quot;infiniteReserveAmmo&amp;quot;: false,&lt;br /&gt;
    &amp;quot;warmupSeconds&amp;quot;: 15,&lt;br /&gt;
    &amp;quot;preRoundSeconds&amp;quot;: 5,&lt;br /&gt;
    &amp;quot;roundSeconds&amp;quot;: 600,&lt;br /&gt;
    &amp;quot;overtimeSeconds&amp;quot;: 120,&lt;br /&gt;
    &amp;quot;postRoundSeconds&amp;quot;: 10,&lt;br /&gt;
    &amp;quot;respawnWaveSeconds&amp;quot;: 10,&lt;br /&gt;
    &amp;quot;minimumPlayers&amp;quot;: 2,&lt;br /&gt;
    &amp;quot;winLimit&amp;quot;: 1,&lt;br /&gt;
    &amp;quot;maximumRounds&amp;quot;: 0&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
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 &amp;lt;code&amp;gt;GameMode&amp;lt;/code&amp;gt;. Other VMods cannot use those mutations.&lt;br /&gt;
&lt;br /&gt;
The entry class can also implement &amp;lt;code&amp;gt;OnGameModeStart()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OnGameModeTick(float)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OnGameModePhaseChanged(string, float)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OnGameModeTimeExpired()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OnGameModeDamage(ModDamageRequest)&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;OnGameModeDispose()&amp;lt;/code&amp;gt;. Listen servers and community dedicated servers support scripted modes. Official servers do not.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OnGameModeDamage&amp;lt;/code&amp;gt; receives a mutable request before the host applies damage. The mode can change &amp;lt;code&amp;gt;Amount&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;KnockbackScale&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;MaximumKnockback&amp;lt;/code&amp;gt;, or set &amp;lt;code&amp;gt;Cancelled&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Permissions ==&lt;br /&gt;
&lt;br /&gt;
A permission on &amp;lt;code&amp;gt;ModCommandSpec.Permission&amp;lt;/code&amp;gt; must also appear in &amp;lt;code&amp;gt;mod.json&amp;lt;/code&amp;gt;. The manifest declaration allows the VMod to ask for the permission; server admin configuration decides who has it.&lt;br /&gt;
&lt;br /&gt;
An administration VMod reads &amp;lt;code&amp;gt;admin-groups.json&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;admins.json&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;&amp;amp;lt;data&amp;amp;gt;/config/mods&amp;lt;/code&amp;gt;. The server creates standard groups and an empty user list when those files do not exist. A wildcard such as &amp;lt;code&amp;gt;admin.*&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
Do not put an RCON password or another secret in VMod source, manifests, or add-on configuration.&lt;br /&gt;
&lt;br /&gt;
== Add custom audio and data ==&lt;br /&gt;
&lt;br /&gt;
Use the VMod&amp;#039;s Content controls to import owned media or reference a shared pack. API 15 provides &amp;lt;code&amp;gt;Content.Describe&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Content.TryReadText&amp;lt;/code&amp;gt; for declared text/data, alongside &amp;lt;code&amp;gt;Audio&amp;lt;/code&amp;gt; playback. [[Grimwar/Custom content#Use media in a VMod|Custom content]] includes examples and dependency rules.&lt;br /&gt;
&lt;br /&gt;
== Send signals to VSig ==&lt;br /&gt;
&lt;br /&gt;
Every VMod owns a source alias named &amp;lt;code&amp;gt;mod:&amp;amp;lt;mod-id&amp;amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
_context.VSig.Emit(&amp;quot;round_started&amp;quot;, _context.GameMode.ActiveId);&lt;br /&gt;
_context.VSig.EmitFrom(&lt;br /&gt;
    &amp;quot;mod:community.hello.stage&amp;quot;,&lt;br /&gt;
    &amp;quot;opened&amp;quot;,&lt;br /&gt;
    3,&lt;br /&gt;
    &amp;quot;sewer_gate&amp;quot;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
A VMod can subscribe to a map signal and resolve the participant snapshot emitted by &amp;lt;code&amp;gt;trigger_multiple.executed&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
context.VSig.Subscribe(&amp;quot;extraction_zone&amp;quot;, &amp;quot;executed&amp;quot;, &amp;quot;OnExtraction&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
public void OnExtraction(ModVSigEvent value)&lt;br /&gt;
{&lt;br /&gt;
    if (value.Values.Length == 0)&lt;br /&gt;
        return;&lt;br /&gt;
&lt;br /&gt;
    var token = value.Values[0] as string;&lt;br /&gt;
    var occupants = _context.VSig.GetParticipantSnapshot(token);&lt;br /&gt;
    var playerIds = new string[occupants.Length];&lt;br /&gt;
    for (var i = 0; i &amp;lt; occupants.Length; i++)&lt;br /&gt;
        playerIds[i] = occupants[i].Id;&lt;br /&gt;
&lt;br /&gt;
    if (playerIds.Length &amp;gt; 0)&lt;br /&gt;
        _context.GameMode.AwardObjective(&amp;quot;extraction&amp;quot;, playerIds);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
See [[Grimwar/VSig#Signals shared with VMod|Signals shared with VMod]] for the map side.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;trigger_multiple.execute&amp;lt;/code&amp;gt; captures the participants currently inside the volume and emits an opaque token. &amp;lt;code&amp;gt;VSig.GetParticipantSnapshot&amp;lt;/code&amp;gt; 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 &amp;lt;code&amp;gt;ModPlayer&amp;lt;/code&amp;gt; data instead of colliders or live Unity objects.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;GameMode.AwardObjective(objectiveId, playerIds)&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
Entity &amp;lt;code&amp;gt;activator&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;caller&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;self&amp;lt;/code&amp;gt; handles remain inside the host. Other VMod subscriptions receive only validated scalar &amp;lt;code&amp;gt;ModVSigEvent&amp;lt;/code&amp;gt; data.&lt;br /&gt;
&lt;br /&gt;
== Supported C# subset ==&lt;br /&gt;
&lt;br /&gt;
VMods can use ordinary control flow, classes, structs, enums, arrays, strings, &amp;lt;code&amp;gt;StringBuilder&amp;lt;/code&amp;gt;, and approved &amp;lt;code&amp;gt;System.Collections&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;System.Collections.Generic&amp;lt;/code&amp;gt; shapes.&lt;br /&gt;
&lt;br /&gt;
The validator rejects file and network I/O, reflection, processes, console or environment access, threads, tasks, Unity or Grimwar internals, native interop, unsafe code, &amp;lt;code&amp;gt;typeof&amp;lt;/code&amp;gt;, async methods, iterators, lambdas, LINQ queries, &amp;lt;code&amp;gt;try&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;catch&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;finally&amp;lt;/code&amp;gt;, resource-scoped &amp;lt;code&amp;gt;using&amp;lt;/code&amp;gt; statements, &amp;lt;code&amp;gt;lock&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;foreach&amp;lt;/code&amp;gt;, precompiled DLLs, and extra assembly references. Use indexed &amp;lt;code&amp;gt;for&amp;lt;/code&amp;gt; loops and explicit cleanup.&lt;br /&gt;
&lt;br /&gt;
Compilation alone does not guarantee that an arbitrary generic combination exists in the IL2CPP host. Start with arrays, &amp;lt;code&amp;gt;List&amp;amp;lt;T&amp;amp;gt;&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Dictionary&amp;amp;lt;string, T&amp;amp;gt;&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;HashSet&amp;amp;lt;string&amp;amp;gt;&amp;lt;/code&amp;gt; using types present in the documented public API.&lt;br /&gt;
&lt;br /&gt;
== Runtime limits ==&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
== Install and test a VMod ==&lt;br /&gt;
&lt;br /&gt;
# Build a Development package through VSDK&amp;#039;s standalone resource controls.&lt;br /&gt;
# Point &amp;lt;code&amp;gt;--addon-root&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;GRIMWAR_ADDON_ROOT&amp;lt;/code&amp;gt;, or the Unity Editor Add-on Overlay at the folder that contains your package folders.&lt;br /&gt;
# Run &amp;lt;code&amp;gt;vmod_runtime_paths&amp;lt;/code&amp;gt; through an authenticated server console or RCON session. Use the printed add-on path instead of guessing a platform-specific data directory.&lt;br /&gt;
# For a manual install, copy the complete add-on folder into &amp;lt;code&amp;gt;&amp;amp;lt;data&amp;amp;gt;/addons&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Restart the server, or run &amp;lt;code&amp;gt;vmod_runtime_reload&amp;lt;/code&amp;gt; while no scripted game mode or map-bound VMod is active.&lt;br /&gt;
# Run &amp;lt;code&amp;gt;vmod_runtime_list&amp;lt;/code&amp;gt; and confirm the package is loaded and not faulted.&lt;br /&gt;
# Exercise every chat and console alias with empty, quoted, invalid, and maximum-length arguments.&lt;br /&gt;
# Run the test again with a non-admin player when the VMod declares permissions.&lt;br /&gt;
# Reload it more than once and check that handlers, timers, commands, and effects do not duplicate.&lt;br /&gt;
&lt;br /&gt;
Useful server commands:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Command !! Result&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vmod_runtime_list&amp;lt;/code&amp;gt; || Lists loaded and faulted VMods.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vmod_runtime_reload&amp;lt;/code&amp;gt; || Refreshes add-ons and permissions, recompiles changed source, and reloads in dependency order.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vmod_runtime_reload_host_config&amp;lt;/code&amp;gt; || Reloads admin groups, users, and VMod configuration without unloading VMods.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;vmod_runtime_paths&amp;lt;/code&amp;gt; || Prints add-on, config, state, and compiler-cache roots.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;sv_vmod_enabled 0&amp;lt;/code&amp;gt; || Disables the VMod runtime at startup.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The server refuses &amp;lt;code&amp;gt;vmod_runtime_reload&amp;lt;/code&amp;gt; while a scripted game mode or map-bound VMod is active. Unload that map and change to a native mode first.&lt;br /&gt;
&lt;br /&gt;
== Distribute a VMod ==&lt;br /&gt;
&lt;br /&gt;
In VSDK, enable &amp;#039;&amp;#039;&amp;#039;Release package&amp;#039;&amp;#039;&amp;#039; before building a standalone VMod. Set &amp;lt;code&amp;gt;thumbnail&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;mod.json&amp;lt;/code&amp;gt; to a relative 1920 × 1080 PNG. Install the complete generated add-on folder on the server or publish it as &amp;#039;&amp;#039;&amp;#039;VMod&amp;#039;&amp;#039;&amp;#039; through Grimwar&amp;#039;s Addons screen.&lt;br /&gt;
&lt;br /&gt;
Publish map-bound scripts as part of the map&amp;#039;s Release package using the &amp;#039;&amp;#039;&amp;#039;Map&amp;#039;&amp;#039;&amp;#039; 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.&lt;br /&gt;
&lt;br /&gt;
See [[Grimwar/Steam Workshop addons|Steam Workshop publishing]] and [[Grimwar/Dedicated servers|Dedicated servers]] for installation and dependencies.&lt;br /&gt;
&lt;br /&gt;
[[Category:Grimwar]] [[Category:VMod]] [[Category:Server modding]]&lt;/div&gt;</summary>
		<author><name>DocumentationBot</name></author>
	</entry>
</feed>