Script Modding Guide (WIP)

Modding — Work In Progress
Write C# scripts that react to everything happening on a Holdfast server — and act back on it.
This is the expanded, community-verified edition of the Script Modding Guide. It merges the original guide with several years of verified findings from the modding community, SDK release notes, and cross-references to the Server Configuration Enums and Remote Console Commands pages. Content is under active revision — sections marked Verify still need confirmation against the current SDK.
LanguageC# (.NET Standard 2.1)
EngineUnity 2022.3.62f1 (SDK v2.31)
Mod SystemUMod 2.0 (Trivial Interactive)
DistributionSteam Workshop

How Script Modding Works

Script modding in Holdfast: Nations at War means writing C# scripts inside the official Unity SDK, building them into .umod files, and distributing them through the Steam Workshop. Servers load them with mods_installed / mods_installed_server_only entries in the server config.

The Architecture in One Paragraph

The modding API is event-driven and one-way. The game calls into your mod through the callback interfaces IHoldfastSharedMethods, IHoldfastSharedMethods2 and IHoldfastSharedMethods3 — you cannot call into, inject, or replace game code from the SDK. Your mod acts back on the game primarily by executing console (RC) commands, which since SDK v2.15 is done through the IHoldfastGame interface. There is no built-in networking for mod objects; client/server synchronisation must be done manually (see Client/Server Synchronisation).

Why mods work on PC but not consoles: Holdfast on PC runs Mono (just-in-time compilation), which is what makes runtime mod loading possible. Consoles use IL2CPP (ahead-of-time compilation), which cannot side-load assemblies — hence no mods on PS5/Xbox, and loading a script or map mod flips the server to Steam-only. See Mods and Consoles.

What the Mod System Actually Is

The underlying mod system is UMod 2.0 by Trivial Interactive (a Unity Asset Store product) — not uMod.org/Oxide. UMod has its own quirks and hard rules that shape how you must structure your code; read UMod Rules & Limitations before writing your first class, it will save you hours of confusing bugs.

What You Can Build

  • Server-side mods (stat trackers, auto-admins, chat filters, event automation, bot puppeteering) — loaded with mods_installed_server_only, clients never download them.
  • Client-side mods (custom UI, audio replacements, visual tweaks) — the whole client scene is modifiable: you can reparent or hide UI, disable renderers, attach objects to the camera, and more.
  • Hybrid mods — a server component and client component that talk to each other over quiet messages.

Getting Started

Basic knowledge of C# and Unity is required.

Install the Right Unity Version

The SDK currently targets Unity 2022.3.62f1 (the last LTS release of Unity 2022, required since SDK v2.31). Using a different version is the source of many build failures — check the SDK Version History if you are upgrading an old project.

Connecting Unity to Your Editor

Make sure your Unity Editor is connected to whatever you're using to write the script (e.g. Visual Studio Community).

In the Unity Editor, click Edit → Preferences → External Tools → External Script Editor → Visual Studio Community.

Restart both your Unity Editor and your Script Editor to make sure these changes take effect.

If Visual Studio is not linked to Unity, your script won't be compiled into the mod and you'll hit Failed to locate linked type at load time. See Troubleshooting.

Creating Your Mod

Create a new mod and name it whatever your script does.

In the Unity Editor click Holdfast SDK Tools → Create Empty Mod

In this mod's folder create a new script and name it what you named your mod.

Double click the script in your Unity Project to open it.

Your script must live inside the mod's build folder (the same folder tree as the mod scene). Scripts outside it are silently excluded from the build and fail with Failed to locate linked type.

Testing Workflow

There is no verified way to load a local .umod without going through the Workshop. The community-proven workflow is:

  • Publish a second, private "dev" workshop item that builds to the same target folder as your real mod. Test changes against the dev item without pushing updates to your live subscribers.
  • After uploading, the built .umod file is renamed to <workshopId>.umod in the target folder — your named build file "disappearing" is normal.
  • The SDK keeps automatic scene backups at %userprofile%\AppData\LocalLow\Anvil Game Studio\Holdfast NaW - SDK if you ever lose work.

Core Concepts

The Shared Interfaces

Your script implements one or more of the callback interfaces. Each interface has different callbacks but operates the same way: the game calls your implementation when the corresponding event happens.

IHoldfastGame, IHoldfastSharedMethods, IHoldfastSharedMethods2, IHoldfastSharedMethods3

One class per interface — important! Older guides (including the previous version of this page) showed one class implementing all three interfaces at once. This compiles, but UMod creates a separate instance of your class for each interface it implements — so instance fields are NOT shared between, say, your IHoldfastSharedMethods callbacks and your IHoldfastSharedMethods3 callbacks. State silently diverges.



Recommended pattern: implement one class per interface, and share state through a single app/state object created in OnRoundDetails (the first per-round event) that every interface class references. See Advanced Patterns & Recipes.

Example: Here's a mod called "LineFormer" that has the script "LineFormer.cs" which implements the IHoldfastSharedMethods interface.

How Your Class Gets Instantiated

You do not attach your interface class to a GameObject. UMod scans the mod assembly via reflection for any class implementing one of the interfaces and creates the instances itself. This is also why several UMod rules exist around constructors and statics — see UMod Rules & Limitations.

Client or Server, What Are Those?!

One of the most important concepts to understand while script modding Holdfast is the concept of "Client" and "Server".

The Server is the game server which handles all player verification data, networking, chat/VoIP, etc. The Client is either your own client, referred to as the "Owner/Local Player", or someone else, referred to as a "Proxy".

Your mod receives OnIsServer(bool) and OnIsClient(bool, ulong) early in its lifecycle — branch your logic there. The same mod assembly runs in both places unless you use the load types below.

Mod Install and Load Types

