From 0ae8a420800540692916395fbc8b629e6f9a8179 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 20 Jun 2022 19:38:23 +0100 Subject: [PATCH 01/46] Unfinished modding guide --- docs/changelog.txt | 5 +++ docs/guides/modding-guide.rst | 74 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/guides/modding-guide.rst diff --git a/docs/changelog.txt b/docs/changelog.txt index e91a427a9..e944b7799 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -69,6 +69,11 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `quickfort-alias-guide`: new aliases: ``forbidthread``, ``permitthread``, ``forbidadamantinethread``, ``permitadamantinethread``, ``forbidcloth``, ``permitcloth``, ``forbidadamantinecloth``, and ``permitadamantinecloth`` give you more control how adamantine-derived items are stored - `quickfort`: `Dreamfort ` blueprint set improvements: automatically create tavern, library, and temple locations (restricted to residents only by default), automatically associate the rented rooms with the tavern - `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including were-bitten hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. +- `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including a werebeast-proof hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. + +## Documentation +- Added modding guide +- Added anchor for DF data structure wrapper in Lua API ## API - ``word_wrap``: argument ``bool collapse_whitespace`` converted to enum ``word_wrap_whitespace_mode mode``, with valid modes ``WSMODE_KEEP_ALL``, ``WSMODE_COLLAPSE_ALL``, and ``WSMODE_TRIM_LEADING``. diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst new file mode 100644 index 000000000..d2c453829 --- /dev/null +++ b/docs/guides/modding-guide.rst @@ -0,0 +1,74 @@ +DFHack modding guide +==================== + +Preface +------- + +Why not just use raw modding? + +It's much less hacky for many things, easier to maintain, and easier to extend. Of course if you're adding a new creature or whatever do use raw modding, but for things like adding syndromes when certain actions are performed, absolutely try out DFHack. Many things will require a mix of raw modding and DFHack. + +What kind of things can we make? + +Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. Familiarising yourself with the many structs oof the game will help with ideas immensely, and you can always ask for help in the right places (e.g. DFHack's Discord). + +You will need to know Lua for the scripting guides herein. + +A script-maker's environment +---------------------------- + +A script is run by writing its path and name from a script path folder without the file extension. 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 should also make a folder for external installed scripts from the internet that are not in 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. + +A script that prints data from a unit +------------------------------------- + +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 ``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`` 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. + +Getting used to gm-editor and DFStructs exploration +--------------------------------------------------- + +So how could you have known about the field and type we just used? + +A script that prints more complex data from a unit +-------------------------------------------------- + +s + +Something that uses eventful and/or repeat-util +----------------------------------------------- + +s + +Setting up an environment for a more advanced modular mod +--------------------------------------------------------- + +s + +A whole mod with multiple hooks and multiple functions that happen on tick +-------------------------------------------------------------------------- + +s From 063e4897a84b8579bfcfd022f02602dff26ddb62 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 20 Jun 2022 20:31:19 +0100 Subject: [PATCH 02/46] Forgot to stage a change --- docs/Lua API.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 55a217757..f2f252c4b 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 From a880a2b92d5cb8e7c3f15d7c80315cee736b24c6 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:48:28 +0100 Subject: [PATCH 03/46] Move changelog entries to proper place --- docs/changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e944b7799..a07335715 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,6 +42,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements ## Documentation +- Added modding guide +- Added anchor for DF data structure wrapper in Lua API ## API @@ -72,8 +74,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including a werebeast-proof hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. ## Documentation -- Added modding guide -- Added anchor for DF data structure wrapper in Lua API ## API - ``word_wrap``: argument ``bool collapse_whitespace`` converted to enum ``word_wrap_whitespace_mode mode``, with valid modes ``WSMODE_KEEP_ALL``, ``WSMODE_COLLAPSE_ALL``, and ``WSMODE_TRIM_LEADING``. From ccee6ba48700c16af900e7ff0a5cec54e1cfa0c4 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:53:59 +0100 Subject: [PATCH 04/46] Added link to modding guide from changelog --- docs/changelog.txt | 2 +- docs/guides/modding-guide.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a07335715..2118a5380 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,7 +42,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements ## Documentation -- Added modding guide +- Added `modding guide ` - Added anchor for DF data structure wrapper in Lua API ## API diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d2c453829..5d55ca44e 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -1,3 +1,5 @@ +.. _modding-guide: + DFHack modding guide ==================== From 4c2a533de0960a8fc27d0105c56a550b6ffb0fdb Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:54:28 +0100 Subject: [PATCH 05/46] Remove "added anchor" from changelog --- docs/changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2118a5380..a54957796 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -43,7 +43,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Documentation - Added `modding guide ` -- Added anchor for DF data structure wrapper in Lua API ## API From 88a9755d785e2ecd751c7e0ee1711aadf188eb85 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:54:45 +0100 Subject: [PATCH 06/46] Update docs/guides/modding-guide.rst Co-authored-by: Myk --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 5d55ca44e..58f32fa28 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -26,7 +26,7 @@ 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. -A script that prints data from a unit +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). From 92ab6b10529509b804bae8a4d86397da1cade856 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:58:29 +0100 Subject: [PATCH 07/46] Update docs/guides/modding-guide.rst Co-authored-by: Myk --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 58f32fa28..dfd143e08 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -14,7 +14,7 @@ What kind of things can we make? Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. Familiarising yourself with the many structs oof the game will help with ideas immensely, and you can always ask for help in the right places (e.g. DFHack's Discord). -You will need to know Lua for the scripting guides herein. +DFHack scripts are written in Lua. If you don't already know Lua, there's a great primer at . A script-maker's environment ---------------------------- From 52161df42863a14c18c43a95a90495eaf7a2811d Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:58:36 +0100 Subject: [PATCH 08/46] Update docs/guides/modding-guide.rst Co-authored-by: Myk --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index dfd143e08..d4514156e 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -16,7 +16,7 @@ Lots of things, and the list grows as more and more hooks and tools are develope DFHack scripts are written in Lua. If you don't already know Lua, there's a great primer at . -A script-maker's environment +A mod-maker's development environment ---------------------------- A script is run by writing its path and name from a script path folder without the file extension. 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 should also make a folder for external installed scripts from the internet that are not in hack/scripts/. From 6f2998d7e5514f4a7bbd922f33dae4efdc33c901 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 12:58:40 +0100 Subject: [PATCH 09/46] Update docs/guides/modding-guide.rst Co-authored-by: Myk --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d4514156e..c8f13baf7 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -10,7 +10,7 @@ Why not just use raw modding? It's much less hacky for many things, easier to maintain, and easier to extend. Of course if you're adding a new creature or whatever do use raw modding, but for things like adding syndromes when certain actions are performed, absolutely try out DFHack. Many things will require a mix of raw modding and DFHack. -What kind of things can we make? +What kind of mods can we make? Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. Familiarising yourself with the many structs oof the game will help with ideas immensely, and you can always ask for help in the right places (e.g. DFHack's Discord). From 41526f376d9bc9bcb68b6526eb4f31e72bb6003e Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 13:02:10 +0100 Subject: [PATCH 10/46] Fix wrong - length --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index c8f13baf7..e60bc2650 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -17,7 +17,7 @@ Lots of things, and the list grows as more and more hooks and tools are develope DFHack scripts are written in Lua. If you don't already know Lua, there's a great primer at . A mod-maker's development environment ----------------------------- +------------------------------------- A script is run by writing its path and name from a script path folder without the file extension. 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 should also make a folder for external installed scripts from the internet that are not in hack/scripts/. @@ -27,7 +27,7 @@ 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. 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). From a9ab415ac7f5443a7129db461055125a7d808f6d Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 15:20:49 +0100 Subject: [PATCH 11/46] Clean merge heckery and clean up label reference --- docs/changelog.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a54957796..797a79f3a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,7 +42,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements ## Documentation -- Added `modding guide ` +- Added `modding-guide` ## API @@ -70,9 +70,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `quickfort-alias-guide`: new aliases: ``forbidthread``, ``permitthread``, ``forbidadamantinethread``, ``permitadamantinethread``, ``forbidcloth``, ``permitcloth``, ``forbidadamantinecloth``, and ``permitadamantinecloth`` give you more control how adamantine-derived items are stored - `quickfort`: `Dreamfort ` blueprint set improvements: automatically create tavern, library, and temple locations (restricted to residents only by default), automatically associate the rented rooms with the tavern - `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including were-bitten hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. -- `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including a werebeast-proof hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. - -## Documentation ## API - ``word_wrap``: argument ``bool collapse_whitespace`` converted to enum ``word_wrap_whitespace_mode mode``, with valid modes ``WSMODE_KEEP_ALL``, ``WSMODE_COLLAPSE_ALL``, and ``WSMODE_TRIM_LEADING``. From ac864204c2258151b05bbe019c006c26e2c14e6b Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 21:07:35 +0100 Subject: [PATCH 12/46] More work on guide, added to existing sections and revised sections list --- docs/guides/modding-guide.rst | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index e60bc2650..f124ba121 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -8,18 +8,24 @@ Preface Why not just use raw modding? -It's much less hacky for many things, easier to maintain, and easier to extend. Of course if you're adding a new creature or whatever do use raw modding, but for things like adding syndromes when certain actions are performed, absolutely try out 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. What kind of mods can we make? -Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. Familiarising yourself with the many structs oof the game will help with ideas immensely, and you can always ask for help in the right places (e.g. DFHack's Discord). +Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. 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). -DFHack scripts are written in Lua. If you don't already know Lua, there's a great primer at . +Examples of things we have mod tools to allow are directly changing skills, spawning liquids, adding new powered buildings, creating items/trees/units based on various conditions (in reactions or similar), running code when certain projectiles hit or move, and much else besides. + +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. A mod-maker's development environment ------------------------------------- -A script is run by writing its path and name from a script path folder without the file extension. 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 should also make a folder for external installed scripts from the internet that are not in hack/scripts/. +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``. + +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 should 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. The structure of the game ------------------------- @@ -48,20 +54,17 @@ Now, the field ``sex`` in a unit is an integer, but each integer corresponds to local pronounTypeString = df.pronoun_type[unit.sex] print(pronounTypeString) -Simple. +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 DFStructs exploration --------------------------------------------------- So how could you have known about the field and type we just used? -A script that prints more complex data from a unit --------------------------------------------------- - s -Something that uses eventful and/or repeat-util ------------------------------------------------ +Detecting triggers +------------------ s @@ -70,7 +73,7 @@ Setting up an environment for a more advanced modular mod s -A whole mod with multiple hooks and multiple functions that happen on tick --------------------------------------------------------------------------- +Your first whole mod +-------------------- s From f812e09ae1470b26d3375337bc98de90f27692b4 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 21 Jun 2022 21:35:07 +0100 Subject: [PATCH 13/46] Update modding guide with an extra paragraph and syntax fixes --- docs/guides/modding-guide.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index f124ba121..d3bf384c8 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -27,6 +27,8 @@ A script is run by writing its path and name from a script path folder without t 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 should 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. + The structure of the game ------------------------- @@ -37,19 +39,19 @@ 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). -First line, we get the 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 ``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`` 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) From c026bd6dcb659cef31e2481bb38a1d01aaec22c5 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Fri, 1 Jul 2022 12:04:18 +0100 Subject: [PATCH 14/46] Split sections a bit --- docs/guides/modding-guide.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d3bf384c8..cb37de575 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -3,20 +3,17 @@ DFHack modding guide ==================== -Preface -------- +What is a mod/script? +--------------------- -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. - -What kind of mods can we make? +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. -Lots of things, and the list grows as more and more hooks and tools are developed within DFHack. You can modify behaviours by cleverly watching and modifying certain areas of the game with functions that run on every tick or by using hooks. 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). +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. -Examples of things we have mod tools to allow are directly changing skills, spawning liquids, adding new powered buildings, creating items/trees/units based on various conditions (in reactions or similar), running code when certain projectiles hit or move, and much else besides. +Why not just use raw modding? +----------------------------- -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. +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 ------------------------------------- From 46d0f36d412eedf2bd02bbd7ac901cb29536f095 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Fri, 1 Jul 2022 19:10:26 +0100 Subject: [PATCH 15/46] Pad out Getting used to gm-editor and DFStructs exploration a bit --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index cb37de575..c35e2a41a 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -58,9 +58,9 @@ Simple. Save this as a Lua file in your own scripts directory and run it as show Getting used to gm-editor and DFStructs exploration --------------------------------------------------- -So how could you have known about the field and type we just used? +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. -s +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 ------------------ From b86d16d64ceb76745179936771eea724c2476b12 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Sun, 3 Jul 2022 15:59:01 +0100 Subject: [PATCH 16/46] Some of Detecting Triggers --- docs/guides/modding-guide.rst | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index c35e2a41a..9bbdc9bfd 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -65,7 +65,30 @@ Familiarising yourself with the many structs of the game will help with ideas im Detecting triggers ------------------ -s +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. + +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. :: + + local modId = "my-test-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 units + for _, unit in ipairs(df.global.world.units.all) do + print(unit.id) + end + end) + +``eventful`` is slightly more involved: + +TODO Setting up an environment for a more advanced modular mod --------------------------------------------------------- From f8a8bf6e2993a2ac5a548e2406223a9757b154e6 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Sun, 3 Jul 2022 17:33:36 +0100 Subject: [PATCH 17/46] Some more --- docs/guides/modding-guide.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 9bbdc9bfd..c6ae8ae15 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -77,7 +77,7 @@ Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. It' local modId = "my-test-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 units @@ -86,9 +86,12 @@ Then, we pass the key, amount of time units between function calls, what the tim end end) -``eventful`` is slightly more involved: +``eventful`` is slightly more involved. :: -TODO + local eventful = require("plugins.eventful") + -- blah + +Check the full list of Eventful events at https://docs.dfhack.org/en/stable/docs/Lua%20API.html#list-of-events. Setting up an environment for a more advanced modular mod --------------------------------------------------------- From 0237567c183f7a50305b2db47a2c0902a8eb3189 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 4 Jul 2022 17:11:21 +0100 Subject: [PATCH 18/46] More modding guide --- docs/guides/modding-guide.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index c6ae8ae15..b3b5c515b 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -86,17 +86,24 @@ Then, we pass the key, amount of time units between function calls, what the tim end end) -``eventful`` is slightly more involved. :: +``eventful`` is slightly more involved. First get the module: :: local eventful = require("plugins.eventful") - -- blah -Check the full list of Eventful events at https://docs.dfhack.org/en/stable/docs/Lua%20API.html#list-of-events. +``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. Setting up an environment for a more advanced modular mod --------------------------------------------------------- -s +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. + +TODO Your first whole mod -------------------- From 15aae9cf1ef90c862d75c28e4d81d93764c59a6e Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Thu, 7 Jul 2022 19:23:10 +0100 Subject: [PATCH 19/46] More guide --- docs/guides/modding-guide.rst | 79 ++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index b3b5c515b..085c53a1e 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -98,14 +98,83 @@ Then, we pass the key, amount of time units between function calls, what the tim Check the full list of events at https://docs.dfhack.org/en/stable/docs/Lua%20API.html#list-of-events. -Setting up an environment for a more advanced modular mod ---------------------------------------------------------- +Custom raw tokens +----------------- -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. +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. Both of these involve custom raw tokens. + +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] + [FIRE_TIME:100] 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] + [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. :: -TODO + local eventful = require("plugins.eventful") + local customRawTokens = require("custom-raw-tokens") + +Now, let's make a callback: :: + + local modId = "siege-crossbow" + 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, "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:sub(1, #"handle") == "handle" then + -- Found handle reagent + local item = inputItems[i] -- hopefully found handle item + +...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.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. + +TODO: fire rate +TODO: "running shoes" Your first whole mod -------------------- -s +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. + +TODO From 25175b5c2811d5f1a2da512485b8e1679ad2cde5 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Sat, 9 Jul 2022 17:04:20 +0100 Subject: [PATCH 20/46] Fire rate code --- docs/guides/modding-guide.rst | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 085c53a1e..15e96599d 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -117,7 +117,7 @@ First, let's define a custom crossbow with its own custom reaction. The crossbow [MATERIAL_SIZE:4] [ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250] [ATTACK_PREPARE_AND_RECOVER:3:3] - [FIRE_TIME:100] custom token (you'll see) + [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): :: @@ -169,7 +169,27 @@ Then, we get the product number listed. Next, for every reagent, if the 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. -TODO: fire rate +Let's also make some code to modify the fire rate of the siege crossbow. :: + + eventful.onProjItemCheckMovement[modId] = function(projectile) + if projectile.distance_flown > 0 then -- don't repeat this + 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, "FIRE_RATE_MULTIPLIER")) or 1 + firer.counters.think_counter = math.floor(firer.counters.think_counter * multiplier) + end + TODO: "running shoes" Your first whole mod From f670ef4b60a694f565745076df6d862cb8d6838e Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 11 Jul 2022 18:24:07 +0100 Subject: [PATCH 21/46] Add custom raw tokens link and add prefix to custom raw tokens in raws --- docs/guides/modding-guide.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 15e96599d..ea7ae9eb7 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -101,7 +101,7 @@ Check the full list of events at https://docs.dfhack.org/en/stable/docs/Lua%20AP 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. 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: :: @@ -117,7 +117,7 @@ First, let's define a custom crossbow with its own custom reaction. The crossbow [MATERIAL_SIZE:4] [ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250] [ATTACK_PREPARE_AND_RECOVER:3:3] - [FIRE_RATE_MULTIPLIER:2] custom token (you'll see) + [TUTORIAL_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): :: @@ -132,7 +132,7 @@ 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] - [TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1] another custom token + [TUTORIAL_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. @@ -149,7 +149,9 @@ Now, let's make a callback: :: First, we check to see if it the reaction that just happened is relevant to this callback: :: - if not customRawTokens.getToken(reaction, "TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT") then return end + if not customRawTokens.getToken(reaction, "TUTORIAL_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... :: @@ -186,7 +188,7 @@ Let's also make some code to modify the fire rate of the siege crossbow. :: return end - local multiplier = tonumber(customRawTokens.getToken(weapon.subtype, "FIRE_RATE_MULTIPLIER")) or 1 + local multiplier = tonumber(customRawTokens.getToken(weapon.subtype, "TUTORIAL_MOD_FIRE_RATE_MULTIPLIER")) or 1 firer.counters.think_counter = math.floor(firer.counters.think_counter * multiplier) end From aa8809833b4048ca2a8d4d0f5489058db67309f7 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 11 Jul 2022 18:56:28 +0100 Subject: [PATCH 22/46] Do the "running shoes" TODO --- docs/guides/modding-guide.rst | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index ea7ae9eb7..5cafba37d 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -192,7 +192,40 @@ Let's also make some code to modify the fire rate of the siege crossbow. :: firer.counters.think_counter = math.floor(firer.counters.think_counter * multiplier) end -TODO: "running shoes" +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] + [EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token (you don't have to say 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: :: + + 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: :: + + if entry.mode == df.unit_inventory_item.T_mode.Worn then + amount = amount + tonumber((customRawTokens.getToken(entry.item, "EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK")) or 0) + end + end + dfhack.units.addMoveTimer(-amount) -- Subtract amount from movement timer if currently moving Your first whole mod -------------------- From c9cffc7da92e25bf9a1032e18a81eb000aac8f73 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 12 Jul 2022 11:26:49 +0100 Subject: [PATCH 23/46] Fix docs bugs --- docs/guides/modding-guide.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 5cafba37d..990c0ce96 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -101,7 +101,7 @@ Check the full list of events at https://docs.dfhack.org/en/stable/docs/Lua%20AP 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 ` 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: :: @@ -194,20 +194,20 @@ Let's also make some code to modify the fire rate of the siege crossbow. :: 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] - [EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token (you don't have to say this every time) + [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] + [EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token (you don't have to say this every time) Then, let's make a ``repeat-util`` callback for once a tick: :: From c39e2fe2cbf88a2e88552f7b09f2bdc28d199dca Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Wed, 13 Jul 2022 22:42:35 +0100 Subject: [PATCH 24/46] Mod structure --- docs/guides/modding-guide.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 990c0ce96..abc5e5e01 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -232,4 +232,15 @@ Your first whole 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. -TODO +Create a folder for mod projects within your Dwarf Fortress directory somewhere (e.g. ``hack/my-scripts/mods/``) 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``. + * 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) too. + +* Using git within each mod folder is recommended, but not required. +* A ``readme.mkd`` 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. From 9c04a28bd96503e4a2ff1b2e52745b7326f5a4ff Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Wed, 13 Jul 2022 22:43:01 +0100 Subject: [PATCH 25/46] Shouldn't've replaced the TODO --- docs/guides/modding-guide.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index abc5e5e01..d52607798 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -244,3 +244,5 @@ Create a folder for mod projects within your Dwarf Fortress directory somewhere * A ``readme.mkd`` 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. + +TODO From d7976e63b60ab3664485f7b16b9c8101a8d8e654 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Wed, 13 Jul 2022 22:44:38 +0100 Subject: [PATCH 26/46] const example. also, (preemptively acquiescing) readme.mkd --> readme.md --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d52607798..033ddc731 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -238,10 +238,10 @@ Create a folder for mod projects within your Dwarf Fortress directory somewhere * 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) 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.mkd`` markdown file is also recommended. +* 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. From a996b29cf5845024ac50dacb7c35c097c28abcda Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Thu, 14 Jul 2022 11:10:23 +0100 Subject: [PATCH 27/46] Some editing of mod guide --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 033ddc731..6b18188a0 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -230,9 +230,9 @@ Now, we will add up the effect of all speed-increasing gear and apply it: :: Your first whole 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. +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. -Create a folder for mod projects within your Dwarf Fortress directory somewhere (e.g. ``hack/my-scripts/mods/``) 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: From d06a63e4dc583732f70bce03945f6da20d1e4e6d Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Thu, 14 Jul 2022 11:11:50 +0100 Subject: [PATCH 28/46] DFStructs (a nickname?) --> df-structures --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 6b18188a0..1badad2dd 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -55,8 +55,8 @@ Now, the field ``sex`` in a unit is an integer, but each integer corresponds to 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 DFStructs exploration ---------------------------------------------------- +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. From 3f848b8836ef8eeb6cc720c298539b2e62696998 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Thu, 14 Jul 2022 17:46:12 +0100 Subject: [PATCH 29/46] Misc minor changes to modding guide --- docs/guides/modding-guide.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 1badad2dd..d0c9cb398 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -22,7 +22,7 @@ Scripts can be run from a world's ``raw/scripts/`` directory, and (configurably) 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 should 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. @@ -75,13 +75,13 @@ First, we load the module: :: Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. It's recommended to use something like a mod id. :: - local modId = "my-test-mod" + 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 units - for _, unit in ipairs(df.global.world.units.all) do + -- Do something like iterating over all active units + for _, unit in ipairs(df.global.world.units.active) do print(unit.id) end end) @@ -117,7 +117,7 @@ First, let's define a custom crossbow with its own custom reaction. The crossbow [MATERIAL_SIZE:4] [ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250] [ATTACK_PREPARE_AND_RECOVER:3:3] - [TUTORIAL_MOD_FIRE_RATE_MULTIPLIER:2] custom token (you'll see) + [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): :: @@ -132,7 +132,7 @@ 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] - [TUTORIAL_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. @@ -144,12 +144,12 @@ First, require the modules we are going to use. :: Now, let's make a callback: :: - local modId = "siege-crossbow" + 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, "TUTORIAL_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 @@ -188,7 +188,7 @@ Let's also make some code to modify the fire rate of the siege crossbow. :: return end - local multiplier = tonumber(customRawTokens.getToken(weapon.subtype, "TUTORIAL_MOD_FIRE_RATE_MULTIPLIER")) or 1 + 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 @@ -207,7 +207,7 @@ Now, let's see how we could make some "pegasus boots". First, let's define the i [METAL] [LEATHER] [HARD] - [EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token (you don't have to say 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: :: @@ -222,7 +222,7 @@ Let's iterate over every active unit, and for every unit, initialise a variable Now, we will add up the effect of all speed-increasing gear and apply it: :: if entry.mode == df.unit_inventory_item.T_mode.Worn then - amount = amount + tonumber((customRawTokens.getToken(entry.item, "EXAMPLE_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK")) or 0) + 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 From 0e704f39f7cdb4710923df648a000e7db635c0ed Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Fri, 15 Jul 2022 19:28:59 +0100 Subject: [PATCH 30/46] Got the really big part of the guide done --- docs/Lua API.rst | 2 + docs/guides/modding-guide.rst | 97 +++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index f2f252c4b..c459bf1f2 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -5040,6 +5040,8 @@ General script API Importing scripts ================= +.. _reqscript: + * ``dfhack.reqscript(name)`` or ``reqscript(name)`` Loads a Lua script and returns its environment (i.e. a table of all global diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d0c9cb398..aa07ca8a1 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -227,10 +227,10 @@ Now, we will add up the effect of all speed-increasing gear and apply it: :: end dfhack.units.addMoveTimer(-amount) -- Subtract amount from movement timer if currently moving -Your first whole mod --------------------- +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. +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: @@ -245,4 +245,93 @@ Create a folder for mod projects somewhere (e.g. ``hack/my-scripts/mods/``, or m * 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. -TODO +Now, let's take a look at an example ``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 + 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 + + 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 + -- 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 + 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. + +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" + +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 + +It is recommended to check `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. From fcd8839c0d81da1e47ab7e2902caf91e27feaa97 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Fri, 15 Jul 2022 19:43:48 +0100 Subject: [PATCH 31/46] Wrap lines to 80 characters --- docs/guides/modding-guide.rst | 213 +++++++++++++++++++++++++--------- 1 file changed, 161 insertions(+), 52 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index aa07ca8a1..cebd34745 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -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 `. 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 `. 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 ` 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 ` +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 `'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 `'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. From 7cf5a7dac90a5675c90336e67de56dad94d22aa9 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Fri, 15 Jul 2022 19:44:21 +0100 Subject: [PATCH 32/46] main.lua --> raw/scripts/main.lua --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index cebd34745..e35422aef 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -340,7 +340,7 @@ environment for fully-functioning modular mods are as follows: 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. :: +Now, let's take a look at an example ``raw/scripts/main.lua`` file. :: local repeatUtil = require("repeat-util") local eventful = require("plugins.eventful") From 253a1a80ada0928d0ac660ec38b545c3f4f2bc91 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 11 Sep 2022 23:02:04 -0700 Subject: [PATCH 33/46] add modding guide to guides index --- docs/guides/index.rst | 1 + 1 file changed, 1 insertion(+) 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 From 540a2a9fa75218b1f931f192277777b891d0ac74 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 12 Sep 2022 00:14:03 -0700 Subject: [PATCH 34/46] editing pass up through The structure of the game --- docs/guides/modding-guide.rst | 79 ++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index e35422aef..45f922b10 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -3,51 +3,61 @@ DFHack modding guide ==================== -What is a mod/script? ---------------------- +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. DFHack -mods contain and use scripts as well as often having a raw mod component. +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 https://www.lua.org/pil/1.html. +great primer at `lua.org `__. -Why not just use raw modding? ------------------------------ +Why not just mod the raws? +-------------------------- -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. +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. 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. +While you're writing your mod, you need a place to store your in-development scripts +that will: -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``. +- be directly runnable by DFHack +- not get lost when you upgrade DFHack -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. +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. -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. +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 ------------------------- @@ -55,7 +65,8 @@ 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. +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 ----------------- @@ -88,8 +99,8 @@ with one of the strings, will yield its corresponding number. So: :: 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 -------------------------------------------------------- +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 From 4a7faeef553da99c51c230595a47ccb733769ca3 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 12 Sep 2022 14:51:26 -0700 Subject: [PATCH 35/46] editing pass up through Custom raw tokens --- docs/guides/modding-guide.rst | 134 +++++++++++++++++----------------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 45f922b10..2eb8c7424 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -71,100 +71,103 @@ guide. We'll explore more of the game structures `below `__ 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 entities 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 (e.g. DFHack's -Discord). +immensely, and you can always ask for help in the `right places `. 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. +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! -To get something to run once per tick, we would want to call ``repeat-util``'s -``scheduleEvery`` function. +``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. -First, we load the module: :: +To get something to run once per tick, we can call ``repeat-util.scheduleEvery()``. +First, we load the module:: - local repeatUtil = require("repeat-util") + 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. :: +You should use something unique, like your mod name, perhaps with a suffix if you +are registering multiple keys:: 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: :: +time units are, and finally the callback function itself:: repeatUtil.scheduleEvery(modId, 1, "ticks", function() - -- Do something like iterating over all active units + -- Do something like iterating over all active units and check + -- for something interesting for _, unit in ipairs(df.global.world.units.active) do - print(unit.id) + ... end end) -``eventful`` is slightly more involved. First get the module: :: +``eventful`` is slightly more involved. First get the module:: - local eventful = require("plugins.eventful") + 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: :: +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 out the `full list of supported events ` to see what else +you can react to with ``eventful``. Custom raw tokens ----------------- @@ -172,11 +175,10 @@ 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. Both of these involve -custom raw tokens. +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: :: +crossbow:: [ITEM_WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE] [NAME:crossbow:crossbows] @@ -193,7 +195,7 @@ crossbow: :: [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): :: +entity raw):: [REACTION:MAKE_SIEGE_CROSSBOW] [NAME:make siege crossbow] @@ -214,19 +216,19 @@ 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. :: +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: :: +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: :: +callback:: if not customRawTokens.getToken(reaction, "SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT") @@ -235,28 +237,30 @@ callback: :: 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... :: +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 + if reagent.code:startswith('handle') then -- Found handle reagent - local item = inputItems[i] -- hopefully found handle item + local item = inputItems[i] -...We then add a handle improvement to the listed product within our loop. :: +...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.maker = outputItems[0].maker -- not a typical improvement new.type = df.itemimprovement_specific_type.HANDLE - outputItems[productNumber - 1].improvements:insert("#", new) + outputItems[productNumber - 1].improvements:insert('#', new) -It's all a bit loose and hacky but it works, at least if you don't have multiple -stacks filling up one reagent. +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 the siege crossbow. :: +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 repeat this + if projectile.distance_flown > 0 then + -- don't make this adjustment more than once return end @@ -276,7 +280,7 @@ Let's also make some code to modify the fire rate of the siege crossbow. :: end Now, let's see how we could make some "pegasus boots". First, let's define the -item in the raws: :: +item in the raws:: [ITEM_SHOES:ITEM_SHOES_BOOTS_PEGASUS] [NAME:pegasus boot:pegasus boots] @@ -292,9 +296,9 @@ item in the raws: :: [LEATHER] [HARD] [PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token - (you don't have to comment this every time) + (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: :: +Then, let's make a ``repeat-util`` callback for once a tick:: repeatUtil.scheduleEvery(modId, 1, "ticks", function() @@ -306,24 +310,24 @@ their worn items: :: 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: :: +Now, we will add up the effect of all speed-increasing gear and apply it:: if entry.mode == df.unit_inventory_item.T_mode.Worn then 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 + -- Subtract amount from movement timer if currently moving + dfhack.units.addMoveTimer(-amount) 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 +tick/as event callbacks with that ``modId`` key 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 +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 ``repeat-util`` callbacks, though. Create a folder for mod projects somewhere (e.g. ``hack/my-scripts/mods/``, or From cd83f3fcf13e1b6208b5266b0bc36cded20294a3 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 12 Sep 2022 15:04:16 -0700 Subject: [PATCH 36/46] Remove bad link --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 2eb8c7424..549b93e3b 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -66,7 +66,7 @@ The structure of the game 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 `_. +guide. We'll explore more of the game structures below. Your first script ----------------- From b89a30a3091720b64e5ea066b9f65c5cc7b6c48f Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 13 Sep 2022 10:51:38 +0100 Subject: [PATCH 37/46] Update modding-guide.rst Change use of word entities to objects because entitiy is a taken word in DF, and remove incorrect slash from a command. --- docs/guides/modding-guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 549b93e3b..6687ca6d0 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -107,7 +107,7 @@ 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 entities like units are +interactive data explorer. You can run the script while obects 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. @@ -338,7 +338,7 @@ 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``. + ``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 From 92b047fda1e1b84c7b454c7fa125db9d7ed67317 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Tue, 13 Sep 2022 10:52:56 +0100 Subject: [PATCH 38/46] Update modding-guide.rst Obects to objects typo fix --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 6687ca6d0..64087fd94 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -107,7 +107,7 @@ 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 obects like units are +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. From 7ccacd7875a2af3dfcff559c798c30413ef4ba87 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 13 Sep 2022 23:16:54 -0700 Subject: [PATCH 39/46] editing pass for the structure of a full mod the sample code itself needs some adjustment to exemplify best pratices. --- docs/guides/modding-guide.rst | 86 +++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 64087fd94..d6e05ea41 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -137,8 +137,7 @@ 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, perhaps with a suffix if you -are registering multiple keys:: +You should use something unique, like your mod name:: local modId = "callback-example-mod" @@ -169,6 +168,12 @@ projectile:: 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 ----------------- @@ -322,45 +327,46 @@ Now, we will add up the effect of all speed-increasing gear and apply it:: 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`` key 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 call order (if that is important -to you). 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: - -* 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``. - * 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. - -* 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. - -Now, let's take a look at an example ``raw/scripts/main.lua`` file. :: - - local repeatUtil = require("repeat-util") - local eventful = require("plugins.eventful") +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/main.lua + raw/scripts/example-mod/... + README.md + +Let's go through that line by line. + +* You'll need a short 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 subfolder for your mod under ``raw/scripts/`` will contain all the scripts used by + your mod, including the main initialization code in ``main.lua`` which registers all + your timer and event callbacks. + +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/main.lua`` file:: + + local repeatUtil = require('repeat-util') + local eventful = require('plugins.eventful') - local modId = "example-mod" + local modId = 'example-mod' local args = {...} if args[1] == "enable" then From d68c17d0708bfd79c9be223559ff53ce55e3d713 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 14 Sep 2022 10:33:27 -0700 Subject: [PATCH 40/46] fix paths/clean up code/use best practices --- docs/guides/modding-guide.rst | 138 ++++++++++++++++------------------ 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index d6e05ea41..57504143a 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -336,7 +336,7 @@ structure that looks like this:: raw/init.d/example-mod.lua raw/objects/... - raw/scripts/example-mod/main.lua + raw/scripts/example-mod.lua raw/scripts/example-mod/... README.md @@ -346,9 +346,9 @@ Let's go through that line by line. loaded. * Modifications to the game raws (potentially with custom raw tokens) go in ``raw/objects/``. -* A subfolder for your mod under ``raw/scripts/`` will contain all the scripts used by - your mod, including the main initialization code in ``main.lua`` which registers all - your timer and event callbacks. +* 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 + 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 @@ -361,107 +361,99 @@ 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/main.lua`` file:: - +``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' - 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 - 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 + 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() - repeatUtil.scheduleEvery(modId .. " 1 ticks", 1, "ticks", function() - moduleA.every1Tick() - end) + -- register your callbacks + repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', moduleA.every1Tick) + repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', moduleD.every100Frames) - repeatUtil.scheduleEvery(modID .. " 100 frames", 1, "frames", function() - moduleD.every100Frames() + 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 - 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 - -- Order: on unload, then cancel the callbacks in the same order as - -- above + 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 .. " 1 ticks") - repeatUtil.cancel(modId .. " 100 frames") + 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") - 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") + print('Example mod disabled') + enabled = false 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. - -Here is an example of an ``raw/init.d/`` file: :: +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("example-mod/main enable") -- Very simple. Could be - -- called "init-example-mod.lua" + dfhack.run_command('enable example-mod') -Here is what ``raw/scripts/module-a.lua`` would look like: :: +Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: --@ module = true - -- The above line is required for dfhack.reqscript to work + -- The above line is required for reqscript to work function onLoad() -- global variables are exported - -- blah + -- do initialization here end - local function usedByOnTick() -- local variables are not exported - -- blah + local function usedByOnTick(unit) -- local variables are not exported + -- this is an internal function: local functions/variables are not exported end function onTick() -- exported - for blah in ipairs(blah) do - usedByOnTick() + for _,unit in ipairs(df.global.world.units.all) do + usedByOnTick(unit) end end -It is recommended to check `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. +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! From 51f5349bafab12d916b15a25e18bd0d363f79b04 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 14 Sep 2022 10:43:51 -0700 Subject: [PATCH 41/46] labels must go above a section header --- docs/Lua API.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index b0891da57..e8be88626 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -5181,12 +5181,11 @@ General script API return end +.. _reqscript: Importing scripts ================= -.. _reqscript: - * ``dfhack.reqscript(name)`` or ``reqscript(name)`` Loads a Lua script and returns its environment (i.e. a table of all global From 7079fe7ea0e42381bddaa72d631d7e415bc1aa04 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 14 Sep 2022 10:56:58 -0700 Subject: [PATCH 42/46] rewrap, fix a code syntax error --- docs/guides/modding-guide.rst | 204 +++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 90 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 57504143a..706f2cd28 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -18,21 +18,22 @@ 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 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. +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: +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 @@ -42,17 +43,19 @@ 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:: +: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. +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: +For scripts with the same name, the `order of precedence ` will +be: 1. ``own-scripts/`` 2. ``data/save/*/raw/scripts/`` @@ -88,9 +91,9 @@ If ``unit`` is ``nil``, we don't want the script to run anymore:: 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:: +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) @@ -105,12 +108,12 @@ 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. +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 `. @@ -121,23 +124,23 @@ 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! +``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. +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:: +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:: +Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. You +should use something unique, like your mod name:: local modId = "callback-example-mod" @@ -145,8 +148,8 @@ 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 + -- Do something like iterating over all active units and + -- check for something interesting for _, unit in ipairs(df.global.world.units.active) do ... end @@ -162,17 +165,19 @@ 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) + 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. +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 ----------------- @@ -229,8 +234,9 @@ First, require the modules we are going to use:: 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) + 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:: @@ -279,9 +285,11 @@ Let's also make some code to modify the fire rate of our siege crossbow:: 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) + 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 @@ -301,26 +309,29 @@ item in the raws:: [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) + (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, 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, 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 - -Now, we will add up the effect of all speed-increasing gear and apply it:: - - if entry.mode == df.unit_inventory_item.T_mode.Worn then - amount = amount + tonumber((customRawTokens.getToken(entry.item, "PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK")) or 0) + 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) @@ -328,11 +339,11 @@ 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:: +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/... @@ -342,21 +353,23 @@ structure that looks like this:: Let's go through that line by line. -* You'll need a short script in ``raw/init.d/`` to initialise your mod when a save is - loaded. +* You'll need 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 - used by your mod. +* 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. +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``:: +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 @@ -364,8 +377,9 @@ 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" + -- 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 = [[ @@ -378,7 +392,8 @@ Ok, you're all set up! Now, let's take a look at an example local repeatUtil = require('repeat-util') local eventful = require('plugins.eventful') - -- you can reference global values or functions declared in any of your internal scripts + -- 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') @@ -390,7 +405,8 @@ Ok, you're all set up! Now, let's take a look at an example if not dfhack_flags.enable then print(usage) print() - print(('Example mod is currently '):format(enabled and 'enabled' or 'disabled')) + print(('Example mod is currently '):format( + enabled and 'enabled' or 'disabled')) return end @@ -400,13 +416,19 @@ Ok, you're all set up! Now, let's take a look at an example moduleB.onLoad() -- register your callbacks - repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', moduleA.every1Tick) - repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', moduleD.every100Frames) - - eventful.onReactionComplete[modId] = function(reaction, reaction_product, unit, input_items, input_reagents, output_items) + repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', + moduleA.every1Tick) + repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', + moduleD.every100Frames) + + 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) + 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 eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement @@ -444,8 +466,10 @@ Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: -- do initialization here end - local function usedByOnTick(unit) -- local variables are not exported - -- this is an internal function: local functions/variables are not exported + -- this is an internal function: local functions/variables + -- are not exported + local function usedByOnTick(unit) + -- ... end function onTick() -- exported @@ -454,6 +478,6 @@ Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: 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! +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! From b90126600f19c075284b69ef25e2794de8200d76 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 14 Sep 2022 11:00:26 -0700 Subject: [PATCH 43/46] whiiittespaaaace!!! --- docs/guides/modding-guide.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 706f2cd28..de07136b3 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -381,11 +381,11 @@ Ok, you're all set up! Now, let's take a look at an example -- 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 ]] @@ -398,7 +398,7 @@ Ok, you're all set up! Now, let's take a look at an example 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' From d7142749218a6c14d0b7d0aac37bc7883ae63bd1 Mon Sep 17 00:00:00 2001 From: Myk Date: Wed, 14 Sep 2022 11:24:27 -0700 Subject: [PATCH 44/46] fix reqscript link name --- docs/guides/modding-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index de07136b3..3c087c80a 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -478,6 +478,6 @@ Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: end end -The `reqscript` function reloads scripts that have changed, so you can modify +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! From ee123a24615cff76918033ad2f082a39efa95132 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Wed, 14 Sep 2022 19:41:45 +0100 Subject: [PATCH 45/46] Update modding-guide.rst Add missing slashes and change the wording to be more consistent in one part --- docs/guides/modding-guide.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 3c087c80a..5b043c02c 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -339,10 +339,10 @@ 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 +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`` +``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 @@ -353,7 +353,7 @@ directory and has a basic structure that looks like this:: Let's go through that line by line. -* You'll need a short (one-line) script in ``raw/init.d/`` to initialise your +* 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/``. @@ -367,11 +367,11 @@ 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 +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 + +/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:: From 20a55299dd92681aed24cc8f3afc9da509524e58 Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Wed, 14 Sep 2022 20:04:50 +0100 Subject: [PATCH 46/46] Update modding-guide.rst Add some potentially helpful comments to explain an inconsistenecy --- docs/guides/modding-guide.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 5b043c02c..6ec80eed1 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -421,6 +421,7 @@ Ok, you're all set up! Now, let's take a look at an example 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) @@ -431,6 +432,8 @@ Ok, you're all set up! Now, let's take a look at an example 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