Forts
107 ratings
How to mod Forts
By [DEV] BeeMan and 1 collaborators
The official guide on how to create mods for Forts to add new devices, weapons, materials, change balance, visuals and effects.
3
   
Award
Favorite
Favorited
Unfavorite
Learning Lua
Forts uses Lua to configure the game. By writing your own Lua scripts you can change and add to Forts. Lua is a powerful and easy to learn scripting language. You can pick up a lot by reading the scripts included in Forts, but to get a more structured introduction I recommend the tutorials found at lua-users:

http://lua-users.org/wiki/TutorialDirectory

The first 8 tutorials are enough to do everything needed to mod Forts effectively.

You can use any text editor to edit Lua scripts, such as Notepad, which comes with Windows. Something more sophisticated such as Notepad++[notepad-plus-plus.org] or Sublime Text[www.sublimetext.com] will make life easier, however. To support localisation into some non-English languages the editor will need to support UTF-16 Unicode encoding.
Game configuration
Forts' data can be browsed as ordinary files. You can find it by right clicking on Forts in Steam, then properties, then local files. You will see the data directory there.

The data is divided into subdirectories according to their purpose. For example there is a directory for devices, weapons and materials. You can browse these folders to learn about how the game is set up. The game looks for specific plain text Lua files in order to get the parameters and asset filenames it needs to load and run the game.

The scripts most important to modding include:
  • db/constants.lua
  • db/rules.lua
  • materials/building_materials.lua
  • devices/device_list.lua
  • weapons/weapons_list.lua
  • weapons/projectile_list.lua
During the loading of a battle, the game will read these files and look for certain tables and variables within them. If you open weapon_list.lua, you'll see a table called Weapons. The game looks through this table to find the weapons to be used for this battle, what they are called internally (SaveName), how much they cost, build duration, etc.

Each weapon entry has a value called FileName. This points to a Lua file with detailed data about the appearance and behavior of the weapon. It sets the effects used while firing, reloading, being destroyed etc. It also has a value called Projectile. This value references the SaveName of an entry in the Projectiles table of the projectile_list.lua file, and so specifies which projectile to create when the weapon fires. Devices are similar to weapons but don't need parameters relating to firing. Materials have all of their configuration data in building_materials.lua, and their appearance is in the Sprites table, including damage levels.

You can browse this material, device, weapon and projectile data to see what parameters, texture files and effects are needed to make up the game. There is quite a lot, but everything is named well and you can ask us if something doesn't make sense. Understanding the game configuration is essential for modding the game correctly.
Mods replicate part of the file structure
In the data directory you'll see a folder called 'mods'. If you browse that you'll see subdirectories. Each of these is a mod. If you browse some of these mods (such as weapon_pack) you'll see some folders and Lua files that are the same as in the base data directory.

The way mods work in Forts is that the base Lua file is loaded first, and then for each of the mods that are active, the game looks for the same Lua file in the mod's directory and loads that on top of what's been loaded already. So each mod can change the variables and tables of the base game. They can also change images, projectiles, and effects. They can add completely new items, disable existing items, or remove them altogether.

If you are changing a variable it is often better to multiply it by a factor, or add or subtract, instead of setting an absolute value. We make balancing changes from time to time, and an absolute change may be off the mark afterwards.

Warning: never make changes to the base game files or included mod folders. Doing so will change your experience, make the game unstable, and cause desyncs in multiplayer and when watching older replays. If you need to revert a change you can right click on Forts in Steam, then properties, then local files. Select Verify Integrity of Game Files.
A simple mod: changing a value
Let's take a simple example of setting gravity to zero. The value that controls gravity is in db/constants.lua, in the table called Physics, and the variable is Gravity. By default this is 981, the value of acceleration under Earth's gravity in game units per second per second.

The first step is to create a folder under mods, let's call it zerog. Then we create a subdirectory called db, and within that a blank text file called constants.lua. It should look like the original file containing the variable we want to mod, but under our own mod directory instead.

Now we can't use the same format in the base constants.lua file, because that would wipe all the other values in the Physics table. Instead we can reference a value within the table like so:

(in file mods/zerog/db/constants.lua)
Physics.Gravity = 0

You also need to create a mod.lua file in mods/zerog and add the following lines:

Selectable = true Category = "Physics"

Run Forts and go to Sandbox. Start a map and go to the Player tab. You should see under the Physics category the name of the mod folder. Select that, start the Sandbox and watch things float about. Congratulations! You've made your first mod.

As you know we've already made a zero gravity mod, and the mod name is zerogravity. We found that setting the gravity to zero wasn't the only change needed. We also reduced the drag so ships could move faster and the maximum angle at which a strut will accept a device, and changed a few other things. You might also find that making a mod requires more changes than you anticipated.
The order of application
Because there can be any number of mods active, the data being modified is not necessarily what is in the base game. The base file is loaded, then the first mod, then the next mod, and so on. Each mod is applied to the changes made by the mods before it.