Similarly to the normal process of using the load_mod <steam_id> load type for mods, the following are available:

  • load_mod_server_only <steamid> — loads a mod specifically only on server side (such as a custom stats tracking or auto admin mod)
  • load_mod_client_only <steamid> — inversely only loads the mod on client side (such as a UI mod or an audio replacement mod)
  • mods_installed_server_only <steamid> — loads the mod purely server-side; clients don't download it at all

The advantage: for mods that the client does not need in order to join the server, clients won't need to install the mod on their end.

The load order for this feature is: first all the Common Mods are loaded, then the client/server specific ones. The internal order of each list depends on the order they were typed in the config file.

Note: Players can only load mods that the server allows on its list, so a mods_installed_client_only <steamid> doesn't make sense. If you're going to make a client-only loaded mod, it needs to be installed on both the client and the server. A server-only mod can never draw anything on a client's screen — its only channel to clients is (quiet) chat messages.

Mod Variables

Mods can receive variables through the PassConfigVariables() method. These variables are supplied in the server config file in the format:

  • mod_variable <string> — Global Scope (outside of the map rotations)
  • mod_variable_local <string> — Local Scope (inside the map rotation)

Global Scope variables will always be loaded BEFORE any local scope variables.

Server config mod variables are sent to all mods running on the server, and are received on both the server and the client when the mod loads. To prevent conflicts, prefix your variables. A common and effective pattern is modID:key:value, where modID is a unique identifier for your mod (like its Steam Workshop ID or a custom prefix).

Values must not contain #. The hash character is treated as a comment in the server config, so a value like a hex colour #ff7a00 will never arrive at your mod. Encode such values differently (e.g. ff7a00 without the hash).

Practical Example: Slay Players on Spawn

Part 1: The Mod Variable

mod_variable HardcoreMod:EnableKillOnSpawn:true
  1. HardcoreMod: This is the modID.
  2. EnableKillOnSpawn: This is the key.
  3. true: This is the value.

Part 2: Script Implementation

This script would parse this variable and use its value. This example shows how to safely check for your mod's specific variable and convert the text "true" into a boolean true.

Security Concerns & Restricted Namespaces

Security-related restrictions prevent a modder from gaining access to the server or client's machine (i.e. System.IO, System.Reflection, etc). These typically show up as an error on the server's console:

[UMod.Scripting.ScriptDomain]: Illegal reference to disallowed namespace: System.Reflection.

Server hosts can pass -allowRestrictedMods in the launch parameters of the server. This turns off the securities on the server machine ONLY, for ALL the mods. If you rent your servers, you will need to talk to your host provider to enable this. What this allows you to do is hook directly into IO operations and other restricted features (writing files, databases, website APIs, etc).

HTTP works without restricted mods. Unity's own WWW/WWWForm classes are not blocked — you can POST player stats to an external web API from a regular mod. What you cannot do is import external SDKs (Firebase, database drivers, etc.); requests must be hand-rolled, and System.IO stays blocked either way.

Gotchas

  • It's highly recommended that most actions a mod does to a player (healing, damaging, reviving, positioning, rotating, etc.) be deferred by a frame or a few milliseconds. If your code is logically sound but behaves oddly in-game, try delaying the action first.
  • Related: don't fire several RC commands on the same frame from a script — stagger them across frames.
  • Don't use static members inside interface-implementing classes — see UMod Rules & Limitations for why and for the workaround.

UMod Rules & Limitations

UMod instantiates and relinks your code itself, which comes with hard rules. Most "mysterious" script mod bugs trace back to one of these.

Instantiation Rules

  • One instance per interface implementation. UMod reflects over your assembly and news up one instance for each interface a class implements — two interfaces on one class means two separate instances with separate fields. Use one class per interface plus a shared state object (see singleton pattern).
  • No static members inside interface-implementing classes. This confuses the dynamic loader and causes crashes/undefined behaviour. Workaround: keep static state in a separate plain static class (e.g. a static FogState class read by a MonoBehaviour).
  • No constructor overloading. Multiple constructors on a mod class break UMod's relinker with System.Reflection.AmbiguousMatchException: Ambiguous match found at mod load. Abstract classes are fine.

Asset & Build Rules

  • ScriptableObjects don't work with UMod.
  • Resources/ folders are not included in the mod build. Workarounds: put shared assets in an extra scene inside the build folder (all scenes in the build folder are loaded), or reference them from a "database" component in the main scene.
  • Runtime Instantiate only works for assets already loaded at map load. Spawning brand-new asset types later fails — pre-place or pool a copy in the scene.
  • Mod-to-mod references don't work. Referencing another mod or using exported packages in UMod build options builds fine but does not load in game. No workaround is known.
  • Git repositories inside the SDK project can break UMod builds. Cause unknown; deleting and recreating .git has fixed it.

State & Lifetime Rules

  • Statics do NOT persist between rounds. Every mod load loads a separate assembly, so static variables reset on each map rotation. (Older community advice from 2021–2022 claimed statics survive rounds — that behaviour changed in a later UMod/game update and is no longer true.) For persistence patterns, see Event Lifecycle & State.
  • Interface classes are not MonoBehaviours — no Update(), no coroutines. Per-frame work can hang off OnUpdateSyncedTime/OnUpdateElapsedTime, or spawn a GameObject with your own MonoBehaviour (see coroutine pattern).

Event Lifecycle & State

Load & Round Start Order

When your mod loads, the first events arrive in this order:

  1. OnIsServer(bool) — first event fired; branch client/server logic here.
  2. OnIsClient(bool, ulong steamId)
  3. PassConfigVariables(string[]) — fired on both server and client.
  4. OnSyncValueState(int) / time events begin.
  5. OnRoundDetails(...) — fired at round start with map, factions, game mode. This is the standard place to construct your per-round state object.

Player Flow

