Wrap lines to 80 characters

develop
Tachytaenius 2022-07-15 19:43:48 +01:00
parent 0e704f39f7
commit fcd8839c0d
1 changed files with 161 additions and 52 deletions

@ -6,41 +6,70 @@ DFHack modding guide
What is a mod/script?
---------------------
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. DFHack mods contain and use scripts as well as often having a raw mod component.
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. DFHack
mods contain and use scripts as well as often having a raw mod component.
DFHack scripts are written in Lua. If you don't already know Lua, there's a great primer at https://www.lua.org/pil/1.html.
DFHack scripts are written in Lua. If you don't already know Lua, there's a
great primer at https://www.lua.org/pil/1.html.
Why not just use raw modding?
-----------------------------
For many things it's either completely and only (sensibly) doable in raws or completely and only doable with DFHack. For mods where DFHack is an alternative and not the only option, it's much less hacky, easier to maintain, and easier to extend, and is not prone to side-effects. A great example is adding a syndrome when a reaction is performed requiring an exploding boulder in raws but having dedicated tools for it if you use DFHack. Many things will require a mix of raw modding and DFHack.
For many things it's either completely and only (sensibly) doable in raws or
completely and only doable with DFHack. For mods where DFHack is an alternative
and not the only option, it's much less hacky, easier to maintain, and easier to
extend, and is not prone to side-effects. A great example is adding a syndrome
when a reaction is performed requiring an exploding boulder in raws but having
dedicated tools for it if you use DFHack. Many things will require a mix of raw
modding and DFHack.
A mod-maker's development environment
-------------------------------------
Scripts can be run from a world's ``raw/scripts/`` directory, and (configurably) are run by default from ``hack/scripts/``. Scripts in ``raw/init.d/`` are automatically run on world load. Scripts within the raws are a component for more advanced mods.
Scripts can be run from a world's ``raw/scripts/`` directory, and (configurably)
are run by default from ``hack/scripts/``. Scripts in ``raw/init.d/`` are
automatically run on world load. Scripts within the raws are a component for
more advanced mods.
A script is run by writing its path and name from a script path folder without the file extension into a DFHack command prompt (in-game or the external one). E.g. ``gui/gm-editor`` for ``hack/scripts/gui/gm-editor.lua``.
A script is run by writing its path and name from a script path folder without
the file extension into a DFHack command prompt (in-game or the external one).
E.g. ``gui/gm-editor`` for ``hack/scripts/gui/gm-editor.lua``.
You can make all your scripts in ``hack/scripts/``, but this is not recommended as it makes things much harder to maintain each update. It's recommended to make a folder with a name like ``own-scripts`` and add it to ``dfhack-config/script-paths.txt``. You could also make a folder for external installed scripts from the internet that are not in ``hack/scripts/``. You can prepend your script paths entries with a ``+`` so that they take precedence over other folders.
You can make all your scripts in ``hack/scripts/``, but this is not recommended
as it makes things much harder to maintain each update. It's recommended to make
a folder with a name like ``own-scripts`` and add it to
``dfhack-config/script-paths.txt``. You could also make a folder for external
installed scripts from the internet that are not in ``hack/scripts/``. You can
prepend your script paths entries with a ``+`` so that they take precedence over
other folders.
If your mod is installed into ``raw/scripts/`` be aware that the copies of the scripts in ``data/save/*/raw/`` are checked first and will run instead of any changes you make to an in-development copy outside of a raw folder.
If your mod is installed into ``raw/scripts/`` be aware that the copies of the
scripts in ``data/save/*/raw/`` are checked first and will run instead of any
changes you make to an in-development copy outside of a raw folder.
The structure of the game
-------------------------
"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.
"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.
Your first script
-----------------
So! It's time to write your first script. We are going to make a script that will get the pronoun type of the currently selected unit (there are many contexts where the function that gets the currently selected unit works).
So! It's time to write your first script. We are going to make a script that
will get the pronoun type of the currently selected unit (there are many
contexts where the function that gets the currently selected unit works).
First line, we get the unit. ::
local unit = dfhack.gui.getSelectedUnit()
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``.
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``.
If ``unit`` is ``nil``, we don't want the script to run anymore. ::
@ -48,36 +77,60 @@ If ``unit`` is ``nil``, we don't want the script to run anymore. ::
return
end
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`` with an integer from the unit. Indexing the other way, with one of the strings, will yield its corresponding number. So: ::
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`` with an integer from the unit. Indexing the other way,
with one of the strings, will yield its corresponding number. So: ::
local pronounTypeString = df.pronoun_type[unit.sex]
print(pronounTypeString)
Simple. Save this as a Lua file in your own scripts directory and run it as shown before when focused on a unit one way or another.
Simple. Save this as a Lua file in your own scripts directory and run it as
shown before when focused on a unit one way or another.
Getting used to gm-editor and df-structures exploration
-------------------------------------------------------
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 (at https://github.com/DFHack/df-structures) that contains XML files denoting the contents of the game's structures. The second is the script ``gui/gm-editor`` which is an interactive data explorer. You can run the script while objects like units are selected to view the data within them. You can also pass ``scr`` as an argument to the script to view the data for the current screen. Press ? while the script is active to view help.
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
(at https://github.com/DFHack/df-structures) that contains XML files denoting
the contents of the game's structures. The second is the script
``gui/gm-editor`` which is an interactive data explorer. You can run the script
while objects like units are selected to view the data within them. You can also
pass ``scr`` as an argument to the script to view the data for the current
screen. Press ? while the script is active to view help.
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 (e.g. DFHack's Discord).
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 (e.g. DFHack's
Discord).
Detecting triggers
------------------
One main method for getting new behaviour into the game is callback functions. There are two main libraries for this, ``repeat-util`` and ``eventful``. ``repeat-util`` is used to run a function once per configurable number of frames (paused or unpaused), ticks (unpaused), in-game days, months, or years. For adding behaviour you will most often want something to run once a tick. ``eventful`` is used to get code to run (with special parameters!) when something happens in the game, like a reaction or job being completed or a projectile moving.
One main method for getting new behaviour into the game is callback functions.
There are two main libraries for this, ``repeat-util`` and ``eventful``.
``repeat-util`` is used to run a function once per configurable number of frames
(paused or unpaused), ticks (unpaused), in-game days, months, or years. For
adding behaviour you will most often want something to run once a tick.
``eventful`` is used to get code to run (with special parameters!) when
something happens in the game, like a reaction or job being completed or a
projectile moving.
To get something to run once per tick, we would want to call ``repeat-util``'s ``scheduleEvery`` function.
To get something to run once per tick, we would want to call ``repeat-util``'s
``scheduleEvery`` function.
First, we load the module: ::
local repeatUtil = require("repeat-util")
Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. It's recommended to use something like a mod id. ::
Both ``repeat-util`` and ``eventful`` require keys for registered callbacks.
It's recommended to use something like a mod id. ::
local modId = "callback-example-mod"
Then, we pass the key, amount of time units between function calls, what the time units are, and finally the callback function itself: ::
Then, we pass the key, amount of time units between function calls, what the
time units are, and finally the callback function itself: ::
repeatUtil.scheduleEvery(modId, 1, "ticks", function()
-- Do something like iterating over all active units
@ -90,20 +143,29 @@ Then, we pass the key, amount of time units between function calls, what the tim
local eventful = require("plugins.eventful")
``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: ::
``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: ::
eventful.onProjItemCheckMovement[modId] = function(projectile)
print(projectile.cur_pos.x, projectile.cur_pos.y, projectile.cur_pos.z)
end
Check the full list of events at https://docs.dfhack.org/en/stable/docs/Lua%20API.html#list-of-events.
Check the full list of events at
https://docs.dfhack.org/en/stable/docs/Lua%20API.html#list-of-events.
Custom raw tokens
-----------------
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. Both of these involve custom raw tokens.
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. Both of these involve
custom raw tokens.
First, let's define a custom crossbow with its own custom reaction. The crossbow: ::
First, let's define a custom crossbow with its own custom reaction. The
crossbow: ::
[ITEM_WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE]
[NAME:crossbow:crossbows]
@ -119,7 +181,8 @@ First, let's define a custom crossbow with its own custom reaction. The crossbow
[ATTACK_PREPARE_AND_RECOVER:3:3]
[SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER:2] custom token (you'll see)
The reaction to make it (you would add the reaction and not the weapon to an entity raw): ::
The reaction to make it (you would add the reaction and not the weapon to an
entity raw): ::
[REACTION:MAKE_SIEGE_CROSSBOW]
[NAME:make siege crossbow]
@ -132,10 +195,13 @@ The reaction to make it (you would add the reaction and not the weapon to an ent
[ANY_PLANT_MATERIAL]
[REAGENT:handle 2:1:BLOCKS:NONE:NONE:NONE]
[ANY_PLANT_MATERIAL]
[SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1] another custom token
[SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1]
another custom token
[PRODUCT:100:1:WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE:GET_MATERIAL_FROM_REAGENT:bar:NONE]
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.
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.
First, require the modules we are going to use. ::
@ -145,15 +211,20 @@ First, require the modules we are going to use. ::
Now, let's make a callback: ::
local modId = "siege-crossbow-mod"
eventful.onReactionComplete[modId] = function(reaction, reactionProduct, unit, inputItems, inputReagents, outputItems)
eventful.onReactionComplete[modId] = function(reaction, reactionProduct,
unit, inputItems, inputReagents, outputItems)
First, we check to see if it the reaction that just happened is relevant to this callback: ::
First, we check to see if it the reaction that just happened is relevant to this
callback: ::
if not customRawTokens.getToken(reaction, "SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT") then
if not customRawTokens.getToken(reaction,
"SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT")
then
return
end
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... ::
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... ::
for i, reagent in ipairs(inputReagents) do
if reagent.code:sub(1, #"handle") == "handle" then
@ -167,9 +238,9 @@ Then, we get the product number listed. Next, for every reagent, if the reagent
-- new.maker = outputItems[0].maker -- not a typical improvement
new.type = df.itemimprovement_specific_type.HANDLE
outputItems[productNumber - 1].improvements:insert("#", new)
-- break -- multiple handles, multiple "the handle is made from"s, so no break
It's all a bit loose and hacky but it works, at least if you don't have multiple stacks filling up one reagent.
It's all a bit loose and hacky but it works, at least if you don't have multiple
stacks filling up one reagent.
Let's also make some code to modify the fire rate of the siege crossbow. ::
@ -189,10 +260,12 @@ Let's also make some code to modify the fire rate of the siege crossbow. ::
end
local multiplier = tonumber(customRawTokens.getToken(weapon.subtype, "SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER")) or 1
firer.counters.think_counter = math.floor(firer.counters.think_counter * multiplier)
firer.counters.think_counter = math.floor(firer.counters.think_counter *
multiplier)
end
Now, let's see how we could make some "pegasus boots". First, let's define the item in the raws: ::
Now, let's see how we could make some "pegasus boots". First, let's define the
item in the raws: ::
[ITEM_SHOES:ITEM_SHOES_BOOTS_PEGASUS]
[NAME:pegasus boot:pegasus boots]
@ -207,13 +280,16 @@ Now, let's see how we could make some "pegasus boots". First, let's define the i
[METAL]
[LEATHER]
[HARD]
[PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token (you don't have to comment this every time)
[PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token
(you don't have to comment this every time)
Then, let's make a ``repeat-util`` callback for once a tick: ::
repeatUtil.scheduleEvery(modId, 1, "ticks", function()
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: ::
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: ::
for _, unit in ipairs(df.global.world.units.active) do
local amount = 0
@ -225,25 +301,44 @@ Now, we will add up the effect of all speed-increasing gear and apply it: ::
amount = amount + tonumber((customRawTokens.getToken(entry.item, "PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK")) or 0)
end
end
dfhack.units.addMoveTimer(-amount) -- Subtract amount from movement timer if currently moving
dfhack.units.addMoveTimer(-amount) -- Subtract amount from movement timer if
currently moving
The structure of a full mod
---------------------------
Now, you may have noticed that you won't be able to run multiple functions on tick/as event callbacks with that ``modId`` idea alone. To solve that we can 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 order if that is important. You will have to use your mod ID as a prefix if you register multiple ``repeat-util`` callbacks, though.
Now, you may have noticed that you won't be able to run multiple functions on
tick/as event callbacks with that ``modId`` idea alone. To solve that we can
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 order if that is important. You
will have to use your mod ID as a prefix if you register multiple
``repeat-util`` callbacks, though.
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:
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:
* The main content of the mod would be in the ``raw`` folder:
* A Lua file in ``raw/init.d/`` to initialise the mod by calling ``your-mod-id/main/ enable``.
* A Lua file in ``raw/init.d/`` to initialise the mod by calling
``your-mod-id/main/ enable``.
* Raw content (potentially with custom raw tokens) in ``raw/objects/``.
* 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.
* 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.
* Using git within each mod folder is recommended, but not required.
* A ``readme.md`` markdown file is also recommended.
* 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.
* 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.
Now, let's take a look at an example ``main.lua`` file. ::
@ -256,13 +351,19 @@ Now, let's take a look at an example ``main.lua`` file. ::
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
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
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
-- 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
-- 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()
@ -291,7 +392,8 @@ Now, let's take a look at an example ``main.lua`` file. ::
print("Example mod enabled")
elseif args[1] == "disable" then
-- Order: on unload, then cancel the callbacks in the same order as above
-- Order: on unload, then cancel the callbacks in the same order as
-- above
moduleA.onUnload()
@ -306,14 +408,18 @@ Now, let's take a look at an example ``main.lua`` file. ::
elseif not args[1] then
dfhack.printerr("No argument given to example-mod/main")
else
dfhack.printerr("Unknown argument \"" .. args[1] .. "\" to example-mod/main")
dfhack.printerr("Unknown argument \"" .. args[1] ..
"\" to example-mod/main")
end
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.
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: ::
dfhack.run_command("example-mod/main enable") -- Very simple. Could be called "init-example-mod.lua"
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: ::
@ -334,4 +440,7 @@ Here is what ``raw/scripts/module-a.lua`` would look like: ::
end
end
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.
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.