The order that mods are applied in is influenced by the optional Priority value in the mod.lua file. Lower values are applied first. Higher values are applied last, and so tend to have the final say. The expected range is 1 to 10, and the default is 5. If two mods have the same priority the order is alphabetical according to the mod name (or Workshop published id).

(in mod.lua)
-- apply this mod late Priority = 10

If a mod should make a change to every item, it should have a high value for Priority so it is applied last. The fast-build mod, for example, has a building_material.lua file which iterates the material items, including any that came from mods applied before it, and adjusts their build and scrap time like so:

for k,v in ipairs(Materials) do v.BuildTime = v.BuildTime/4 v.ScrapTime = v.ScrapTime/2 end

Also, don't be dependent on the data being untouched. If you want your new weapon to appear in a particular location you need to look for the position of the weapon you want it to appear after and insert it after that. Examples can be found in the weapon_pack mod. The Rocket is positioned after the EMP Rocket:

table.insert(Weapons, IndexOfWeapon("rocketemp") + 1, { -- rocket data }
Post Processing: RegisterApplyMod
You can schedule a function to be called once all mods have been applied. It allows you to do processing, calculations and call functions you've written yourself which may not be possible during normal script loading call. This helps you apply changes to items added after your mod - regardless of priority.

This works in practically all configuration scripts. In a specific device configuration script, or device.lua, to double production, for example:

-- simple form using an anonymous function RegisterApplyMod(function() EnergyProductionRate = 2*EnergyProductionRate MetalProductionRate = 2*MetalProductionRate end)

Commanders use this system to apply changes to modded devices, weapons, materials, projectiles, and more. For example, Architect reduces build times in weapon_list.lua with a named function, like so:

-- name the function and register that CommanderApplyModBase = function() for k, device in ipairs(Weapons) do if active and device.Prerequisite ~= "upgrade" then device.BuildTimeComplete = 0.25*device.BuildTimeComplete end end end RegisterApplyMod(CommanderApplyModBase)

All commander 'ApplyMod' functions have the same name. If you are modding a commander and want to prevent this from functioning, you can remove it using the DeregisterApplyMod function:

DeregisterApplyMod(CommanderApplyModBase)

You are free to name your own functions so that other mods can remove yours, just keep in mind that earlier mods may have defined such a function too. Make sure you give them a unique name, or you could overwrite a previous mod's value.

NOTE: The older ApplyMod system is deprecated. It is still functional but we don't recommend using it, and we may remove it at some point in the future.
Adding a device with included textures
We created the Large Sandbags mod as a comprehensive example of how to add a new device to Forts. Once you are subscribed, navigate to your Steam library and then steamapps\workshop\content\410900\1293804859. This is an example of where a Workshop mod ends up after you upload it and someone subscribes to it. The original folder name is changed to a number, but everything else is the same. The change of location is why prefixing filenames with the path variable is so important - more below.

The critical file in this mod is devices/device_list.lua. The first part of this script creates new sprites based on the included textures, found in ui/textures/HUD. The DetailSprite and ButtonSprite helper functions are defined in data/ui/uihelper.lua, and the Sprites table is read by the game to create any sprites defined by the configuration script. The 'path' variable is set by the game to the relative or absolute path of the mod (in the case of a subscribed mod), allowing it to reference new texture assets stored in the mod's directory. You don't need to understand exactly how this works, but if you keep your texture files in the same location and update the names it should work.

table.insert(Sprites, DetailSprite("hud-detail-sandbags-large", "HUD-Details-SandBagsLarge", path)) table.insert(Sprites, ButtonSprite("hud-sandbags-large-icon", "HUD/HUD-SandbagsLarge", nil, ButtonSpriteBottom, nil, nil, path))

Moving down a bit, you'll see where the new device is added to the Devices table.

table.insert(Devices, IndexOfDevice("sandbags") + 1, { SaveName = "sandbags_large", FileName = path .. "/devices/sandbags_large.lua", Icon = "hud-sandbags-large-icon", Detail = "hud-detail-sandbags-large", Prerequisite = "upgrade", BuildTimeIntermediate = 0, BuildTimeComplete = 30, ScrapPeriod = 8, MetalCost = 20, EnergyCost = 800, MetalRepairCost = 20, EnergyRepairCost = 800, MetalReclaimMin = 0, MetalReclaimMax = 0, EnergyReclaimMin = 0, EnergyReclaimMax = 0, MaxUpAngle = StandardMaxUpAngle, BuildOnGroundOnly = false, SelectEffect = "ui/hud/devices/ui_devices", })

The IndexOfDevice function allows us to put the new device in a sensible position (after the original sandbag). The third parameter is the new item we're adding. We define the SaveName, the FileName which sets the detailed configuration of the device, the Icon and Detail HUD sprites, which we defined at the top, the prerequisite tech structure - the Upgrade Centre, and several other basic parameters (build time, cost and reclaim settings).

You can open devices/sandbags_large.lua to see what detailed parameters are set. This is where you would make the device behave different to other devices. You can look at the devices included with the game and the weapon_pack mod to see what variables to change and what values would be appropriate. It's a good idea to play around with the values to see what feels good, rather than just using the first values that occur to you. Testing with friends helps to get feedback.

In the middle of the file you'll see a new Sprites table, which sets the single-frame sprite of the sandbag to the included texture /devices/SandbagsLarge.tga. Note the use of the path variable again. The following forward slash is important as it's not included in the path variable.

Further down you'll see the Root table, which configures how multiple sprites fit together to make a single device. This can be a complex set of nested tables for a weapon, as there are multiple parts and a pivot where the gun rotates. This structure specifies where projectiles and beams are emitted from by creating a "Hardpoint0" child node (with no Sprite value set). If you want to create a new weapon it's best to copy an existing one and experiment with Root until you understand how it works. The Angle, Pivot and PivotOffset values determine how child nodes are positioned relative to the parent. UserData is set to the percentage of construction at which the node will appear (or disappear on scrap).

The other gameplay related change the Large Sandbags mod does is reduce the damage the 20mm cannon does to it. You'll find this in mods\weapon_pack\weapons\projectile_list.lua. This is a mod of a mod (or a nested mod). It's changing the way the weapon_pack works because the damage multiplier versus a device is associated with the projectile. It is set like this:

cannon20mm = FindProjectile("cannon20mm") if cannon20mm then table.insert(cannon20mm.DamageMultiplier, { SaveName = "sandbags_large", Direct = 0.2, Splash = 0.2 }) end
Device Upgrades
Devices (and weapons) can have one or more upgrades, with optional prerequisites (see the Prerequisites section below). This is achieved by adding an Upgrades table to the device in device_list.lua (or weapon_list.lua for weapons).

The entries in this table set the SaveName and cost of the upgrade, and can also set a button sprite for the context menu. Although the detail sprite of the device will be shown on rollover, when there are multiple upgrades custom buttons are recommended so the player can tell them apart easily.

The ButtonSprite helper function can be used to make the button sprite so it has disabled, rollover and pressed states. The built-in textures with path ui/textures/context/HUD-Buttons-Blank-*.tga can be copied and edited to create new buttons.

Upgrades can be disabled at start by setting Enabled = false, and later enabled by mission or mod script by using the EnableDeviceUpgrade or EnableWeaponUpgrade functions.

If you are modding an existing device then make sure to insert your upgrades rather than setting the table from scratch, to prevent the loss of the existing upgrades, or those added by other mods.

-- this will look for the following textures to make the sprite (path is the root of your mod): -- path .. "/ui/textures/context/HUD-Buttons-Mortar2-A.tga" -- active (waiting for input) -- path .. "/ui/textures/context/HUD-Buttons-Mortar2-D.tga" -- disabled (unable to be pressed) -- path .. "/ui/textures/context/HUD-Buttons-Mortar2-R.tga" -- rollover -- path .. "/ui/textures/context/HUD-Buttons-Mortar2-S.tga" -- selected table.insert(Sprites, ButtonSprite("hud-context-upgrade-mortar2", "context/HUD-Buttons-Mortar2", nil, nil, nil, nil, path))

-- located within a device table of device_list.lua or weapon_list.lua Upgrades = { { Enabled = true, SaveName = "minigun", MetalCost = 200, EnergyCost = 400, }, { Enabled = true, SaveName = "mortar2", MetalCost = 100, EnergyCost = 200, Button = "hud-context-upgrade-mortar2", }, },

Along with the High Seas update, a new upgrade variable was added: TransferReloadProgress. When this is set to true, the upgraded weapon retains the reload state of the original. This was used for the multi-barrelled Deck Gun weapons when switching between spread and focus fire.
Prerequisites
By default materials and devices can be built if the player has enough resources. It's possible, however, to add a requirement to build a certain device or a combination of devices first. This is how the 'tech' buildings operate to restrict construction.

Both materials and devices (which includes weapons) have the same syntax for the Prerequisites variable. If it is left as nil, then it can always be built.

If assigned to a string then at least one device with that SaveName must be built before the material or device can be constructed:

Prerequisite = "workshop",

There is an obsolete method of setting an alternative required device, but we recommend you use the more flexible syntax below. In the following example the item can be built if the workshop or the armoury has been built.

Prerequisite = "workshop", PrerequisiteAlt = "armoury",

You can also set up a list of combinations of device SaveNames for complex requirements. If any one of the combinations is satisfied then the item can be built. In the following example, the item can be built if the workshop and the upgrade centre is built, or the factory alone is built. This list of combinations can be as long as you like.

Prerequisite = { { "workshop", "upgrade" }, { "factory" } },

Device upgrades can have their own prerequisites set in the same way. If the upgrade's Prerequisite variable is not set then it defaults to the prerequisites of the target device of the upgrade.

Upgrades = { { Enabled = true, SaveName = "mortar2", Prerequisite = { { "upgrade" }, { "munitions" } }, MetalCost = 50, EnergyCost = 800, Button = "hud-context-mortar2", -- if set this will customise the context menu }, },
Beam Configuration
Beams operate over a period of time, set by the BeamDuration variable of the weapon, and so have configuration options that projectile weapons don't. The simplest way of setting up their width and damage is with the ProjectileThickness and ProjectileDamage variables in the weapons/projectile_list.lua item, which remain constant. ProjectileDamage for beams means 'hitpoints per second'.

In addition, beams can have time functions for thickness and damage. This is to allow more dramatic wind up, wind down and oscillation. To achieve this you can implement the BeamThickness and BeamDamage functions in your weapon configuration script. They take a time value (relative to the end of FireDelay), and return a value which represents the instantaneous thickness and damage (still in hitpoints per second) of the beam.

Within the weapon configuration script, the constant variables and the result of the functions can both be modified with BeamThicknessMultiplier and BeamDamageMultiplier. They default to 1.

Here is part of the Plasma Laser weapon configuration script as an example. To allow more intuitive tuning of the beam we organise the data in a time-keyed table, BeamTable, and then use InterpolateTable to calculate intermediate values.

dofile("scripts/interpolate.lua") -- first column is time keypoint -- second coloumn is thickness at that keypoint -- third column is damage at that keypoint BeamTable = { { 0, 1, 0, }, { 0.25, 3, 0, }, { 0.5, 30, 1000, }, { 1, 30, 1000, }, { 1.5, 0, 0, }, } function BeamThickness(t) return InterpolateTable(BeamTable, t, 2) end function BeamDamage(t) return InterpolateTable(BeamTable, t, 3) end

With the High Seas DLC update in March 2022, the ability to specify these functions as part of the projectile item was introduced. This was necessary because the Ammo system allowed a weapon to produce multiple beams. Also added was a new sprite which tracks the collision end point of the beam, described below. The Orbital Sweep projectile gives an example:

table.insert(Projectiles, { SaveName = "ol_beam_sweep", ProjectileType = "beam", ProjectileSprite = path .. "/weapons/media/ol_beam.tga", -- other details omitted BeamThickness = function(t) return InterpolateTable(BeamTableOrbitalSweep, t, 2) end, BeamDamage = function(t) return InterpolateTable(BeamTableOrbitalSweep, t, 3) end, BeamCollisionThickness = function(t) return InterpolateTable(BeamTableOrbitalSweep, t, 4) end, }) -- first column is time keypoint -- second coloumn is thickness -- third column is damage -- fourth column is collision sprite thickness BeamTableOrbitalSweep = { { 0, 1, 0, 0, }, { 1, 10, 0, 0.25, }, { 1.25, 100, 1500, 1, }, { 3.25, 100, 1500, 1, }, { 4.25, 0, 0, 0, }, }

WARNING: name your beam table something unique to your mod. Using the same table name will overwrite previous values and change the behaviour of other beams.

Beam Layers and Collision Sprite
Prior to High Seas, beams could only have one visual layer. Now it's possible to have multiple layers, each with their own thickness functions, scroll rate, tile rate, and additive value.

They could also only have an effect produced at collision points. We needed more control of this, so we added an optional sprite with its own thickness function. It will exactly track the endpoint of the beam, even if it moves rapidly.

The sprites for each beam layer should specify "repeatS = true," to make the texture tile along the length of the beam.

The Orbital Sweep beam is an example:

table.insert(Sprites, { Name = "ol_beam", States = { Normal = { Frames = { { texture = path .. "/weapons/media/ol_beam.tga" }, repeatS = true, } }, }, }) -- additional beam layer sprite definitions omitted for brevity table.insert(Projectiles, { SaveName = "ol_beam_sweep", ProjectileType = "beam", -- other details omitted ProjectileSprite = path .. "/weapons/media/ol_beam.tga", Beam = { Sprites = { { Sprite = "ol_beam", ThicknessFunction = "BeamThickness", ScrollRate = -2, TileRate = 400*3, }, { Sprite = "ol_beam4", ThicknessFunction = "BeamThickness", ScrollRate = -2, TileRate = 400*3, Additive = true, }, { Sprite = "ol_beam6", ThicknessFunction = "BeamThickness", ScrollRate = -3, TileRate = 800*3, Additive = true, }, { Sprite = "ol_beam2", ThicknessFunction = "BeamThickness", ScrollRate = -6, TileRate = 800*3, Additive = true, }, { Sprite = "ol_beam5", ThicknessFunction = "BeamThickness", ScrollRate = -7, TileRate = 600*3, Additive = true, }, }, CollisionSprite = { Sprite = "ol_collision", ThicknessFunction = "BeamCollisionThickness", Scale = 0.5, Additive = true, }, }, })

Maximum Travel
The High Seas update also introduced the concept of beams having a maximum distance of travel, which continues through reflections and when passing portals (which add no distance themselves). The beam will stop abruptly after this distance has been reached, like a light saber. It can be adjusted by setting the BeamMaxTravel variable of the projectile item. It defaults to a large number, so will not affect legacy beams.

table.insert(Projectiles, { SaveName = "mylightsaber", ProjectileType = "beam", -- other details omitted BeamMaxTravel = 1000, })
Material Conversions
Right clicking on a strut will bring up a context menu, including a number of buttons with material icons. These buttons can be used to convert the strut into a different material. By default this list of materials is set to the contents of the DefaultConversions table in the base building_materials.lua file:

DefaultConversions = { "bracing", "backbracing", "armour", "door", "shield" }

This list can be modified like anything, but it's also possible to customise it for a specific material by setting the Conversions table. It's best to merge any new materials with the DefaultConversions table, to keep the changes introduced by other mods. To allow a material to be convertible to a portal in the context menu, for example:

-- in the material Conversions = MergeLists(DefaultConversions, { "portal", }),

In order for this to work the material to be included must have the Context variable set to the name of a valid sprite. The ButtonSprite function from ui/uihelper.lua must be provided with a path in order for it to find the textures within the mod's ui/textures folder:

Sprites = { ButtonSprite("context-shield", "context/HUD-Buttons-Shield", nil, nil, nil, nil, path), } -- in the material Context = "context-shield",

See the 'bracing' material in the base building_materials.lua file for an example.
Adding effects and audio
You can add custom effects to your mods such as explosions, muzzle flashes, and projectile trails. These can including new textures and sound effects, or instead use existing assets from the game. Reference these new effects from your other scripts using the file's name relative to your mod's 'path', for example in a device script:

DestroyEffect = path .. "/effects/mygun_med_exp.lua"

To do this make new .lua scripts in a folder called 'effects' in your mod. The game will scan this folder and pre-load the effects. Assets for your effects should be placed in the 'effects/media' folder, which are also pre-loaded.

Effect scripts take the following general format, for example:

-- how long the effect can last, even if component effects are still going LifeSpan = 3 -- optional triggering of an fmod sound event (usually based on an existing effect script) SoundEvent = "effects/mushroom_cloud.lua" -- list of (potentially animated) sprites. be careful of duplicate names. Sprites = { { Name = "mygun_medium_explosion", States = { Normal = { Frames = { -- the last frame is blank to prevent looping { texture = path .. "/effects/media/med_exp_01.tga" }, { texture = path .. "/effects/media/med_exp_02.tga" }, { texture = path .. "/effects/media/med_exp_03.tga" }, { texture = path .. "/effects/media/med_exp_03.tga", colour = { 1, 1, 1, 0 }, duration = 2 }, duration = 0.1, blendColour = false, blendCoordinates = false, }, }, }, }, } -- list of sub-effects that make up this effect Effects = { { Type = "sprite", PlayForEnemy = true, TimeToTrigger = 0, LocalPosition = { x = 0, y = 100, z = 0 }, LocalVelocity = { x = 0, y = 0, z = 0 }, Acceleration = { x = 0, y = 0, z = 0 }, Drag = 0.0, Sprite = "mygun_medium_explosion", -- defined above Additive = false, TimeToLive = 2, InitialSize = 1.4, ExpansionRate = 0, Angle = 0, AngleMaxDeviation = 0, AngularVelocity = 0, RandomAngularVelocityMagnitude = 0, Colour1 = { 255, 255, 255, 255 }, Colour2 = { 255, 255, 255, 255 }, }, },

The following effect types are available:

sprite
Creates a single textured quad that can move, rotate, scale and blend between two colours. See example above.

sound
Makes an audio effect based on a .wav or .mp3 file asset. This is not commoly used by the game itself because more control can be had using the Fmod event system. For user created mods it is fine, and allows new sound effect files to be added to the game.

{ Type = "sound", TimeToTrigger = 0.0, LocalPosition = { x = 0, y = 0, z = 0 }, Sound = path .. "/effects/media/med_exp.mp3", Volume = 0.6, },

sparks
Creates a series of bursts made up of sprites with rules about how they are positioned, sized and rotated depending on the direction they are emitted.

{ Type = "sparks", PlayForEnemy = true, TimeToTrigger = 0.1, SparkCount = 12, SparksPerBurst = 12, BurstPeriod = 1, LocalPosition = { x = 0, y = 0 }, Texture = path .. "/effects/media/debris.tga", Gravity = 0981, -- particles are emitted according to this distribution pattern -- NormalDistribution is the alternative, which is bell shaped EvenDistribution = { -- minimum and maximum angle in degrees (e.g. -180, 45, 0) Min = -35, Max = 35, -- standard deviation at each iteration in degrees (zero spaces evenly) StdDev = 5, }, -- particles get properties interpolated from these keyframes -- depending on the angle at which it is emitted Keyframes = { { Angle = -35, RadialOffsetMin = 0, RadialOffsetMax = 20, ScaleMean = 0.5, ScaleStdDev = 0.25, SpeedStretch = 0, SpeedMean = 500, SpeedStdDev = 200, Drag = 1, RotationMean = 45, RotationStdDev = 180, RotationalSpeedMean = 10, RotationalSpeedStdDev = 5, AgeMean = 1, AgeStdDev = 0.5, AlphaKeys = { 0.1, 0.8 }, ScaleKeys = { 0.1, 1 }, }, { Angle = 35, RadialOffsetMin = 0, RadialOffsetMax = 20, ScaleMean = 0.5, ScaleStdDev = 0.25, SpeedStretch = 0, SpeedMean = 500, SpeedStdDev = 200, Drag = 1, RotationMean = -45, RotationStdDev = -180, RotationalSpeedMean = 10, RotationalSpeedStdDev = 5, AgeMean = 1, AgeStdDev = 0.5, AlphaKeys = { 0.1, 0.8 }, ScaleKeys = { 0.1, 1 }, }, }, },

trail
Creates a textured streamer which follows the position of the effect.

{ Type = "trail", Texture = path .. "/media/bullet_trail.tga", LocalPosition = { x = 0, y = 0, z = 9.0 }, Width = 3, Length = 0.75, Keyframes = 100, KeyframePeriod = 0.05, RepeatRate = 0.001, ScrollRate = 0, FattenRate = 0, },

shake
Makes the camera move rapidly depending on how close it is to the effect.

{ Type = "shake", TimeToTrigger = 0, TimeToLive = 8, Magnitude = 28, },

There are many variables associated with each of these. Please use the Forts/data/mods/weapon_pack/effects folder as a basis to learn from. You can copy and edit the scripts to make your own.
Localisation
In Forts all languages are implemented as mods. You can inspect the base string tables in the Forts/data/mods/language-* folders to see what files are available for modding. In order to change or extend string tables for your mod you need to mod these language mods. Using the Large Sandbags mod as a reference again, you will see the following folder structure (with English expanded as an example):

mods/ language-BrazilianPortuguese/ language-Chinese/ language-English/ devices/ strings.lua mods/ commander-bpo-scattershot/ strings.lua language-French/ language-German/ language-Hungarian/ language-Italian/ language-Japanese/ language-Korean/ language-Polish/ language-Portuguese/ language-Russian/ language-Spanish/ language-Turkish/

If you opened mods/language-English/devices/strings.lua, you will see:

function Merge(t1, t2) for k, v in pairs(t2) do t1[k] = v end end Merge(Device, { sandbags_large = L"Large Sandbags", sandbags_largeTip2 = L"Heavy sandbags with greater hitpoints", sandbags_largeTip3 = L"Requires: Upgrade Centre", })

This shows how the strings describing the new device are merged into the sub-string table called Device. The string ids, like "sandbags_large", and "sandbags_largeTip2", are automatically generated from each device SaveName to look for the strings to show.

If you are replacing an existing string, you can just overwrite it, as in the Scattershot commander string.lua example:

Scattershot.ToolTipLine2 = L"Passive: sucky cannon with only splash damage." Scattershot.ToolTipLine3 = L"Active: try it out. "

If a particular language is missing for a particular string id then it will be blank for tips, but in most cases it will appear as the string id. This makes it easier to find string ids that are missing translations.
Moonshot Only Features
A few features were introduced with the Moonshot DLC that are not available in the base game. If you wish to make use of these features then your mod becomes dependent on Moonshot, and can only be activated when the host of the game has it installed (which is always you in single player games). These mods are displayed with a blue Moonshot icon in the mod selection screen.

When you make a Moonshot dependent mod, you must add a flag to your mod.lua file so the game can quickly identify it as such. You may get an error message if you try to publish a mod that depends on moonshot and does not have this flag:

RequiresMoonshot = true

The features and their associated keywords which trigger dependency are outlined below. Note that if these words are found anywhere in the appropriate file it will become dependent, even if it's out of place or in a comment.

Supporting Moonshot
If you want to make use of Moonshot features when they're available, but still allow your mod to run without them working if Moonshot is not available, then use this flag instead:

SupportsMoonshot = true

The game will automatically set two variables, dlc1Var_Active and dlc1Var_Value. These are set to true and 1 if Moonshot is available, and false and 0 if it's not, respectively. You can use these to prevent the addition of weapons which don't make sense, for example.

Portal
The keyword Portal in building_materials.lua triggers dependency. This allows a material to teleport projectiles and beams when two such struts are linked by the player.

MaxBursts
The keyword MaxBursts in a weapon script, or weapon_list.lua will trigger dependency. When used in a weapon script (e.g. machinegun.lua) the weapon will self destruct after firing that many times. This is used on the Moonshot only Buzzsaw weapon.

RayDamage
The keyword RayDamage in the projectile_list.lua file will trigger dependency. This is used in combination with a few other variables to create damage in a straight line when a projectile collides with a strut. The buzzsaw projectile, for example:

RayOffset = 0.1, RayLength = 50.1, RayDamage = 7.5, RayDamageLimit = 3000, RayIncendiaryRadius = 0, RayExcludeBackground = false, RayStopAtForeground = false,

FieldRadius
The keyword FieldRadius in the projectile_list.lua will trigger dependency. This is used in combination with a few other variables to create a field for various purposes. It is used to implement smoke and to attract projectiles to the magnabeam point of impact. For example, the smoke projectile uses it like so:

FieldRadius = 200.0, FieldType = FIELD_DISRUPT_BUILDING | FIELD_BLOCK_BEAMS | FIELD_SCRAMBLE_GUIDANCE | FIELD_BLOCK_FIRING, FieldIntersectionNearest = true, FieldStrengthMax = 1.0, FieldStrengthFalloffPower = 1.0, MagneticModifierFriendly = 0.0, CollidesWithBeams = true,

The options for FieldType are:

FIELD_DISRUPT_BUILDING FIELD_MAGNETIC FIELD_BLOCK_BEAMS FIELD_SCRAMBLE_GUIDANCE FIELD_BLOCK_FIRING
Dynamic Scripting
By adding a script.lua file in their root folder, mods can have arbitrary Lua script executed based on events triggered by the game, including projectile impacts, destruction of devices and weapons, ownership changes and user input. There are also events for the initial load, physics frame, display frame, and restart. Functions can also be scheduled to execute at a specific time in the future. They behave identically to mission scripts, but are not tied to a specific map.

In response to these events the mod script can perform a host of changes to the game state, create effects, or show controls on the screen. It can also track any state it wishes in the 'data' table, which will automatically be serialised for the purpose of replays and players joining a battle.

The details of this dynamic mod scripting can be found at the Forts Scripting API Reference[www.earthworkgames.com] page, which is generated directly from the Forts source code with mark up to provide descriptions.
Mod categories
To make your mod easy to find on the Steam Workshop and in the game's mod selection screen, we recommend you put it into a category. This is set in the (optional) mod.lua file you put in the mod's base folder. This will apply a tag to the item in the Workshop, useful for searching for those of interest. In the game it will put the mod into a branch of a tree view which can be collapsed for quick browsing.

An example mod.lua would be

Category = "Weapons"

If mod.lua is missing, the Category variable is not specified, or it's invalid then the mod is placed in the Misc category. Valid categories are:

  • Combat
  • Devices
  • Physics
  • Resources
  • Rules
  • Misc
  • Disable
  • Weapons
  • Technology
  • Materials
  • Environments
  • Campaigns
Publishing and testing
Once you are happy with how your mod is working locally, you can upload it to the Steam Workshop. Before you do this you need to create a preview.jpg file in the mod's folder which will represent it in Forts and on Steam Workshop. The size isn't so important, but the aspect ratio should be 16:9 so it appears correctly in Forts. The map previews are 1024*576, for example. This is fine for mods too.

Now to publish the mod, go to the main menu and then enter the Workshop menu. Go to the Upload tab and select the mod you've been working on. Click on the Upload button, and then Yes. After a short while it will open the new item in the workshop. You should be subscribed to it automatically, and you should make it either hidden or visible to friends only for testing. To make sure the mod will work for others, you should get a friend to subscribe to it and run the game with it. Otherwise you should rename your mod folder and run the Workshop version of the mod. This will bring to light any asset path problems that had been missed during development. Change the folder name of you mod back to edit and update once the problems are fixed.
Including Mods
You can extend or force include other mods by adding an "Include" table to the top of the mod.lua script. You can only include built-in forts mods and workshop mods. Attempting to include non-built in local mods will fail upload validation and prevent the mod from being uploaded to the workshop.

An example valid Include table added to the top of a mod.lua file would be:

Include = { "unlimited-energy", "1293804859" }

In the above example, this would force the inclusion of the "unlimited-energy" built in mod and the workshop mod with the id of 1293804859 (Large Sandbags).

Including other Workshop mods will automatically tag your uploaded mod with the "Nested" tag so players can easily identify them in the workshop. There is a maximum include nesting depth of 5. i.e. you cannot include mods that chain include other mods if the total nesting (chain) depth is greater than 5.
Forcing a mod in a map
You can force a Workshop mod on in your custom map, in the same way as a built-in mod. In your map's mission script, add or edit the Mods table to include the published file id. For example, to make sure Large Sandbags are available in your map:

Mods = { "1293804859" }

To make it easier to understand, you could write it like this instead:

sandbags_large = "1293804859" Mods = { sandbags_large }

This is vital if your map includes new materials, devices or weapons added by a mod at the start.
Environment Mods
It is possible to create new environments via a mod. The mod must contain an "environment" sub-folder which follows the same folder structure as per the built-in environments. Environment mods will be detected on uploading and automatically tagged and categorised as an environment mod.

The official Mars Workshop Environment Mod (workshop id 1308302446) can be used as an example of the folder structure for an environment mod.

It is possible to override an existing mod with the "Include" table. As per Nested Mods, you can only include Built-in and Workshop environment mods. There are also additional Include table limitations for the environments:
  • You can only include a single (environment) mod in the table
  • Included environment mods cannot include/override other environment mods (i.e. max nesting depth of 1)

Some built-in environment already override other environments and therefore cannot be overridden further. This is the list of built-in environments currently available to override:
"alpine" "canyon" "desert" "fields" "midwest" "polluted" "trainingground" "dlc1_indo" "dlc1_japan" "dlc1_newzealand" "dlc1_oz" "dlc1_oznight" "dlc2_oceans_base"
83 Comments
alibejsenby May 28 @ 10:43am 
NICE
[DEV] BeeMan  [author] Aug 12, 2023 @ 11:27am 
@bugenis I recommend joining our Discord server and using the #modding channel for detailed troubleshooting. There is a button on the main menu of Forts.
bugenis Aug 12, 2023 @ 3:00am 
My code is:

table.insert(Sprites, ButtonSprite("hud-implosioncannon-icon", "HUD/HUD-FireBeam", nil, ButtonSpriteBottom, nil, nil, path))

table.insert(Sprites, DetailSprite("hud-detail-cannon-wp", "HUD-Details-Cannon", path))

This works in other mods, but when I try launching mine, it says:

Error loading mod weapons/weapon_list.lua: [string table.insert(Sprites, ButtonSprite("hud-implosioncannon-icon", ..."]:5: unexpected symbol near ')'
SharkLazerBoy Dec 12, 2022 @ 4:34pm 
Sooo, I have this cool mod idea for the game, but it might be to big for my first mod, so can someone help me in any way?
[DEV] BeeMan  [author] Oct 17, 2022 @ 3:29am 
@Recruit glad you found the guide helpful. I'd recommend joining our friendly Discord (https://discord.gg/TP4CpnF) and ask about this in the modding channel.
[DEV] BeeMan  [author] Oct 10, 2022 @ 5:05pm 
@Gereon please join our Discord (https://discord.gg/TP4CpnF) and use the #tech-support channel for assistance.
Gereon Oct 9, 2022 @ 11:09am 
I have made a mod that changes some sprites. It works fine in sandbox, but whenever I try to upload it to the workshop, I get this: "Error: Item Update Failure: Generic Failure. Possible communication issue with Steam Workshop, try again later". Steam will then create a Mod in the workshop that is 0MB big, can't have it's visibility changed and can't be found in-game. I have tried many times over the last week, every time the same error.
[DEV] BeeMan  [author] Sep 25, 2022 @ 3:12am 
@Altas the instructions above should be complete. Join the Discord and tell the modding channel what you've done in detail. There's a button on the Forts main menu.
Atlas Sep 25, 2022 @ 2:17am 
Hey, I already failed at "A simple Mod" ^^. Somehow my mod isn´t appearing in the mod list ingame in Sandbox mode. Any ideas?
[DEV] BeeMan  [author] Aug 23, 2022 @ 3:46am 
@pro100roman100 Yes. I suggest you join the Discord server and ask in the #modding channel to get direction. It's too much to answer here.