diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 286fe97e1..ba63030b5 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -30,6 +30,7 @@ implemented by Lua files located in :file:`hack/lua/*` :local: :depth: 2 +.. _lua-df: ========================= DF data structure wrapper @@ -5175,6 +5176,8 @@ General script API return end +.. _reqscript: + Importing scripts ================= diff --git a/docs/changelog.txt b/docs/changelog.txt index 5abae3dec..f46b197ab 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `quickfort`: `Dreamfort ` blueprint set improvements: set traffic designations to encourage dwarves to eat cooked food instead of raw ingredients ## Documentation +- Added `modding-guide` - Update all DFHack tool documentation (300+ pages) with standard syntax formatting, usage examples, and overall clarified text. - Group DFHack tools by `tag ` so similar tools are grouped and easy to find diff --git a/docs/guides/index.rst b/docs/guides/index.rst index d0125921f..161b1bb68 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -8,6 +8,7 @@ These pages are detailed guides covering DFHack tools. :maxdepth: 1 /docs/guides/examples-guide + /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide /docs/guides/quickfort-alias-guide diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst new file mode 100644 index 000000000..6ec80eed1 --- /dev/null +++ b/docs/guides/modding-guide.rst @@ -0,0 +1,486 @@ +.. _modding-guide: + +DFHack modding guide +==================== + +What is the difference between a script and a mod? +-------------------------------------------------- + +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. + +DFHack scripts are written in Lua. If you don't already know Lua, there's a +great primer at `lua.org `__. + +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 +apply 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. + +A mod-maker's development environment +------------------------------------- + +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. + +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 ` will +be: + +1. ``own-scripts/`` +2. ``data/save/*/raw/scripts/`` +3. ``raw/scripts/`` +4. ``hack/scripts/`` + +The structure of the game +------------------------- + +"The game" is in the global variable `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 +guide. We'll explore more of the game structures below. + +Your first script +----------------- + +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. + +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 ``unit`` is ``nil``, we don't want the script to run anymore:: + + if not unit then + 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``. Indexing the other way, incidentally, +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 a unit is selected in the Dwarf Fortress UI. + +Exploring DF structures +----------------------- + +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 `__ 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. + +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 `. + +Detecting triggers +------------------ + +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 `. +``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! + +``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. + +To get something to run once per tick, we can call +``repeat-util.scheduleEvery()``. First, we load the module:: + + local repeatUtil = require('repeat-util') + +Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. You +should use something unique, like your mod name:: + + 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:: + + repeatUtil.scheduleEvery(modId, 1, "ticks", function() + -- Do something like iterating over all active units and + -- check for something interesting + for _, unit in ipairs(df.global.world.units.active) do + ... + end + end) + +``eventful`` is slightly more involved. First get the module:: + + 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.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 ` to see what else +you can react to with ``eventful``. + +Now, you may have noticed that you won't be able to register multiple callbacks +with a single key named after your mod. You can, of course, call all the +functions you want from a single registed callback. Alternately, you can create +multiple callbacks using different keys, using your mod ID as a key name prefix. +If you do register multiple callbacks, though, there are no guarantees about the +call order. + +Custom raw tokens +----------------- + +In this section, we are going to use `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. + +First, let's define a custom crossbow with its own custom reaction. The +crossbow:: + + [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] + [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):: + + [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] + [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. + +First, require the modules we are going to use:: + + 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):: + + local modId = "siege-crossbow-mod" + 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:: + + 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... + +:: + + for i, reagent in ipairs(inputReagents) do + if reagent.code:startswith('handle') then + -- Found handle reagent + local item = inputItems[i] + +...We then add a handle improvement to the listed product within our loop:: + + 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) + +This works well as long as you don't have multiple stacks filling up one +reagent. + +Let's also make some code to modify the fire rate of our siege crossbow:: + + eventful.onProjItemCheckMovement[modId] = function(projectile) + if projectile.distance_flown > 0 then + -- don't make this adjustment more than once + 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 + + 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) + end + +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] + [ARMORLEVEL:1] + [UPSTEP:1] + [METAL_ARMOR_LEVELS] + [LAYER:OVER] + [COVERAGE:100] + [LAYER_SIZE:25] + [LAYER_PERMIT:15] + [MATERIAL_SIZE:2] + [METAL] + [LEATHER] + [HARD] + [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) + +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, iterate over their +worn items to calculate how much we are going to take from their movement +timer:: + + for _, unit in ipairs(df.global.world.units.active) do + local amount = 0 + for _, entry in ipairs(unit.inventory) do + if entry.mode == df.unit_inventory_item.T_mode.Worn then + local reduction = customRawTokens.getToken( + entry.item, + 'PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK') + amount = amount + (tonumber(reduction) or 0) + end + end + end + + -- Subtract amount from movement timer if currently moving + dfhack.units.addMoveTimer(-amount) + +The structure of a full mod +--------------------------- + +Create a folder for mod projects somewhere outside your Dwarf Fortress +installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the +names for the mod folders within it. In the example below, we'll use a mod ID of +``example-mod``. I'm sure your mods will have more creative names! The +``example-mod`` mod will be developed in the ``/path/to/mymods/example-mod/`` +directory and has a basic structure that looks like this:: + + raw/init.d/example-mod.lua + raw/objects/... + raw/scripts/example-mod.lua + raw/scripts/example-mod/... + README.md + +Let's go through that line by line. + +* A short (one-line) script in ``raw/init.d/`` to initialise your + mod when a save is loaded. +* Modifications to the game raws (potentially with custom raw tokens) go in + ``raw/objects/``. +* A control script in ``raw/scripts/`` that handles enabling and disabling your + mod. +* A subfolder for your mod under ``raw/scripts/`` will contain all the internal + scripts and/or modules used by your mod. + +It is a good idea to use a version control system to organize changes to your +mod code. You can create a separate Git repository for each of your mods. The +``README.md`` file will be your mod help text when people browse to your online +repository. + +Unless you want to install your ``raw/`` folder into your DF game folder every +time you make a change to your scripts, you should add your development scripts +directory to your script paths in ``dfhack-config/script-paths.txt``:: + + +/path/to/mymods/example-mod/raw/scripts/ + +Ok, you're all set up! Now, let's take a look at an example +``raw/scripts/example-mod.lua`` file:: + + -- main setup and teardown for example-mod + -- this next line indicates that the script supports the "enable" + -- API so you can start it by running "enable example-mod" and stop + -- it by running "disable example-mod" + --@ enable = true + + local usage = [[ + Usage + ----- + + enable example-mod + disable example-mod + ]] + local repeatUtil = require('repeat-util') + local eventful = require('plugins.eventful') + + -- you can reference global values or functions declared in any of + -- your internal scripts + local moduleA = reqscript('example-mod/module-a') + local moduleB = reqscript('example-mod/module-b') + local moduleC = reqscript('example-mod/module-c') + local moduleD = reqscript('example-mod/module-d') + + enabled = enabled or false + local modId = 'example-mod' + + if not dfhack_flags.enable then + print(usage) + print() + print(('Example mod is currently '):format( + enabled and 'enabled' or 'disabled')) + return + end + + if dfhack_flags.enable_state then + -- do any initialization your internal scripts might require + moduleA.onLoad() + moduleB.onLoad() + + -- register your callbacks + repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', + moduleA.every1Tick) + repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', + moduleD.every100Frames) + + -- multiple functions in the same callback + eventful.onReactionComplete[modId] = function(reaction, + reaction_product, unit, input_items, input_reagents, + output_items) + -- pass the event's parameters to the listeners + moduleB.onReactionComplete(reaction, reaction_product, + unit, input_items, input_reagents, output_items) + moduleC.onReactionComplete(reaction, reaction_product, + unit, input_items, input_reagents, output_items) + end + + -- one function per callback (you can put them in the + -- above format if you prefer) + eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement + eventful.onProjUnitCheckMovement[modId] = moduleD.onProjUnitCheckMovement + + print('Example mod enabled') + enabled = true + else + -- call any shutdown functions your internal scripts might require + moduleA.onUnload() + + repeatUtil.cancel(modId .. ' every ticks') + repeatUtil.cancel(modId .. ' 100 frames') + + eventful.onReactionComplete[modId] = nil + eventful.onProjItemCheckMovement[modId] = nil + eventful.onProjUnitCheckMovement[modId] = nil + + print('Example mod disabled') + enabled = false + end + +You can call ``enable example-mod`` and ``disable example-mod`` yourself while +developing, but for end users you can start your mod automatically from +``raw/init.d/example-mod.lua``:: + + dfhack.run_command('enable example-mod') + +Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: + + --@ module = true + -- The above line is required for reqscript to work + + function onLoad() -- global variables are exported + -- do initialization here + end + + -- this is an internal function: local functions/variables + -- are not exported + local function usedByOnTick(unit) + -- ... + end + + function onTick() -- exported + for _,unit in ipairs(df.global.world.units.all) do + usedByOnTick(unit) + end + end + +The `reqscript ` function reloads scripts that have changed, so you can modify +your scripts while DF is running and just disable/enable your mod to load the +changes into your ongoing game!