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).
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.
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.
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
.umodfile is renamed to<workshopId>.umodin 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 - SDKif you ever lose work.
Core Concepts
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
IHoldfastSharedMethods callbacks and your IHoldfastSharedMethods3 callbacks. State silently diverges.
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.
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).
#. 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
HardcoreMod: This is the modID.EnableKillOnSpawn: This is the key.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).
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
staticmembers 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
FogStateclass read by a MonoBehaviour). - No constructor overloading. Multiple constructors on a mod class break UMod's relinker with
System.Reflection.AmbiguousMatchException: Ambiguous match foundat 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
Instantiateonly 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
.githas 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 offOnUpdateSyncedTime/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:
OnIsServer(bool)— first event fired; branch client/server logic here.OnIsClient(bool, ulong steamId)PassConfigVariables(string[])— fired on both server and client.OnSyncValueState(int)/ time events begin.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
OnPlayerJoinedgives youplayerId,steamId,name,regimentTag,isBot.OnPlayerSpawnedgives you the player'sGameObject.- Player tracking pattern: keep a
Dictionary<int, PlayerInfo>keyed byplayerIdand join the data from both events. - Positions: read the
playerObjecttransform fromOnPlayerSpawned— it updates automatically as the player moves. AvoidOnPlayerPacketfor 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
MonoBehaviourand use itsOnDestroy()to detect the end of the round and trigger the save.
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).
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>
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 byrc 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:
- The server executes an rc command to send a string of data.
- The client receives that string from the
OnTextMessageevent.
Sending Data from the Server
Use one of the following commands:
rc serverAdmin quietPrivateMessage <player_id> <message>
rc serverAdmin quietBroadcastMessage <message>
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);
command: The RC command string to execute.output: An out parameter that will be filled with any text the command returns. This is very useful for debugging.success: An out parameter that will be true if the command was recognized and executed successfully.
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.

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
OnPlayerHurtdoes not include the attacker — onlyOnPlayerKilledPlayerlinks 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, notHitByCannonball. OnScorableActioncan catch kill-like events thatOnPlayerKilledPlayermisses — use it as a fallback signal.
Positions & Packets
- Prefer the
playerObjecttransform fromOnPlayerSpawnedfor positions; it updates automatically. OnPlayerPacketis server-side only and unreliable (packet loss/ordering). WhetherpacketTimestampis 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
oldHpin the firstOnShipDamaged. - Player→ship association can tentatively be inferred from
spawnSectionIdinOnPlayerSpawned. Verify
Admins & Config
OnPlayerConnected'sisAutoAdminonly flags whitelisted auto-login admins. To detect a password-logged-in admin from a client mod, have the client executerc playerlist(an admin-only command) and check whether it succeeds.PassConfigVariablesfires on both server and client when the mod loads.
Messaging
quietPrivateMessagecannot reach players who haven't spawned yet; usequietBroadcastMessagefor join-time sync.
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
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:
- Faction —
FactionCountry - Class —
PlayerClass - Game Modes —
GameplayMode/GameType - Weapon and Weapon Classes
- Player Actions —
PlayerActions(used byOnPlayerPacketand bot control) - Voice Phrases —
CharacterVoicePhrase - Carryable Object Type —
CarryableObjectType - Buff Type —
BuffType - High Command Order Type —
HighCommandOrderType - Emplacement Type —
EmplacementType - Ship Type, Ship Names —
ShipType - Explosion Type and Faction Round Winner Reason
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.
(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.
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.mainorFindObjectsOfType<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).
- Open
Assembly-CSharp.dllfrom the Holdfast game folder (Holdfast NaW_Data/Managed/) in ILSpy. - Search for the system you care about (e.g. "Spectator", "Fog", "Spawning").
- 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.
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:
- Create a plain class (e.g.
MyModApp) that owns all your mod's state and logic. - Implement each interface in its own thin class (e.g.
MyModInterface,MyModInterface3). - In
OnRoundDetails(the first per-round event), create or fetch the sharedMyModAppand hand every interface class a reference to it. - 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:
- Create a
GameObjectfrom any callback (e.g.OnRoundDetails). AddComponenta custom MonoBehaviour of yours.- 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:
- Serialize your state to a string (JSON works well).
- Store it on a GameObject that survives the round change (camera-attached objects are long-lived).
- 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 OfficerOrderType → HighCommandOrderType 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
- AGS's Misc Examples: https://github.com/CM2Walki/HoldfastMods — updated for v2.8+.
- Xarkanoth's Examples: https://github.com/Xarkanoth/Holdfast-Scripts — includes the up-to-date SharedMethods copy/paste sources.
- Spammy's Chat Filter: https://github.com/AgentNo/holdfast-scripts-and-configs/blob/main/scripts/spammys_chat_filter/SpammyChatFilter.cs
- Spammy's OwO Slapper: https://github.com/AgentNo/holdfast-scripts-and-configs/blob/main/scripts/no_uwu_allowed/TestScriptMod.cs
- Elf's Custom Factions: https://github.com/LoganBlinco/BaseCustomFactionMod — see also Custom Factions#Replacing Factions.
- eLF's learning path for new script modders: https://pastebin.com/mr7psxk6
Official Documentation
- SDK Documentation — 100+ pages covering the whole SDK.
- UMod 2.0 User Guide — the underlying mod system's own docs.
- Wiki: SDK User Guide, SDK Upgrade Guide, Remote Console Commands, Server Configuration Enums, Mods and Consoles.
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.dllto 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;
OnPlayerHurtlacks 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
.umodwithout 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. OnPlayerPackettimestamp 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.