dfhack/docs/guides/modding-guide.rst

462 lines
18 KiB
ReStructuredText

.. _modding-guide:
2022-06-20 12:38:23 -06:00
DFHack modding guide
====================
What is the difference between a script and a mod?
--------------------------------------------------
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
A script is a single file that can be run as a command in DFHack, like something
that modifies or displays game data on request. A mod is something you install
to get persistent behavioural changes in the game and/or add new content. Mods
can contain and use scripts in addition to (or instead of) modifications to the
DF game raws.
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
DFHack scripts are written in Lua. If you don't already know Lua, there's a
great primer at `lua.org <https://www.lua.org/pil/contents.html>`__.
2022-06-20 12:38:23 -06:00
Why not just mod the raws?
--------------------------
It depends on what you want to do. Some mods *are* better to do in just the raws.
You don't need DFHack to add a new race or modify attributes, for example. However,
DFHack scripts can do many things that you just can't do in the raws, like make a
creature that trails smoke. Some things *could* be done in the raws, but writing a
script is less hacky, easier to maintain, easier to extend, and is not prone to
side-effects. A great example is adding a syndrome when a reaction is performed.
If done in the raws, you have to create an exploding boulder to effect the syndrome.
DFHack scripts can add the syndrome directly and with much more flexibility. In the
end, complex mods will likely require a mix of raw modding and DFHack scripting.
2022-06-20 12:38:23 -06:00
A mod-maker's development environment
2022-06-21 06:02:10 -06:00
-------------------------------------
2022-06-20 12:38:23 -06:00
While you're writing your mod, you need a place to store your in-development scripts
that will:
- be directly runnable by DFHack
- not get lost when you upgrade DFHack
The recommended approach is to create a directory somewhere outside of your DF
installation (let's call it "/path/to/own-scripts") and do all your script
development in there.
2022-06-20 12:38:23 -06:00
Inside your DF installation folder, there is a file named
:file:`dfhack-config/script-paths.txt`. If you add a line like this to that file::
+/path/to/own-scripts
Then that directory will be searched when you run DFHack commands from inside the
game. The ``+`` at the front of the path means to search that directory first,
before any other script directory (like :file:`hack/scripts` or
:file:`raw/scripts`). That way, your latest changes will always be used instead of
older copies that you may have installed in a DF directory.
For scripts with the same name, the `order of precedence <script-paths>` will be:
1. ``own-scripts/``
2. ``data/save/*/raw/scripts/``
3. ``raw/scripts/``
4. ``hack/scripts/``
2022-06-20 12:38:23 -06:00
The structure of the game
-------------------------
2022-07-15 12:43:48 -06:00
"The game" is in the global variable `df <lua-df>`. The game's memory can be
found in ``df.global``, containing things like the list of all items, whether to
reindex pathfinding, et cetera. Also relevant to us in ``df`` are the various
types found in the game, e.g. ``df.pronoun_type`` which we will be using in this
2022-09-12 16:04:16 -06:00
guide. We'll explore more of the game structures below.
2022-06-20 12:38:23 -06:00
Your first script
2022-06-21 06:02:10 -06:00
-----------------
2022-06-20 12:38:23 -06:00
So! It's time to write your first script. This section will walk you through how
to make a script that will get the pronoun type of the currently selected unit.
2022-06-20 12:38:23 -06:00
First line, we get the unit::
2022-06-20 12:38:23 -06:00
local unit = dfhack.gui.getSelectedUnit()
2022-07-15 12:43:48 -06:00
If no unit is selected, an error message will be printed (which can be silenced
by passing ``true`` to ``getSelectedUnit``) and ``unit`` will be ``nil``.
2022-06-20 12:38:23 -06:00
If ``unit`` is ``nil``, we don't want the script to run anymore::
2022-06-20 12:38:23 -06:00
if not unit then
return
end
2022-07-15 12:43:48 -06:00
Now, the field ``sex`` in a unit is an integer, but each integer corresponds to
a string value ("it", "she", or "he"). We get this value by indexing the bidirectional
map ``df.pronoun_type``. Indexing the other way, incidentally, with one of the strings,
will yield its corresponding number. So::
2022-06-20 12:38:23 -06:00
local pronounTypeString = df.pronoun_type[unit.sex]
print(pronounTypeString)
2022-07-15 12:43:48 -06:00
Simple. Save this as a Lua file in your own scripts directory and run it as
shown before when a unit is selected in the Dwarf Fortress UI.
2022-06-20 12:38:23 -06:00
Exploring DF structures
-----------------------
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
So how could you have known about the field and type we just used? Well, there
are two main tools for discovering the various fields in the game's data
structures. The first is the ``df-structures``
`repository <https://github.com/DFHack/df-structures>`__ that contains XML files
describing the contents of the game's structures. These are complete, but difficult
to read (for a human). The second option is the `gui/gm-editor` script, an
interactive data explorer. You can run the script while objects like units are
selected to view the data within them. You can also run ``gui/gm-editor scr`` to
view the data for the current screen. Press :kbd:`?` while the script is active to
view help.
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
Familiarising yourself with the many structs of the game will help with ideas
immensely, and you can always ask for help in the `right places <support>`.
2022-06-20 12:38:23 -06:00
Detecting triggers
------------------
2022-06-20 12:38:23 -06:00
The common method for injecting new behaviour into the game is to define a
callback function and get it called when something interesting happens. DFHack
provides two libraries for this, ``repeat-util`` and `eventful <eventful-api>`.
``repeat-util`` is used to run a function once per a configurable number of frames
(paused or unpaused), ticks (unpaused), in-game days, months, or years. If you
need to be aware the instant something happens, you'll need to run a check once a
tick. Be careful not to do this gratuitiously, though, since running that often can
slow down the game!
2022-07-03 08:59:01 -06:00
``eventful``, on the other hand, is much more performance-friendly since it will
only call your callback when a relevant event happens, like a reaction or job being
completed or a projectile moving.
2022-07-03 08:59:01 -06:00
To get something to run once per tick, we can call ``repeat-util.scheduleEvery()``.
First, we load the module::
2022-07-03 08:59:01 -06:00
local repeatUtil = require('repeat-util')
2022-07-03 08:59:01 -06:00
2022-07-15 12:43:48 -06:00
Both ``repeat-util`` and ``eventful`` require keys for registered callbacks.
You should use something unique, like your mod name, perhaps with a suffix if you
are registering multiple keys::
2022-07-03 08:59:01 -06:00
2022-07-14 10:46:12 -06:00
local modId = "callback-example-mod"
2022-07-03 08:59:01 -06:00
2022-07-15 12:43:48 -06:00
Then, we pass the key, amount of time units between function calls, what the
time units are, and finally the callback function itself::
2022-07-03 08:59:01 -06:00
repeatUtil.scheduleEvery(modId, 1, "ticks", function()
-- Do something like iterating over all active units and check
-- for something interesting
2022-07-14 10:46:12 -06:00
for _, unit in ipairs(df.global.world.units.active) do
...
2022-07-03 08:59:01 -06:00
end
end)
``eventful`` is slightly more involved. First get the module::
2022-07-03 08:59:01 -06:00
local eventful = require('plugins.eventful')
2022-07-03 10:33:36 -06:00
2022-07-15 12:43:48 -06:00
``eventful`` contains a table for each event which you populate with functions.
Each function in the table is then called with the appropriate arguments when
the event occurs. So, for example, to print the position of a moving (item)
projectile::
2022-07-04 10:11:21 -06:00
eventful.onProjItemCheckMovement[modId] = function(projectile)
print(projectile.cur_pos.x, projectile.cur_pos.y, projectile.cur_pos.z)
end
Check out the `full list of supported events <eventful-api>` to see what else
you can react to with ``eventful``.
2022-06-20 12:38:23 -06:00
2022-07-07 12:23:10 -06:00
Custom raw tokens
-----------------
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
In this section, we are going to use `custom raw tokens <custom-raw-tokens>`
applied to a reaction to transfer the material of a reagent to a product as a
handle improvement (like on artifact buckets), and then we are going to see how
you could make boots that make units go faster when worn.
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
First, let's define a custom crossbow with its own custom reaction. The
crossbow::
2022-07-07 12:23:10 -06:00
[ITEM_WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE]
[NAME:crossbow:crossbows]
[SIZE:600]
[SKILL:HAMMER]
[RANGED:CROSSBOW:BOLT]
[SHOOT_FORCE:4000]
[SHOOT_MAXVEL:800]
[TWO_HANDED:0]
[MINIMUM_SIZE:17500]
[MATERIAL_SIZE:4]
[ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250]
[ATTACK_PREPARE_AND_RECOVER:3:3]
2022-07-14 10:46:12 -06:00
[SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER:2] custom token (you'll see)
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
The reaction to make it (you would add the reaction and not the weapon to an
entity raw)::
2022-07-07 12:23:10 -06:00
[REACTION:MAKE_SIEGE_CROSSBOW]
[NAME:make siege crossbow]
[BUILDING:BOWYER:NONE]
[SKILL:BOWYER]
[REAGENT:mechanism 1:2:TRAPPARTS:NONE:NONE:NONE]
[REAGENT:bar:150:BAR:NONE:NONE:NONE]
[METAL_ITEM_MATERIAL]
[REAGENT:handle 1:1:BLOCKS:NONE:NONE:NONE] wooden handles
[ANY_PLANT_MATERIAL]
[REAGENT:handle 2:1:BLOCKS:NONE:NONE:NONE]
[ANY_PLANT_MATERIAL]
2022-07-15 12:43:48 -06:00
[SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1]
another custom token
2022-07-07 12:23:10 -06:00
[PRODUCT:100:1:WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE:GET_MATERIAL_FROM_REAGENT:bar:NONE]
2022-07-15 12:43:48 -06:00
So, we are going to use the ``eventful`` module to make it so that (after the
script is run) when this crossbow is crafted, it will have two handles, each
with the material given by the block reagents.
2022-07-07 12:23:10 -06:00
First, require the modules we are going to use::
2022-07-04 10:11:21 -06:00
2022-07-07 12:23:10 -06:00
local eventful = require("plugins.eventful")
local customRawTokens = require("custom-raw-tokens")
Now, let's make a callback (we'll be defining the body of this function soon)::
2022-07-07 12:23:10 -06:00
2022-07-14 10:46:12 -06:00
local modId = "siege-crossbow-mod"
2022-07-15 12:43:48 -06:00
eventful.onReactionComplete[modId] = function(reaction, reactionProduct,
unit, inputItems, inputReagents, outputItems)
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
First, we check to see if it the reaction that just happened is relevant to this
callback::
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
if not customRawTokens.getToken(reaction,
"SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT")
then
return
end
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
Then, we get the product number listed. Next, for every reagent, if the reagent
name starts with "handle" then we get the corresponding item, and...
::
2022-07-07 12:23:10 -06:00
for i, reagent in ipairs(inputReagents) do
if reagent.code:startswith('handle') then
2022-07-07 12:23:10 -06:00
-- Found handle reagent
local item = inputItems[i]
2022-07-07 12:23:10 -06:00
...We then add a handle improvement to the listed product within our loop::
2022-07-07 12:23:10 -06:00
local new = df.itemimprovement_itemspecificst:new()
new.mat_type, new.mat_index = item.mat_type, item.mat_index
new.type = df.itemimprovement_specific_type.HANDLE
outputItems[productNumber - 1].improvements:insert('#', new)
2022-07-07 12:23:10 -06:00
This works well as long as you don't have multiple stacks filling up one
reagent.
2022-07-07 12:23:10 -06:00
Let's also make some code to modify the fire rate of our siege crossbow::
2022-07-09 10:04:20 -06:00
eventful.onProjItemCheckMovement[modId] = function(projectile)
if projectile.distance_flown > 0 then
-- don't make this adjustment more than once
2022-07-09 10:04:20 -06:00
return
end
local firer = projectile.firer
if not firer then
return
end
local weapon = df.item.find(projectile.bow_id)
if not weapon then
return
end
2022-07-14 10:46:12 -06:00
local multiplier = tonumber(customRawTokens.getToken(weapon.subtype, "SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER")) or 1
2022-07-15 12:43:48 -06:00
firer.counters.think_counter = math.floor(firer.counters.think_counter *
multiplier)
2022-07-09 10:04:20 -06:00
end
2022-07-15 12:43:48 -06:00
Now, let's see how we could make some "pegasus boots". First, let's define the
item in the raws::
2022-07-11 11:56:28 -06:00
2022-07-12 04:26:49 -06:00
[ITEM_SHOES:ITEM_SHOES_BOOTS_PEGASUS]
[NAME:pegasus boot:pegasus boots]
[ARMORLEVEL:1]
[UPSTEP:1]
[METAL_ARMOR_LEVELS]
[LAYER:OVER]
[COVERAGE:100]
[LAYER_SIZE:25]
[LAYER_PERMIT:15]
[MATERIAL_SIZE:2]
[METAL]
[LEATHER]
[HARD]
2022-07-15 12:43:48 -06:00
[PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token
(you don't have to comment the custom token every time, but it does clarify what it is)
2022-07-11 11:56:28 -06:00
Then, let's make a ``repeat-util`` callback for once a tick::
2022-07-11 11:56:28 -06:00
repeatUtil.scheduleEvery(modId, 1, "ticks", function()
2022-07-15 12:43:48 -06:00
Let's iterate over every active unit, and for every unit, initialise a variable
for how much we are going to take from their movement timer and iterate over all
their worn items: ::
2022-07-11 11:56:28 -06:00
for _, unit in ipairs(df.global.world.units.active) do
local amount = 0
for _, entry in ipairs(unit.inventory) do
Now, we will add up the effect of all speed-increasing gear and apply it::
2022-07-11 11:56:28 -06:00
if entry.mode == df.unit_inventory_item.T_mode.Worn then
2022-07-14 10:46:12 -06:00
amount = amount + tonumber((customRawTokens.getToken(entry.item, "PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK")) or 0)
2022-07-11 11:56:28 -06:00
end
end
-- Subtract amount from movement timer if currently moving
dfhack.units.addMoveTimer(-amount)
2022-06-20 12:38:23 -06:00
The structure of a full mod
---------------------------
2022-06-20 12:38:23 -06:00
2022-07-15 12:43:48 -06:00
Now, you may have noticed that you won't be able to run multiple functions on
tick/as event callbacks with that ``modId`` key alone. To solve that we can
2022-07-15 12:43:48 -06:00
just define all the functions we want and call them from a single function.
Alternatively you can create multiple callbacks with your mod ID being a prefix,
though this way there is no guarantee about the call order (if that is important
to you). You will have to use your mod ID as a prefix if you register multiple
2022-07-15 12:43:48 -06:00
``repeat-util`` callbacks, though.
2022-07-07 12:23:10 -06:00
2022-07-15 12:43:48 -06:00
Create a folder for mod projects somewhere (e.g. ``hack/my-scripts/mods/``, or
maybe somewhere outside your Dwarf Fortress installation) and use your mod ID
(in hyphen-case) as the name for the mod folders within it. The structure of and
environment for fully-functioning modular mods are as follows:
2022-07-13 15:42:35 -06:00
* The main content of the mod would be in the ``raw`` folder:
2022-07-15 12:43:48 -06:00
* A Lua file in ``raw/init.d/`` to initialise the mod by calling
``your-mod-id/main enable``.
2022-07-13 15:42:35 -06:00
* Raw content (potentially with custom raw tokens) in ``raw/objects/``.
2022-07-15 12:43:48 -06:00
* A subfolder for your mod in ``raw/scripts/`` containing a ``main.lua`` file
(an example of which we will see) and all the modules containing the functions
used in callbacks to ``repeat-util`` and ``eventful``. Potentially a file
containing constant definitions used by your mod (perhaps defined by the
game, like the acceleration of parabolic projectiles due to gravity
(``4900``)) too.
2022-07-13 15:42:35 -06:00
* Using git within each mod folder is recommended, but not required.
* A ``readme.md`` markdown file is also recommended.
2022-07-15 12:43:48 -06:00
* An ``addToEntity.txt`` file containing lines to add to entity definitions for
access to mod content would be needed if applicable.
* Unless you want to merge your ``raw`` folder with your worlds every time you
make a change to your scripts, you should add
``path/to/your-mod/raw/scripts/`` to your script paths.
2022-07-13 15:43:01 -06:00
2022-07-15 12:44:21 -06:00
Now, let's take a look at an example ``raw/scripts/main.lua`` file. ::
local repeatUtil = require("repeat-util")
local eventful = require("plugins.eventful")
local modId = "example-mod"
local args = {...}
if args[1] == "enable" then
-- The modules and what they link into the environment with
-- Each module exports functions named the way they are to be used
2022-07-15 12:43:48 -06:00
local moduleA = dfhack.reqscript("example-mod/module-a") -- on load,
-- every tick
local moduleB = dfhack.reqscript("example-mod/module-b") -- on load,
-- on unload, onReactionComplete
local moduleC = dfhack.reqscript("example-mod/module-c")
-- onReactionComplete
local moduleD = dfhack.reqscript("example-mod/module-d") -- every 100
-- frames, onProjItemCheckMovement, onProjUnitCheckMovement
-- Set up the modules
2022-07-15 12:43:48 -06:00
-- Order: on load, repeat-util ticks (from smallest interval to
-- largest), days, months, years, and frames, then eventful callbacks in
-- the same order as the first modules to use them
moduleA.onLoad()
moduleB.onLoad()
repeatUtil.scheduleEvery(modId .. " 1 ticks", 1, "ticks", function()
moduleA.every1Tick()
end)
repeatUtil.scheduleEvery(modID .. " 100 frames", 1, "frames", function()
moduleD.every100Frames()
end
eventful.onReactionComplete[modId] = function(...)
-- Pass the event's parameters to the listeners, whatever they are
moduleB.onReactionComplete(...)
moduleC.onReactionComplete(...)
end
eventful.onProjItemCheckMovement[modId] = function(...)
moduleD.onProjItemCheckMovement(...)
end
eventful.onProjUnitCheckMovement[modId] = function(...)
moduleD.onProjUnitCheckMovement(...)
end
print("Example mod enabled")
elseif args[1] == "disable" then
2022-07-15 12:43:48 -06:00
-- Order: on unload, then cancel the callbacks in the same order as
-- above
moduleA.onUnload()
repeatUtil.cancel(modId .. " 1 ticks")
repeatUtil.cancel(modId .. " 100 frames")
eventful.onReactionComplete[modId] = nil
eventful.onProjItemCheckMovement[modId] = nil
eventful.onProjUnitCheckMovement[modId] = nil
print("Example mod disabled")
elseif not args[1] then
dfhack.printerr("No argument given to example-mod/main")
else
2022-07-15 12:43:48 -06:00
dfhack.printerr("Unknown argument \"" .. args[1] ..
"\" to example-mod/main")
end
2022-07-15 12:43:48 -06:00
You can see there are four cases depending on arguments. Set up the callbacks
and call on load functions if enabled, dismantle the callbacks and call on
unload functions if disabled, no arguments given, and invalid argument(s) given.
Here is an example of an ``raw/init.d/`` file: ::
2022-07-15 12:43:48 -06:00
dfhack.run_command("example-mod/main enable") -- Very simple. Could be
-- called "init-example-mod.lua"
Here is what ``raw/scripts/module-a.lua`` would look like: ::
--@ module = true
-- The above line is required for dfhack.reqscript to work
function onLoad() -- global variables are exported
-- blah
end
local function usedByOnTick() -- local variables are not exported
-- blah
end
function onTick() -- exported
for blah in ipairs(blah) do
usedByOnTick()
end
end
2022-07-15 12:43:48 -06:00
It is recommended to check `reqscript <reqscript>`'s documentation.
``reqscript`` caches scripts but will reload scripts that have changed (it
checks the file's last modification date) so you can do live editing *and* have
common tables et cetera between scripts that require the same module.