On the server, a joining player produces this sequence:

OnPlayerConnected  →  (loading screen, picks faction + class)  →  OnPlayerJoined  →  OnPlayerSpawned
  • OnPlayerJoined gives you playerId, steamId, name, regimentTag, isBot.
  • OnPlayerSpawned gives you the player's GameObject.
  • Player tracking pattern: keep a Dictionary<int, PlayerInfo> keyed by playerId and join the data from both events.
  • Positions: read the playerObject transform from OnPlayerSpawned — it updates automatically as the player moves. Avoid OnPlayerPacket for position tracking; it is server-side only and unreliable (packet loss/order).

Round End & Persistence

The interfaces give no reliable "round ended for the mod" signal, and (as noted in UMod Rules) statics reset between rounds. The community-proven pattern for state that must survive map rotation:

  • Serialize your state (e.g. to a JSON string) onto a GameObject that survives the round change, and re-acquire it after the mod reloads.
  • Attach a small MonoBehaviour and use its OnDestroy() to detect the end of the round and trigger the save.
Mod unloading between rotations has been observed to misbehave (mods not cleanly unloading). If you see stale behaviour across rotations, design defensively: re-initialise everything in OnRoundDetails.

Executing Console Commands

Executing RC commands is how your mod acts on the game. There is one supported way to do it, and one deprecated legacy way you will still find in older mods and tutorials.

The Supported Way: IHoldfastGame

Since SDK v2.15, implement the IHoldfastGame interface, receive an IHoldfastGameMethods object in OnGameMethodsInitialized, and call ExecuteConsoleCommand(...) from anywhere in your mod. Full walkthrough in the IHoldfastGame reference section.

The Deprecated Way: Console InputField Injection Deprecated

Before v2.15, mods executed commands by finding the F1 console UI (Game Console Panel), grabbing its InputField, and invoking inputField.onEndEdit.Invoke(command).

Do not use this in new mods. Mods built against pre-2022 Unity that use this technique now throw MissingMethodException: UnityEngine.UI.InputField.get_onEndEdit() after the engine upgrade. The technique still works in freshly built mods, but IHoldfastGame.ExecuteConsoleCommand is the supported path and the community has migrated to it.

Rules of Thumb

  • Stagger commands — never fire several RC commands on the same frame.
  • Check the success/output of the command while developing; it's the fastest way to catch typos in command strings.
  • Anything an admin can do from the console, your mod can do — browse Remote Console Commands for the full command set.

RC Commands Specific for Modding Use

Make Carbon Player Bots Not Do Any Auto Input

If you plan on modifying bots' position or rotation, call the following to force bots to not auto move or rotate. Note: this is different than setting the carbonplayer forceInputAxis/forceInputRotation, since those lock ALL bots in the action, whereas this allows you to individually control each bot's facing rotation and movement.

rc carbonPlayers ignoreAutoControls <true/false>
Verify: bot inputAxis control has been observed to work without ignoreAutoControls in some tests. Which commands strictly require it is unconfirmed.

Bot Puppeteering

The carbonPlayers command group is the modder's bot-control toolkit (see Bot Commands for the full table, and run rc help carbonPlayers in-game for the live list). Highlights confirmed working from scripts:

  • rc carbonPlayers spawnSpecific <faction> <class> [name] [regimentTag] [uniformId] — spawn a specific bot.
  • inputAxis / inputRotation / forceInputAxis / forceInputRotation — drive bot movement and facing.
  • playerAction / switchWeapon / voicePhrase / equipCarryableItem / mountVehicle / officerOrder / sapperBuildItem — make bots act.
  • rc carbonPlayers dropAllWeapons <playerId> followed by rc carbonPlayers pickupWeapon <playerId> <weaponName> — confirmed working sequence to swap a bot's weapon.

Send Network Trajectory Info to Clients Without Showing It to Players

Similar to drawFirearmTrajectories, this sends trajectory data from server to client. Unlike the aforementioned command, this will NOT display it to the player, but the mod will still receive the data. Enable this only if you're using OnShotInfo on the client side; for server-side mods it isn't required.

rc set drawFirearmTrajectoriesInfo <true/false>

Quiet Messages: Server-to-Client Data Transfer

Quiet messages are a powerful tool for a server-side mod to send data to a client-side mod without showing anything in the player's chat. They are essential for any mod where the server needs to update a client's custom UI or trigger a client-side action.

The communication flow is simple:

  1. The server executes an rc command to send a string of data.
  2. The client receives that string from the OnTextMessage event.

Sending Data from the Server

Use one of the following commands:

rc serverAdmin quietPrivateMessage <player_id> <message>
rc serverAdmin quietBroadcastMessage <message>
Quiet private messages can't reach un-spawned players. quietPrivateMessage fails for players who haven't spawned yet — use quietBroadcastMessage for join-time state sync.

Receiving Data on the Client

Your client-side mod receives the data in the OnTextMessage method. It's crucial to filter these messages to ensure you only process the ones intended for your mod.

Because OnTextMessage can be used by many different mods simultaneously, you must create a unique message format to avoid conflicts. A highly recommended pattern is to prefix your messages with a unique ID, like: MOD_ID:COMMAND:VALUE.

Practical Example: Welcome Message to a Debug.Log

Part 1: The Server-Side Code (Sending the Message)

This code runs on the server. When a player spawns, it sends them a quiet message containing their own PlayerID.

Part 2: The Client-Side Code (Receiving the Message)

This code runs on every client. It listens for the specific welcome message and logs it.

Client/Server Synchronisation

There is no networking layer for mod objects — the game will not replicate your custom state between server and clients. Two proven approaches:

1. Message Sync

