Script Modding Guide
How to create a script SDK mod
The Basic Flow
Basic knowledge of C# and Unity is required.
To start with script modding in holdfast, create a new mod and in this mod’s folder create a new script. This script should implement the shared interface IHoldfastSharedMethods.
For example, here’s a mod that’s called “LineFormer” that has the script called “LineFormer.cs” which implements the IHoldfastSharedMethods interface.
Client or Server, what are those?!
One of the most important concepts to understand while script modifying Holdfast is the concept of "Client" and "Server".
The "Server" is the game server which handles all players verification data, networking, chat/voip, etc.
Whereas the "Client" is either your own client referred to as the "Owner/Local Player" or someone else referred to as a "Proxy".
Mod Install and Load Types
Similarly to the normal process of using the
load_mod <steam_id> load type for mods, we've added a
load_mod_server_only <steamid> which loads a mod specifically only on server side (such as a custom stats tracking or auto admin mod), and a
load_mod_client_only <steamid> which inversely only loads the mod on client side (such as a ui mod or a audio replacement mod). In addition
mods_installed_server_only <steamid> was also added. The advantage of this is mods that the client does not need to join the server, the clients won't need to install the mod on their end.
The load order for this new feature is first all the Common Mods are loaded and than the client/server specific one's are loaded. As always, the internal order of each list, is dependant on the order of how they were typed in the config file.
Please note players can only load mods that the server allows on their list, so a
mods_installed_client_only <steamid> doesn't make sense, so if you're going to be making a client only loaded mod, it needs to be installed on both the client and the server.
Simply put, mods can receive variables through the PassConfigVariables() method. These variables are supplied in the server config file in the format
mod_variable <string> for Global Scope (outside of the map rotations) or
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 received from all mods, so a pattern of prefixing your mod variables with your mod id,
modid:key:value, is highly recommended, unless you're working on multiple mods that can share settings data.
As you might be aware, we have some security related scripts that, for example don't allow a modder to gain access to the server or client's machine (ie: System.IO, System.Reflection, etc), these typically will show up as an error on the server's console with something along the lines of
[UMod.Scripting.ScriptDomain]: Illegal reference to disallowed namespace: System.Reflection.
We've added a feature for server hosts to be able to pass a "-allowRestrictedMods" in the launch parameters of the server. This will turn off the securities on the server machine ONLY, for ALL the mods. As such, if you rent your servers, you will need to talk to your host provider to enable this if they want to support it.
What this will allow you to do is hook directly with IO operations and other restricted features (writing files, databases, websites api's, etc) so for development this will open major features that will be able to be done easier or that weren't able to be done outright.
It's highly recommended that most actions that a mod would do would be defered by a frame or a few ms when it comes to interacting with a player (healing, damaging, reviving, positioning, rotation, etc). If you code something, and it's logically sound, but ingame it's not doing exactly as expected or has bugs occuring due to it, after trying to delay the action, feel free to ask in mod-support.
Note, don't use a "static" member inside of the Interface classes. This confuses the dynamic loader of C# and it'll be bad times for you trying to figure out why.
New 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 script to force bots to not auto move or rotate. Note: this is different than setting the carbonplayer forceInputAxis/forceInputRotation, since thoses ones will lock ALL the bots in the action, whereas this will allow you to individually control bots's facing rotation and movement system.
rc carbonPlayers ignoreAutoControls <true/false>
Send network trajectory info to client without showing it to players
Similar to drawFirearmTrajectories, this will send the trajectory data from server to client. Unlike the aforementioned command, this will NOT display it to the player, but the mod will still be recieving the data. Enable this only if you're planning on using the "OnShotInfo" on a client side, for server side mods, this isn't required.
rc set drawFirearmTrajectoriesInfo <true/false>
These messages will not shown to the player anywhere. The intended use for them is for a server-side mod to be able to communicate to a client side mod.
You have a mod that every time a player presses a button on your mod custom ui, you want to slap a player for 5 damage and they will be shown the message `haha you got slapped by <playername>` on their end of the mod. You are be able to do this by creating a mod that on the player's client side [ie player 3], you have a custom ui panel with a button, which when the button is pressed will send a message to the server (multiple ways of doing this, for example using rc system). This message is than read by the server side mod (could be the same mod, just need conditional logic using the "OnIsServer" and "OnIsClient" to split the mod up) and slaps the targetted player [ie player 5]. Than the server would send a quietPrivateMessage to the slapped target [player 5] informing him that he just got slapped by player 3. The client side of player 5 will than read the quiet private message (using OnTextMessage) and handle it appriopiately.
rc serverAdmin quietPrivateMessage <player_id> <message> rc serverAdmin quietBroadcastMessage <message>
If you hover over the interface's properties in visual studio, these can be found in the inspector too. In case visual studio doesn't link the dll correctly to inspect the data, feel free to refer to the raw cs files inside the DLL.
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);
The game will share the current synchronized time (same on client and server) on every frame of the game.
void OnUpdateSyncedTime(double time);
The game will return the seconds since the round started on every frame of the game.
void OnUpdateElapsedTime(float time);
The game will share the current remaining time on every frame of the game.
void OnUpdateTimeRemaining(float time);
The game will return if the mod is loaded on a server.
void OnIsServer(bool server);
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);
The game will call this on round start with details regarding the current round.
void OnRoundDetails(int roundId, string serverName, string mapName, FactionCountry attackingFaction, FactionCountry defendingFaction, GameplayMode gameplayMode, GameType gameType);
The game will call this once with all the config variables set on the config file of the server.
Note: you will receive all variables, so make sure to filter only the ones you care about.
void PassConfigVariables(string value);
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);
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);
The game will call this when a player spawns in game.
void OnPlayerSpawned(int playerId, int spawnSectionId, FactionCountry playerFaction, PlayerClass playerClass, int uniformId, GameObject playerObject);
The game will call this when a player is hurt in game.
void OnPlayerHurt(int playerId, byte oldHp, byte newHp, EntityHealthChangedReason reason);
The game will call this when a player is killed by another player.
void OnPlayerKilledPlayer(int killerPlayerId, int victimPlayerId, EntityHealthChangedReason reason, string details);
The game will call this when a player receives any score.
void OnScorableAction(int playerId, int score, ScorableActionType reason);
The game will call this when a player shoots his gun.
void OnPlayerShoot(int playerId, bool dryShot);
The game will call this when a bullet.
Note: This requires `rc set drawFirearmTajectories true` or `rc set drawFirearmTajectoriesInfo 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);
The game will call this when a player successfully blocks another player.
void OnPlayerBlock(int attackingPlayerId, int defendingPlayerId);
The game will call this when a player starts a secondary attack (shove).
void OnPlayerMeleeStartSecondaryAttack(int playerId);
The game will call this when a player swaps their weapon.
void OnPlayerWeaponSwitch(int playerId, string weapon);
The game will call this when a player starts carrying an object.
void OnPlayerStartCarry(int playerId, CarryableObjectType carryableObject);
The game will call this when a player stops carrying an object.
void OnPlayerEndCarry(int playerId);
The game will call this when a player shouts using the ingame voice commands(not voip).
void OnPlayerShout(int playerId, CharacterVoicePhrase voicePhrase);
The game will call this when the console command is used.
Note: This will be only called on the client, or on the server that is executing the console command.
void OnConsoleCommand(string input, string output, bool success);
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);
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);
The game will call this when a message is received in the chat system.
void OnTextMessage(int playerId, TextChatChannel channel, string text);
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);
The game will call this when an object is damaged.
void OnDamageableObjectDamaged(GameObject damageableObject, int damageableObjectId, int shipId, int oldHp, int newHp);
The game will call this when an object is interacted with.
void OnInteractableObjectInteraction(int playerId, int interactableObjectId, GameObject interactableObject, InteractionActivationType interactionActivationType, int nextActivationStateTransitionIndex);
The game will call this when an emplacement (sapper object) is initially placed.
void OnEmplacementPlaced(int itemId, GameObject objectBuilt, EmplacementType emplacementType);
The game will call this when an emplacement (sapper object) is fully constructed.
void OnEmplacementConstructed(int itemId);
The game will call this when a capture point is fully captured.
void OnCapturePointCaptured(int capturePoint);
The game will call this when a capture point changes owner.
void OnCapturePointOwnerChanged(int capturePoint, FactionCountry factionCountry);
The game will call this when a capture data changes.
void OnCapturePointDataUpdated(int capturePoint, int defendingPlayerCount, int attackingPlayerCount);
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.
void OnBuffStart(int playerId, BuffType buff);
The game will call this when a buff is removed from a player.
void OnBuffStop(int playerId, BuffType buff);
The game will call this when a faction vs faction round is over.
Note: Most of them except for Army Deathmatch.
void OnRoundEndFactionWinner(FactionCountry factionCountry, FactionRoundWinnerReason reason);
The game will call this when a free for all round is over.
Note: Only Army Deathmatch.
void OnRoundEndPlayerWinner(int playerId);
The game will call this when a vehicle(horse) is spawned.
void OnVehicleSpawned(int vehicleId, FactionCountry vehicleFaction, PlayerClass vehicleClass, GameObject vehicleObject, int ownerPlayerId);
The game will call this when a vehicle(horse) is hurt.
void OnVehicleHurt(int vehicleId, byte oldHp, byte newHp, EntityHealthChangedReason reason);
The game will call this when a vehicle(horse) is killed by a player.
void OnPlayerKilledVehicle(int killerPlayerId, int victimVehicleId, EntityHealthChangedReason reason, string details);
The game will call this when a ship is spawned.
void OnShipSpawned(int shipId, GameObject shipObject, FactionCountry shipfaction, ShipType shipType, int shipName);
The game will call this when a ship takes damage.
void OnShipDamaged(int shipId, int oldHp, int newHp);
Following update 1.20 we introduced more exposed functions using the interface "IHoldfastSharedMethods2". Use the same workflow as "IHoldfastSharedMethods" to access these functions.
This will be called every time we get a packet from a user.
Note: Server Side ONLY.
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);
This will be called every time we get a packet from a vehicle.
Note: Server Side ONLY.
void OnVehiclePacket(int vehicleId, Vector2 inputAxis, bool shift, bool strafe, PlayerVehicleActions actionCollection);
This will be called every time we get a officer order.
void OnOfficerOrderStart(int officerPlayerId, OfficerOrderType officerOrderType, Vector3 orderPosition, float orderRotationY, int voicePhraseRandomIndex);
This will be called every time we get a officer order stop.
void OnOfficerOrderStop(int officerPlayerId, OfficerOrderType officerOrderType);
AGS's Misc Examples
Spammy's Chat Filter
Spammy's OwO Slapper
Feel free to post in the Official Holdfast Discord #Mod-Support for any help
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 apply for the 'Artisan' role by selecting the art emoji in the #getting-started channel. This will unlock all channels concerning the discussion of modifications and scripting.