The server sends quietPrivateMessage/quietBroadcastMessage strings (see Quiet Messages above); clients parse them in OnTextMessage. Prefix every message MODID:COMMAND:VALUE so multiple mods don't collide. This is the workhorse for custom UI updates, mod-driven game states, and anything event-based.

2. Deterministic Sync

Drive everything from OnUpdateSyncedTime / OnUpdateTimeRemaining so every client independently computes the same state with zero messages. The classic recipe: map synced time to an Animator "progress" float to animate scene objects identically on every client. OnSyncValueState provides a shared random seed if your deterministic logic needs randomness (e.g. procedural placement).

Which to Use?

  • State changes driven by gameplay events (kills, captures, admin actions) → message sync.
  • Continuous, predictable motion or timed sequences → deterministic sync (no bandwidth, no ordering problems).
  • Remember: server-only mods (mods_installed_server_only) can only reach clients via (quiet) chat messages — clients never load the mod, so deterministic client logic isn't available.

IHoldfastGame

To execute RC commands from your script you must gain access to one of the game's core methods. This is accomplished by implementing the IHoldfastGame interface (added in SDK v2.15).

Implement the Interface

First, ensure your script is using the HoldfastBridge namespace. Your main class signature must then use IHoldfastGame.

Receive and Store the Game Methods

The IHoldfastGame interface requires you to implement the OnGameMethodsInitialized method. The game engine calls this method once during startup and provides an IHoldfastGameMethods object.

The best practice is to store this object where the rest of your mod can reach it. This makes the game methods accessible from anywhere in your mod's codebase without needing to pass references around. (Remember the UMod static rules: keep it on your shared state object or a separate plain static class, not as a static member of the interface class itself.)

Execute a Command

With the GameMethods stored, you can now call ExecuteConsoleCommand from any part of your mod.

void ExecuteConsoleCommand(string command, out string output, out bool success);
  1. command: The RC command string to execute.
  2. output: An out parameter that will be filled with any text the command returns. This is very useful for debugging.
  3. success: An out parameter that will be true if the command was recognized and executed successfully.
Verify — signature discrepancy: the v2.15 release notes describe the method as returning "whether the console command executed successfully, the output, and an optional parsing exception", and current working community code uses an out Exception overload. Check the interface in the current SDK DLL and use whichever signature your IDE resolves.

Practical Example: Toggling Mouse Lock for a UI Panel

This is a common use case. When you show a custom UI, you might want to unlock the mouse so the player can interact with it.

IHoldfastSharedMethods

The primary callback interface. If you hover over the interface's properties in Visual Studio, these can be found in the inspector too. If Visual Studio doesn't link the DLL correctly, use the raw interface sources on GitHub.

OnSyncValueState

The game will share an int when the mod loads. A random synchronized value (same on client and server), for example useful if you need a seed for a map generator.

void OnSyncValueState(int value);

OnUpdateSyncedTime

The game will share the current synchronized time (same on client and server) on every frame of the game.

void OnUpdateSyncedTime(double time);

OnUpdateElapsedTime

The game will return the seconds since the round started on every frame of the game.

void OnUpdateElapsedTime(float time);

OnUpdateTimeRemaining

The game will share the current remaining time on every frame of the game.

void OnUpdateTimeRemaining(float time);

OnIsServer

The game will return if the mod is loaded on a server. This is the first event your mod receives.

void OnIsServer(bool server);

OnIsClient

The game will return if the mod is loaded on a client; if it is, you'll retrieve the current player's steam id.

void OnIsClient(bool client, ulong steamId);

OnRoundDetails

The game will call this on round start with details regarding the current round. Recommended place to construct your per-round state. See the Faction and Game Mode enum tables.

void OnRoundDetails(int roundId, string serverName, string mapName, FactionCountry attackingFaction, FactionCountry defendingFaction, GameplayMode gameplayMode, GameType gameType);

PassConfigVariables

The game will call this once with all the config variables set in the server's config file. Note: you receive ALL variables (from every mod), so filter for your own prefix. Fired on both server and client.

void PassConfigVariables(string[] value);

OnPlayerJoined

The game will call this when a player or a bot joins the round.

void OnPlayerJoined(int playerId, ulong steamId, string name, string regimentTag, bool isBot);

OnPlayerLeft

The game will call this when a player leaves the round. Note: Bots cannot leave; they'll keep respawning once dead.

void OnPlayerLeft(int playerId);

OnPlayerSpawned

The game will call this when a player spawns in game. The playerObject is your handle for positions — its transform updates as the player moves. See Class enums.

void OnPlayerSpawned(int playerId, int spawnSectionId, FactionCountry playerFaction, PlayerClass playerClass, int uniformId, GameObject playerObject);

OnPlayerHurt

The game will call this when a player is hurt in game. Note: there is no attacker id — see Event Fine Print.

void OnPlayerHurt(int playerId, byte oldHp, byte newHp, EntityHealthChangedReason reason);

OnPlayerKilledPlayer

The game will call this when a player is killed by another player.

void OnPlayerKilledPlayer(int killerPlayerId, int victimPlayerId, EntityHealthChangedReason reason, string details);

OnScorableAction

The game will call this when a player receives any score. Useful fallback for catching kill-like events that OnPlayerKilledPlayer misses.

void OnScorableAction(int playerId, int score, ScorableActionType reason);

OnPlayerShoot

The game will call this when a player shoots his gun.

void OnPlayerShoot(int playerId, bool dryShot);

OnShotInfo

The game will call this when a bullet fires. Note: This requires rc set drawFirearmTrajectories true or rc set drawFirearmTrajectoriesInfo true.

void OnShotInfo(int playerId, int shotCount, Vector3[][] shotsPointsPositions, float[] trajectileDistances, float[] distanceFromFiringPositions, float[] horizontalDeviationAngles, float[] maxHorizontalDeviationAngles, float[] muzzleVelocities, float[] gravities, float[] damageHitBaseDamages, float[] damageRangeUnitValues, float[] damagePostTraitAndBuffValues, float[] totalDamages, Vector3[] hitPositions, Vector3[] hitDirections, int[] hitPlayerIds, int[] hitDamageableObjectIds, int[] hitShipIds, int[] hitVehicleIds);

OnPlayerBlock

The game will call this when a player successfully blocks another player.

void OnPlayerBlock(int attackingPlayerId, int defendingPlayerId);

OnPlayerMeleeStartSecondaryAttack

The game will call this when a player starts a secondary attack (shove).

void OnPlayerMeleeStartSecondaryAttack(int playerId);

OnPlayerWeaponSwitch

The game will call this when a player swaps their weapon. See Weapon enums.

void OnPlayerWeaponSwitch(int playerId, string weapon);

OnPlayerStartCarry

The game will call this when a player starts carrying an object. See Carryable Object Type enums (and the staleness notice).

void OnPlayerStartCarry(int playerId, CarryableObjectType carryableObject);

OnPlayerEndCarry

The game will call this when a player stops carrying an object.

void OnPlayerEndCarry(int playerId);

OnPlayerShout

The game will call this when a player shouts using the in-game voice commands (not VoIP). See Voice Phrases enums.

void OnPlayerShout(int playerId, CharacterVoicePhrase voicePhrase);

OnConsoleCommand

The game will call this when the console command is used. Note: This will only be called on the client, or on the server, that is executing the console command.

void OnConsoleCommand(string input, string output, bool success);

OnRCLogin

The game will call this when a player requests a remote console login. Note: This will be called on the client that is executing the request, and the server. Not other people.

void OnRCLogin(int playerId, string inputPassword, bool isLoggedIn);

OnRCCommand

The game will call this when a player requests a remote console command. Note: This will be called on the client that is doing the request, and the server. Not other people.

void OnRCCommand(int playerId, string input, string output, bool success);

OnTextMessage

The game will call this when a message is received in the chat system, including quiet messages.

void OnTextMessage(int playerId, TextChatChannel channel, string text);

OnAdminPlayerAction

The game will call this when an administrator does an admin action using the rc commands or the P menu.

void OnAdminPlayerAction(int playerId, int adminId, ServerAdminAction action, string reason);

OnDamageableObjectDamaged

The game will call this when an object is damaged.

void OnDamageableObjectDamaged(GameObject damageableObject, int damageableObjectId, int shipId, int oldHp, int newHp);

OnInteractableObjectInteraction

The game will call this when an object is interacted with.

void OnInteractableObjectInteraction(int playerId, int interactableObjectId, GameObject interactableObject, InteractionActivationType interactionActivationType, int nextActivationStateTransitionIndex);

OnEmplacementPlaced

The game will call this when an emplacement (sapper object) is initially placed. See Emplacement Type enums.

void OnEmplacementPlaced(int itemId, GameObject objectBuilt, EmplacementType emplacementType);

OnEmplacementConstructed

The game will call this when an emplacement (sapper object) is fully constructed.

void OnEmplacementConstructed(int itemId);

OnCapturePointCaptured

The game will call this when a capture point is fully captured.

void OnCapturePointCaptured(int capturePoint);

OnCapturePointOwnerChanged

The game will call this when a capture point changes owner.

void OnCapturePointOwnerChanged(int capturePoint, FactionCountry factionCountry);

OnCapturePointDataUpdated

The game will call this when capture data changes.

void OnCapturePointDataUpdated(int capturePoint, int defendingPlayerCount, int attackingPlayerCount);

OnBuffStart

The game will call this when a buff is applied to a player. Note: Buffs that already exist on a player may stack, so this call will be called multiple times even if a player already has the buff. See Buff Type enums.

void OnBuffStart(int playerId, BuffType buff);

OnBuffStop

The game will call this when a buff is removed from a player.

void OnBuffStop(int playerId, BuffType buff);

OnRoundEndFactionWinner

The game will call this when a faction vs faction round is over. Note: Most of them except for Army Deathmatch. See Faction Round Winner Reason enums.

void OnRoundEndFactionWinner(FactionCountry factionCountry, FactionRoundWinnerReason reason);

OnRoundEndPlayerWinner

The game will call this when a free for all round is over. Note: Only Army Deathmatch.

void OnRoundEndPlayerWinner(int playerId);

OnVehicleSpawned

The game will call this when a vehicle (horse) is spawned.

void OnVehicleSpawned(int vehicleId, FactionCountry vehicleFaction, PlayerClass vehicleClass, GameObject vehicleObject, int ownerPlayerId);

OnVehicleHurt

The game will call this when a vehicle (horse) is hurt.

void OnVehicleHurt(int vehicleId, byte oldHp, byte newHp, EntityHealthChangedReason reason);

OnPlayerKilledVehicle

The game will call this when a vehicle (horse) is killed by a player.

void OnPlayerKilledVehicle(int killerPlayerId, int victimVehicleId, EntityHealthChangedReason reason, string details);

OnShipSpawned

The game will call this when a ship is spawned. See Ship Type and Ship Names enums.

void OnShipSpawned(int shipId, GameObject shipObject, FactionCountry shipfaction, ShipType shipType, int shipName);

OnShipDamaged

The game will call this when a ship takes damage. Note: a ship's max HP is not exposed anywhere until this fires — infer it from oldHp on the first hit.

void OnShipDamaged(int shipId, int oldHp, int newHp);

IHoldfastSharedMethods2

OnPlayerPacket Server only

This will be called every time we get a packet from a user. Packets can arrive out of order or be lost — do not rely on this for position tracking (use OnPlayerSpawned's playerObject instead).

void OnPlayerPacket(int playerId, byte? instance, Vector3? ownerPosition, double? packetTimestamp, Vector2? ownerInputAxis, float? ownerRotationY, float? ownerPitch, float? ownerYaw, PlayerActions[] actionCollection, Vector3? cameraPosition, Vector3? cameraForward, ushort? shipID, bool swimming);

OnVehiclePacket Server only

This will be called every time we get a packet from a vehicle.

void OnVehiclePacket(int vehicleId, Vector2 inputAxis, bool shift, bool strafe, PlayerVehicleActions[] actionCollection);

OnOfficerOrderStart

This will be called every time we get an officer order. See High Command Order Type enums. Note: SDK v2.8 renamed OfficerOrderType to HighCommandOrderType, which broke older mods — make sure you're compiling against the current interface.

void OnOfficerOrderStart(int officerPlayerId, HighCommandOrderType officerOrderType, Vector3 orderPosition, float orderRotationY, int voicePhraseRandomIndex);

OnOfficerOrderStop

This will be called every time we get an officer order stop.

void OnOfficerOrderStop(int officerPlayerId, HighCommandOrderType officerOrderType);

IHoldfastSharedMethods3

OnStartSpectate

This will be called when a player starts spectating someone else.

void OnStartSpectate(int playerId, int spectatedPlayerId);

OnStopSpectate

This will be called when a player stops spectating.

void OnStopSpectate(int playerId, int spectatedPlayerId);

OnStartFreeflight

This will be called when a player starts using freeflight cam.

void OnStartFreeflight(int playerId);

OnStopFreeflight

This will be called when a player stops using freeflight cam.

void OnStopFreeflight(int playerId);

OnMeleeArenaRoundEndFactionWinner

This will be called on a melee arena round end depending on who wins the round.

void OnMeleeArenaRoundEndFactionWinner(int roundId, bool attackers);

OnPlayerConnected Server only

This will be called when a player connects to a server, before they pick a faction and class.

This is the normal flow: OnPlayerConnected → player goes to loading screen → player picks faction+class → OnPlayerJoined

isAutoAdmin is only true for auto-login (whitelisted) admins — it does not detect password-logged-in admins. See Event Fine Print for a client-side admin detection trick.

void OnPlayerConnected(int playerId, bool isAutoAdmin, string backendId);

OnPlayerDisconnected Server only

This will be called when a player leaves a server. Unlike OnPlayerLeft, this happens even for players that never spawned in.

void OnPlayerDisconnected(int playerId);

Event Fine Print

Community-verified semantics that the raw signatures don't tell you. Read these before designing kill trackers, stat mods, or anything that attributes damage.

Damage & Kill Attribution

  • OnPlayerHurt does not include the attacker — only OnPlayerKilledPlayer links attacker to victim.
  • Explosion/mortar splash kills only report the directly hit victim through OnPlayerKilledPlayer; other splash victims die without any attacker attribution.
  • Grapeshot cannon kills arrive with reason ShotByFirearm, not HitByCannonball.
  • OnScorableAction can catch kill-like events that OnPlayerKilledPlayer misses — use it as a fallback signal.

Positions & Packets

  • Prefer the playerObject transform from OnPlayerSpawned for positions; it updates automatically.
  • OnPlayerPacket is server-side only and unreliable (packet loss/ordering). Whether packetTimestamp is send-time or receive-time is undocumented.

Ships

  • There is no way to read a ship's max/base HP until it takes damage — infer it from oldHp in the first OnShipDamaged.
  • Player→ship association can tentatively be inferred from spawnSectionId in OnPlayerSpawned. Verify

Admins & Config

  • OnPlayerConnected's isAutoAdmin only flags whitelisted auto-login admins. To detect a password-logged-in admin from a client mod, have the client execute rc playerlist (an admin-only command) and check whether it succeeds.
  • PassConfigVariables fires on both server and client when the mod loads.

Messaging

  • quietPrivateMessage cannot reach players who haven't spawned yet; use quietBroadcastMessage for join-time sync.

SharedMethods Copy/Paste

All the SharedMethods interfaces in single files so you can copy/paste them easily, kept up to date by the community:

https://github.com/Xarkanoth/Holdfast-Scripts/tree/main/IHoldfastSharedMethods

Older guides linked a HoldfastSharedMethods.rar hosted on Discord's CDN. That link is perishable and predates several interface updates — use the GitHub source above instead.

Enums & Data Types

The callback interfaces hand you enum values everywhere: FactionCountry, PlayerClass, CarryableObjectType, BuffType, ShipType, HighCommandOrderType, CharacterVoicePhrase, PlayerActions, EmplacementType, and more.

The full value tables are maintained on the Server Configuration Enums page. Most relevant to script modders:

The distributed enums can lag behind the game. The HoldfastSharedMethods DLL shipped with the SDK is not always regenerated when the game adds content. Known example (reported Feb 2026): CarryableObjectType was missing FifeAustrian, MarchingDrumAustrian, FlagAustrian, ChristmasPresent and Loudhailer, and had SmallBuckShot where the game uses OBSOLETE_SmallBuckShot. Earlier, Austria was missing from FactionCountry entirely until SDK v2.15.



Stopgap: cast the raw integer value, e.g. (FactionCountry)8 for a faction the enum doesn't know about yet. Find current values by decompiling the game (see ILSpy).

Key Scene GameObjects

When developing client-side mods, you will often need to find and interact with specific GameObjects in the game's scene. Knowing the names of these objects and how to access them is essential for tasks like creating custom UI and manipulating cameras.

Note: GameObject names can change at any time with no update regarding the name change. Verify with ILSpy or a runtime hierarchy dump if a Find starts returning null after a patch.

The Main Camera: Main Camera SCENE

This is the primary camera that renders the player's view. You might interact with it to get its position, change its field of view, or attach custom visual effects.

  • Name: Main Camera SCENE
  • Also reachable via Camera.main or FindObjectsOfType<Camera>().
  • Tip: objects attached to the camera survive round changes better than most scene objects.

The Main UI Canvas: Main Canvas

This GameObject is the root of most of Holdfast's user interface. If you want to add your own UI elements (like panels, text, or images), you will typically make them children of this canvas to ensure they are rendered correctly on the player's screen.

  • Name: Main Canvas

Distance Fog: Height Fog Global

The distance fog is a GameObject named Height Fog Global. Disabling it removes distance fog — but maps re-enable it, so a fog-removal mod needs to periodically Find and re-disable it (e.g. a small MonoBehaviour on a timer).

ClientScene GameObjects

Name Under Description
Post Processing Global Volume ClientScene
MasterAudio - Client Scene ClientScene
Nature Renderer Global Settings ClientScene
Scripts ClientScene
Audio Listener ClientScene
WindZone ClientScene
Camera - Client ClientScene
Main Canvas ClientScene The UI panel that players see in-game.
Menu Canvas ClientScene
Height Fog Global ClientScene Distance fog. Maps re-enable it if disabled.

Main Canvas GameObjects

Name Description
Player Name Tags
Priority Name Tag Canvas
Spyglass Lens Panel
Settings Loading Black Background
End of Round Panel
End of Match Panel
Game Elements Panel
New Player Joined Notification Panel
Free Roam Panel
Death Screen Panel
Spectating Panel
Top Info Bar Turning off the Top Info Bar will result in it being turned back on automatically.
Kill Log Panel
UI Proxy Player Card Container Panel
Main Respawn Timer Container
P Menu Panel
Round Players Control Panel - Dialog Box
Report Submitted Popup
New Scoreboard Panel
Round End Panel
New Chat Panel
VOIP Elements Panel
Poll Voting Panel
In-game Server Details Panel
Controls Notification Popup

Menu Canvas GameObjects

Name Description
Main Screen Panels
IntroInfo Panel
Game Console Panel Hosts the F1 console (and its InputField, used by the deprecated injection technique).
Settings Loading Panel Overlay
Tooltip Panel
Loading Screen Panel

Finding Internal Names (ILSpy)

The scene object tables above go stale. The reliable way to discover internal class names, GameObject names, enum values, and callable client-side methods is to decompile the game's code with ILSpy (free, open source).

  1. Open Assembly-CSharp.dll from the Holdfast game folder (Holdfast NaW_Data/Managed/) in ILSpy.
  2. Search for the system you care about (e.g. "Spectator", "Fog", "Spawning").
  3. Cross-reference the enum values against the Server Configuration Enums page — ILSpy shows the current game values even when the SDK's distributed enums lag behind.

Known Useful Internal Calls (Client-Side)

Found via ILSpy and confirmed working from client mods:

  • HoldfastGame.ClientPlayerSpawningHandler.StartRequestSpawn() — force a respawn request.
  • ClientSpectatorManager.SpectateStartRequest(-1) — enter spectator mode.
These are internal APIs: they are undocumented, unsupported, and can break or be renamed in any patch without notice. Wrap them defensively.

SDK Editor Tooling

Inside the SDK editor, UModEditorTool.editorTool?.gameAssets exposes the game-asset list. Community tooling built on it (e.g. a script that auto-generates proxy prefabs for every GameAssetPrefab in a scene) lives in the example repos.

Advanced Patterns & Recipes

The Singleton State Pattern (Multi-Interface Mods)

Because UMod creates one instance per interface, any mod using more than one interface needs a single shared state object:

  1. Create a plain class (e.g. MyModApp) that owns all your mod's state and logic.
  2. Implement each interface in its own thin class (e.g. MyModInterface, MyModInterface3).
  3. In OnRoundDetails (the first per-round event), create or fetch the shared MyModApp and hand every interface class a reference to it.
  4. Keep the shared instance in a separate plain static class if you must reach it statically — never as a static member of the interface classes themselves.

Hosting Coroutines & Update Loops

Interface classes are not MonoBehaviours. To run coroutines or Update() logic:

  1. Create a GameObject from any callback (e.g. OnRoundDetails).
  2. AddComponent a custom MonoBehaviour of yours.
  3. Run coroutines / per-frame logic on that component. Its OnDestroy() doubles as your round-end detector.

Persistent State Between Rounds

Statics reset between rounds (UMod Rules). To carry state across a map rotation:

  1. Serialize your state to a string (JSON works well).
  2. Store it on a GameObject that survives the round change (camera-attached objects are long-lived).
  3. After the mod reloads, find the object and deserialize.

Talking to the Web

Without -allowRestrictedMods, you can still make HTTP requests with Unity's WWW/WWWForm — e.g. POST kill events from OnPlayerKilledPlayer to a stats API. Limits: no external SDK imports, no System.IO. With -allowRestrictedMods enabled on the server, full IO/database/API access opens up for server-side mods.

Deterministic Scene Animation

To animate a scene object identically on every client with no networking: give the object an Animator with a normalized "progress" parameter, then drive that parameter from OnUpdateSyncedTime. Every client computes the same progress for the same timestamp, so the animation stays in sync for free.

Dev-Mod Workflow

Maintain a second, private workshop item ("MyMod DEV") that builds to the same target folder. Iterate against the dev item on a test server; only push to the public item when stable. Subscribers never see your broken intermediate builds.

Troubleshooting

Recurring errors and their community-verified fixes.

Error / Symptom Cause & Fix
Failed to locate linked type The script wasn't in the mod's build folder (it must live in the same folder tree as the mod scene), or Visual Studio isn't linked to Unity. See Getting Started.
System.Reflection.AmbiguousMatchException: Ambiguous match found at mod load Constructor overloading on a mod class — UMod's relinker can't handle it. Keep one constructor.
Illegal reference to disallowed namespace: System.Reflection (or System.IO) Restricted namespace. Remove the reference, or have the server host enable -allowRestrictedMods (server-side only).
MissingMethodException: UnityEngine.UI.InputField.get_onEndEdit() The mod uses the deprecated console-InputField injection and was built against pre-2022 Unity. Rebuild on the current SDK and migrate to IHoldfastGame.
Could not load file or assembly 'netstandard, Version=2.1.0.0' Hit script mods containing MonoBehaviours after the May 2025 game update; interface-only mods kept working. Rebuild on the current SDK. Verify current status
Failed to pre-relink mod script / Fallback handler could not load library A required file wasn't in the build folder.
ModBuildException: Failed to create shared resources (and other generic build failures) Often unexplained project corruption. The nuclear fix that works surprisingly often: create a fresh empty mod and copy your scene/scripts/assets into it.
Mod builds but statics behave weirdly / crashes on load Static members inside interface-implementing classes. Move them to a separate plain static class. See UMod Rules.
Mod state resets every round Expected — each round loads a fresh assembly. Use the persistence pattern.
Build breaks after adding version control Git repos inside the SDK project can break UMod builds (cause unknown). Delete/recreate .git or keep the repo outside the project.
mod_variable value never arrives Value contains # (config comment character). Encode it differently.
NullReferenceException ... SpawnSectionCapturePoint on map load Map-mod issue: a spawn section tied to capture point -1 / mis-hooked spawn sections.
HoldfastMapTerrain.CheckWhichMaterialsAreUsed NRE Map-mod terrain material setup issue.
ArgumentException: An item with the same key has already been added (EmplacementManager) Broken "populated buildings" assets in the scene — remove them.
Uploaded .umod file "disappeared" Normal — it gets renamed to <workshopId>.umod on upload.

General debugging advice: isolate the issue in a minimal example mod. The developers won't investigate script mod issues without a small replication project.

SDK Version History (Scripting-Relevant)

Release notes that changed how script mods are written. Full upgrade steps live on the SDK Upgrade Guide; full game changelogs on Game Release History.

Version Unity What Changed for Script Modders
v2.31 (current) 2022.3.62f1 Unity bumped to the last LTS release of Unity 2022.
v2.27 2022.3.53f1 Unity bump; uniform/workshop tooling updates (Austrian faction in upload window).
v2.15 2022.3.11f1 The big one: Built-in Render Pipeline → URP, .NET Framework → .NET Standard 2.1, Mono updated. Added IHoldfastGame / ExecuteConsoleCommand (replacing console UI injection). Added Austria to FactionCountry. Added beta-workshop support (-betaWorkshop launch param, use_beta_workshop config). Removed MicroSplat. Mods built on Unity 2020 with deprecated APIs broke and needed rebuilding.
v2.8 2020.3.34f1 Renamed OfficerOrderTypeHighCommandOrderType in IHoldfastSharedMethods2 — broke older mods.
v2.6 2020.3.34f1 Fixed mod_variable_local (local-scope variables) not being delivered.
Game 2.5 Holdfast SDK 5.0: level, uniform and flag editors with 1500+ new assets.
Game 1.20 SDK V3: smarter bots (melee, firing, blocking) and the first rc commands for bot control.
Game 1.18 First exposure of code events for the modding community (the ancestor of the SharedMethods interfaces); mod tagging on upload.
Game 1.3 Holdfast SDK V1 released: level editor, uniform & flag editor.

Known Breakages Timeline

  • May 2025 game update: script mods containing MonoBehaviours failed to load with Could not load file or assembly 'netstandard, Version=2.1.0.0'; interface-only mods kept working. Rebuilding on the current SDK is the fix.
  • Unity 2020 → 2022 (v2.15): prebuilt mods using deprecated Unity APIs (notably InputField.onEndEdit) broke at runtime; statics handling changed around this era so state no longer persists between rounds.
  • IL2CPP experiment: the developers briefly tried IL2CPP and reverted because it broke UMod — Mono is a hard requirement for the modding system.

Code Examples & Resources

Example Repositories

Official Documentation

Video

  • Stan's SDK tutorial series (#1–#22) — linked on the SDK User Guide. Tutorial #17 (Custom Tools) is the most relevant to scripting.

Tools

  • ILSpy — decompile Assembly-CSharp.dll to find internal names and current enum values.

Feel free to post in the #mod-support channel on the Official Holdfast Discord for any help.

Known Limitations & Open Questions

Things the community has hit the edges of. If you solve one of these, share it in #mod-support and update this page.

  • Attributing splash-damage / indirect kills — no interface event ties explosion splash victims (or grapeshot correctly) to the attacker; OnPlayerHurt lacks an attacker id.
  • Force-switching a player's faction/team from a script — not exposed; no RC command or interface method found.
  • Forcing the spawn/faction-selection panel open on a client — only partial client-side hacks exist.
  • Mod-to-mod code sharing — UMod build-option references and exported packages don't load in game; no workaround found.
  • Reading a ship's max HP before it takes damage — not exposed.
  • Loading a local .umod without the Workshop round-trip — never confirmed; the dev-mod workflow is the only verified approach.
  • Clean mod unloading between rotations — observed to misbehave; design mods to fully re-initialise in OnRoundDetails.
  • OnPlayerPacket timestamp semantics — send-time or receive-time? Undocumented.

Support

Got a question that's not covered in this documentation?

We will gladly give you a helping hand should you require, so don't shy away from asking any questions. You will also find members within the community familiar with the toolset willing to do so.

Join the Official Discord, then head to the #become-a-modder channel and click the button to get the Modder role. This will unlock all channels concerning the discussion of modifications and scripting, including #mod-support.

Discord — Holdfast Workshop