Merge branch 'develop' into spectate
						commit
						ddf7850d90
					
				| Can't render this file because it has a wrong number of fields in line 56. | 
| @ -1 +1 @@ | ||||
| Subproject commit ae19aebd795d6d91803e60f46de037b604593cb4 | ||||
| Subproject commit 6ed8aa46462ea01a1122fc49422840a2facc9757 | ||||
| @ -1,11 +1,14 @@ | ||||
| =========== | ||||
| User Guides | ||||
| User guides | ||||
| =========== | ||||
| 
 | ||||
| These pages are detailed guides covering DFHack tools. | ||||
| 
 | ||||
| .. toctree:: | ||||
|    :maxdepth: 1 | ||||
|    :glob: | ||||
| 
 | ||||
|    * | ||||
|    /docs/guides/examples-guide | ||||
|    /docs/guides/modding-guide | ||||
|    /docs/guides/quickfort-library-guide | ||||
|    /docs/guides/quickfort-user-guide | ||||
|    /docs/guides/quickfort-alias-guide | ||||
|  | ||||
| @ -0,0 +1,496 @@ | ||||
| .. _modding-guide: | ||||
| 
 | ||||
| DFHack modding guide | ||||
| ==================== | ||||
| 
 | ||||
| .. highlight:: lua | ||||
| 
 | ||||
| What is the difference between a script and a mod? | ||||
| -------------------------------------------------- | ||||
| 
 | ||||
| A script is a single file that can be run as a command in DFHack, like something | ||||
| that modifies or displays game data on request. A mod is something you install | ||||
| to get persistent behavioural changes in the game and/or add new content. Mods | ||||
| can contain and use scripts in addition to (or instead of) modifications to the | ||||
| DF game raws. | ||||
| 
 | ||||
| DFHack scripts are written in Lua. If you don't already know Lua, there's a | ||||
| great primer at `lua.org <https://www.lua.org/pil/contents.html>`__. | ||||
| 
 | ||||
| Why not just mod the raws? | ||||
| -------------------------- | ||||
| 
 | ||||
| It depends on what you want to do. Some mods *are* better to do in just the | ||||
| raws. You don't need DFHack to add a new race or modify attributes, for example. | ||||
| However, DFHack scripts can do many things that you just can't do in the raws, | ||||
| like make a creature that trails smoke. Some things *could* be done in the raws, | ||||
| but writing a script is less hacky, easier to maintain, easier to extend, and is | ||||
| not prone to side-effects. A great example is adding a syndrome when a reaction | ||||
| is performed. If done in the raws, you have to create an exploding boulder to | ||||
| apply the syndrome. DFHack scripts can add the syndrome directly and with much | ||||
| more flexibility. In the end, complex mods will likely require a mix of raw | ||||
| modding and DFHack scripting. | ||||
| 
 | ||||
| A mod-maker's development environment | ||||
| ------------------------------------- | ||||
| 
 | ||||
| While you're writing your mod, you need a place to store your in-development | ||||
| scripts that will: | ||||
| 
 | ||||
| - be directly runnable by DFHack | ||||
| - not get lost when you upgrade DFHack | ||||
| 
 | ||||
| The recommended approach is to create a directory somewhere outside of your DF | ||||
| installation (let's call it "/path/to/own-scripts") and do all your script | ||||
| development in there. | ||||
| 
 | ||||
| Inside your DF installation folder, there is a file named | ||||
| :file:`dfhack-config/script-paths.txt`. If you add a line like this to that | ||||
| file:: | ||||
| 
 | ||||
|     +/path/to/own-scripts | ||||
| 
 | ||||
| Then that directory will be searched when you run DFHack commands from inside | ||||
| the game. The ``+`` at the front of the path means to search that directory | ||||
| first, before any other script directory (like :file:`hack/scripts` or | ||||
| :file:`raw/scripts`). That way, your latest changes will always be used instead | ||||
| of older copies that you may have installed in a DF directory. | ||||
| 
 | ||||
| For scripts with the same name, the `order of precedence <script-paths>` will | ||||
| be: | ||||
| 
 | ||||
| 1. ``own-scripts/`` | ||||
| 2. ``data/save/*/raw/scripts/`` | ||||
| 3. ``raw/scripts/`` | ||||
| 4. ``hack/scripts/`` | ||||
| 
 | ||||
| The structure of the game | ||||
| ------------------------- | ||||
| 
 | ||||
| "The game" is in the global variable `df <lua-df>`. The game's memory can be | ||||
| found in ``df.global``, containing things like the list of all items, whether to | ||||
| reindex pathfinding, et cetera. Also relevant to us in ``df`` are the various | ||||
| types found in the game, e.g. ``df.pronoun_type`` which we will be using in this | ||||
| guide. We'll explore more of the game structures below. | ||||
| 
 | ||||
| Your first script | ||||
| ----------------- | ||||
| 
 | ||||
| So! It's time to write your first script. This section will walk you through how | ||||
| to make a script that will get the pronoun type of the currently selected unit. | ||||
| 
 | ||||
| First line, we get the unit:: | ||||
| 
 | ||||
|     local unit = dfhack.gui.getSelectedUnit() | ||||
| 
 | ||||
| If no unit is selected, an error message will be printed (which can be silenced | ||||
| by passing ``true`` to ``getSelectedUnit``) and ``unit`` will be ``nil``. | ||||
| 
 | ||||
| If ``unit`` is ``nil``, we don't want the script to run anymore:: | ||||
| 
 | ||||
|     if not unit then | ||||
|         return | ||||
|     end | ||||
| 
 | ||||
| Now, the field ``sex`` in a unit is an integer, but each integer corresponds to | ||||
| a string value ("it", "she", or "he"). We get this value by indexing the | ||||
| bidirectional map ``df.pronoun_type``. Indexing the other way, incidentally, | ||||
| with one of the strings, will yield its corresponding number. So:: | ||||
| 
 | ||||
|     local pronounTypeString = df.pronoun_type[unit.sex] | ||||
|     print(pronounTypeString) | ||||
| 
 | ||||
| Simple. Save this as a Lua file in your own scripts directory and run it as | ||||
| shown before when a unit is selected in the Dwarf Fortress UI. | ||||
| 
 | ||||
| Exploring DF structures | ||||
| ----------------------- | ||||
| 
 | ||||
| So how could you have known about the field and type we just used? Well, there | ||||
| are two main tools for discovering the various fields in the game's data | ||||
| structures. The first is the ``df-structures`` | ||||
| `repository <https://github.com/DFHack/df-structures>`__ that contains XML files | ||||
| describing the contents of the game's structures. These are complete, but | ||||
| difficult to read (for a human). The second option is the `gui/gm-editor` | ||||
| script, an interactive data explorer. You can run the script while objects like | ||||
| units are selected to view the data within them. You can also run | ||||
| ``gui/gm-editor scr`` to view the data for the current screen. Press :kbd:`?` | ||||
| while the script is active to view help. | ||||
| 
 | ||||
| Familiarising yourself with the many structs of the game will help with ideas | ||||
| immensely, and you can always ask for help in the `right places <support>`. | ||||
| 
 | ||||
| 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 <eventful-api>`. | ||||
| ``repeat-util`` is used to run a function once per a configurable number of | ||||
| frames (paused or unpaused), ticks (unpaused), in-game days, months, or years. | ||||
| If you need to be aware the instant something happens, you'll need to run a | ||||
| check once a tick. Be careful not to do this gratuitiously, though, since | ||||
| running that often can slow down the game! | ||||
| 
 | ||||
| ``eventful``, on the other hand, is much more performance-friendly since it will | ||||
| only call your callback when a relevant event happens, like a reaction or job | ||||
| being completed or a projectile moving. | ||||
| 
 | ||||
| To get something to run once per tick, we can call | ||||
| ``repeat-util.scheduleEvery()``. First, we load the module:: | ||||
| 
 | ||||
|     local repeatUtil = require('repeat-util') | ||||
| 
 | ||||
| Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. You | ||||
| should use something unique, like your mod name:: | ||||
| 
 | ||||
|     local modId = "callback-example-mod" | ||||
| 
 | ||||
| Then, we pass the key, amount of time units between function calls, what the | ||||
| time units are, and finally the callback function itself:: | ||||
| 
 | ||||
|     repeatUtil.scheduleEvery(modId, 1, "ticks", function() | ||||
|         -- Do something like iterating over all active units and | ||||
|         -- check for something interesting | ||||
|         for _, unit in ipairs(df.global.world.units.active) do | ||||
|             ... | ||||
|         end | ||||
|     end) | ||||
| 
 | ||||
| ``eventful`` is slightly more involved. First get the module:: | ||||
| 
 | ||||
|     local eventful = require('plugins.eventful') | ||||
| 
 | ||||
| ``eventful`` contains a table for each event which you populate with functions. | ||||
| Each function in the table is then called with the appropriate arguments when | ||||
| the event occurs. So, for example, to print the position of a moving (item) | ||||
| projectile:: | ||||
| 
 | ||||
|     eventful.onProjItemCheckMovement[modId] = function(projectile) | ||||
|         print(projectile.cur_pos.x, projectile.cur_pos.y, | ||||
|               projectile.cur_pos.z) | ||||
|     end | ||||
| 
 | ||||
| Check out the `full list of supported events <eventful-api>` 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 | ||||
| ----------------- | ||||
| 
 | ||||
| .. highlight:: none | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| First, let's define a custom crossbow with its own custom reaction. The | ||||
| crossbow:: | ||||
| 
 | ||||
|     [ITEM_WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE] | ||||
|         [NAME:crossbow:crossbows] | ||||
|         [SIZE:600] | ||||
|         [SKILL:HAMMER] | ||||
|         [RANGED:CROSSBOW:BOLT] | ||||
|         [SHOOT_FORCE:4000] | ||||
|         [SHOOT_MAXVEL:800] | ||||
|         [TWO_HANDED:0] | ||||
|         [MINIMUM_SIZE:17500] | ||||
|         [MATERIAL_SIZE:4] | ||||
|         [ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250] | ||||
|             [ATTACK_PREPARE_AND_RECOVER:3:3] | ||||
|         [SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER:2] custom token (you'll see) | ||||
| 
 | ||||
| The reaction to make it (you would add the reaction and not the weapon to an | ||||
| entity raw):: | ||||
| 
 | ||||
|     [REACTION:MAKE_SIEGE_CROSSBOW] | ||||
|         [NAME:make siege crossbow] | ||||
|         [BUILDING:BOWYER:NONE] | ||||
|         [SKILL:BOWYER] | ||||
|         [REAGENT:mechanism 1:2:TRAPPARTS:NONE:NONE:NONE] | ||||
|         [REAGENT:bar:150:BAR:NONE:NONE:NONE] | ||||
|             [METAL_ITEM_MATERIAL] | ||||
|         [REAGENT:handle 1:1:BLOCKS:NONE:NONE:NONE] wooden handles | ||||
|             [ANY_PLANT_MATERIAL] | ||||
|         [REAGENT:handle 2:1:BLOCKS:NONE:NONE:NONE] | ||||
|             [ANY_PLANT_MATERIAL] | ||||
|         [SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1] | ||||
|             another custom token | ||||
|         [PRODUCT:100:1:WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE:GET_MATERIAL_FROM_REAGENT:bar:NONE] | ||||
| 
 | ||||
| So, we are going to use the ``eventful`` module to make it so that (after the | ||||
| script is run) when this crossbow is crafted, it will have two handles, each | ||||
| with the material given by the block reagents. | ||||
| 
 | ||||
| .. highlight:: lua | ||||
| 
 | ||||
| First, require the modules we are going to use:: | ||||
| 
 | ||||
|     local eventful = require("plugins.eventful") | ||||
|     local customRawTokens = require("custom-raw-tokens") | ||||
| 
 | ||||
| Now, let's make a callback (we'll be defining the body of this function soon):: | ||||
| 
 | ||||
|     local modId = "siege-crossbow-mod" | ||||
|     eventful.onReactionComplete[modId] = function(reaction, | ||||
|             reactionProduct, unit, inputItems, inputReagents, | ||||
|             outputItems) | ||||
| 
 | ||||
| First, we check to see if it the reaction that just happened is relevant to this | ||||
| callback:: | ||||
| 
 | ||||
|     if not customRawTokens.getToken(reaction, | ||||
|         "SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT") | ||||
|     then | ||||
|         return | ||||
|     end | ||||
| 
 | ||||
| Then, we get the product number listed. Next, for every reagent, if the reagent | ||||
| name starts with "handle" then we get the corresponding item, and... | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     for i, reagent in ipairs(inputReagents) do | ||||
|         if reagent.code:startswith('handle') then | ||||
|             -- Found handle reagent | ||||
|             local item = inputItems[i] | ||||
| 
 | ||||
| ...We then add a handle improvement to the listed product within our loop:: | ||||
| 
 | ||||
|     local new = df.itemimprovement_itemspecificst:new() | ||||
|     new.mat_type, new.mat_index = item.mat_type, item.mat_index | ||||
|     new.type = df.itemimprovement_specific_type.HANDLE | ||||
|     outputItems[productNumber - 1].improvements:insert('#', new) | ||||
| 
 | ||||
| This works well as long as you don't have multiple stacks filling up one | ||||
| reagent. | ||||
| 
 | ||||
| Let's also make some code to modify the fire rate of our siege crossbow:: | ||||
| 
 | ||||
|     eventful.onProjItemCheckMovement[modId] = function(projectile) | ||||
|         if projectile.distance_flown > 0 then | ||||
|             -- don't make this adjustment more than once | ||||
|             return | ||||
|         end | ||||
| 
 | ||||
|         local firer = projectile.firer | ||||
|         if not firer then | ||||
|             return | ||||
|         end | ||||
| 
 | ||||
|         local weapon = df.item.find(projectile.bow_id) | ||||
|         if not weapon then | ||||
|             return | ||||
|         end | ||||
| 
 | ||||
|         local multiplier = tonumber(customRawTokens.getToken( | ||||
|                 weapon.subtype, | ||||
|                 "SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER")) or 1 | ||||
|         firer.counters.think_counter = math.floor( | ||||
|                 firer.counters.think_counter * multiplier) | ||||
|     end | ||||
| 
 | ||||
| .. highlight:: none | ||||
| 
 | ||||
| Now, let's see how we could make some "pegasus boots". First, let's define the | ||||
| item in the raws:: | ||||
| 
 | ||||
|     [ITEM_SHOES:ITEM_SHOES_BOOTS_PEGASUS] | ||||
|         [NAME:pegasus boot:pegasus boots] | ||||
|         [ARMORLEVEL:1] | ||||
|         [UPSTEP:1] | ||||
|         [METAL_ARMOR_LEVELS] | ||||
|         [LAYER:OVER] | ||||
|         [COVERAGE:100] | ||||
|         [LAYER_SIZE:25] | ||||
|         [LAYER_PERMIT:15] | ||||
|         [MATERIAL_SIZE:2] | ||||
|         [METAL] | ||||
|         [LEATHER] | ||||
|         [HARD] | ||||
|         [PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token | ||||
|             (you don't have to comment the custom token every time, | ||||
|             but it does clarify what it is) | ||||
| 
 | ||||
| .. highlight:: lua | ||||
| 
 | ||||
| Then, let's make a ``repeat-util`` callback for once a tick:: | ||||
| 
 | ||||
|     repeatUtil.scheduleEvery(modId, 1, "ticks", function() | ||||
| 
 | ||||
| Let's iterate over every active unit, and for every unit, iterate over their | ||||
| worn items to calculate how much we are going to take from their movement | ||||
| timer:: | ||||
| 
 | ||||
|     for _, unit in ipairs(df.global.world.units.active) do | ||||
|         local amount = 0 | ||||
|         for _, entry in ipairs(unit.inventory) do | ||||
|             if entry.mode == df.unit_inventory_item.T_mode.Worn then | ||||
|                 local reduction = customRawTokens.getToken( | ||||
|                         entry.item, | ||||
|                         'PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK') | ||||
|                 amount = amount + (tonumber(reduction) or 0) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| 
 | ||||
|     -- Subtract amount from movement timer if currently moving | ||||
|     dfhack.units.addMoveTimer(-amount) | ||||
| 
 | ||||
| The structure of a full mod | ||||
| --------------------------- | ||||
| 
 | ||||
| Create a folder for mod projects somewhere outside your Dwarf Fortress | ||||
| installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the | ||||
| names for the mod folders within it. In the example below, we'll use a mod ID of | ||||
| ``example-mod``. I'm sure your mods will have more creative names! The | ||||
| ``example-mod`` mod will be developed in the ``/path/to/mymods/example-mod/`` | ||||
| directory and has a basic structure that looks like this:: | ||||
| 
 | ||||
|     raw/init.d/example-mod.lua | ||||
|     raw/objects/... | ||||
|     raw/scripts/example-mod.lua | ||||
|     raw/scripts/example-mod/... | ||||
|     README.md | ||||
| 
 | ||||
| Let's go through that line by line. | ||||
| 
 | ||||
| * A short (one-line) script in ``raw/init.d/`` to initialise your | ||||
|   mod when a save is loaded. | ||||
| * Modifications to the game raws (potentially with custom raw tokens) go in | ||||
|   ``raw/objects/``. | ||||
| * A control script in ``raw/scripts/`` that handles enabling and disabling your | ||||
|   mod. | ||||
| * A subfolder for your mod under ``raw/scripts/`` will contain all the internal | ||||
|   scripts and/or modules used by your mod. | ||||
| 
 | ||||
| It is a good idea to use a version control system to organize changes to your | ||||
| mod code. You can create a separate Git repository for each of your mods. The | ||||
| ``README.md`` file will be your mod help text when people browse to your online | ||||
| repository. | ||||
| 
 | ||||
| Unless you want to install your ``raw/`` folder into your DF game folder every | ||||
| time you make a change to your scripts, you should add your development scripts | ||||
| directory to your script paths in ``dfhack-config/script-paths.txt``:: | ||||
| 
 | ||||
|     +/path/to/mymods/example-mod/raw/scripts/ | ||||
| 
 | ||||
| Ok, you're all set up! Now, let's take a look at an example | ||||
| ``raw/scripts/example-mod.lua`` file:: | ||||
| 
 | ||||
|     -- main setup and teardown for example-mod | ||||
|     -- this next line indicates that the script supports the "enable" | ||||
|     -- API so you can start it by running "enable example-mod" and stop | ||||
|     -- it by running "disable example-mod" | ||||
|     --@ enable = true | ||||
| 
 | ||||
|     local usage = [[ | ||||
|     Usage | ||||
|     ----- | ||||
| 
 | ||||
|         enable example-mod | ||||
|         disable example-mod | ||||
|     ]] | ||||
|     local repeatUtil = require('repeat-util') | ||||
|     local eventful = require('plugins.eventful') | ||||
| 
 | ||||
|     -- you can reference global values or functions declared in any of | ||||
|     -- your internal scripts | ||||
|     local moduleA = reqscript('example-mod/module-a') | ||||
|     local moduleB = reqscript('example-mod/module-b') | ||||
|     local moduleC = reqscript('example-mod/module-c') | ||||
|     local moduleD = reqscript('example-mod/module-d') | ||||
| 
 | ||||
|     enabled = enabled or false | ||||
|     local modId = 'example-mod' | ||||
| 
 | ||||
|     if not dfhack_flags.enable then | ||||
|         print(usage) | ||||
|         print() | ||||
|         print(('Example mod is currently '):format( | ||||
|                 enabled and 'enabled' or 'disabled')) | ||||
|         return | ||||
|     end | ||||
| 
 | ||||
|     if dfhack_flags.enable_state then | ||||
|         -- do any initialization your internal scripts might require | ||||
|         moduleA.onLoad() | ||||
|         moduleB.onLoad() | ||||
| 
 | ||||
|         -- register your callbacks | ||||
|         repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', | ||||
|                                  moduleA.every1Tick) | ||||
|         repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', | ||||
|                                  moduleD.every100Frames) | ||||
| 
 | ||||
|         -- multiple functions in the same callback | ||||
|         eventful.onReactionComplete[modId] = function(reaction, | ||||
|                 reaction_product, unit, input_items, input_reagents, | ||||
|                 output_items) | ||||
|             -- pass the event's parameters to the listeners | ||||
|             moduleB.onReactionComplete(reaction, reaction_product, | ||||
|                     unit, input_items, input_reagents, output_items) | ||||
|             moduleC.onReactionComplete(reaction, reaction_product, | ||||
|                     unit, input_items, input_reagents, output_items) | ||||
|         end | ||||
| 
 | ||||
|         -- one function per callback (you can put them in the | ||||
|         -- above format if you prefer) | ||||
|         eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement | ||||
|         eventful.onProjUnitCheckMovement[modId] = moduleD.onProjUnitCheckMovement | ||||
| 
 | ||||
|         print('Example mod enabled') | ||||
|         enabled = true | ||||
|     else | ||||
|         -- call any shutdown functions your internal scripts might require | ||||
|         moduleA.onUnload() | ||||
| 
 | ||||
|         repeatUtil.cancel(modId .. ' every ticks') | ||||
|         repeatUtil.cancel(modId .. ' 100 frames') | ||||
| 
 | ||||
|         eventful.onReactionComplete[modId] = nil | ||||
|         eventful.onProjItemCheckMovement[modId] = nil | ||||
|         eventful.onProjUnitCheckMovement[modId] = nil | ||||
| 
 | ||||
|         print('Example mod disabled') | ||||
|         enabled = false | ||||
|     end | ||||
| 
 | ||||
| You can call ``enable example-mod`` and ``disable example-mod`` yourself while | ||||
| developing, but for end users you can start your mod automatically from | ||||
| ``raw/init.d/example-mod.lua``:: | ||||
| 
 | ||||
|     dfhack.run_command('enable example-mod') | ||||
| 
 | ||||
| Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: | ||||
| 
 | ||||
|     --@ module = true | ||||
|     -- The above line is required for reqscript to work | ||||
| 
 | ||||
|     function onLoad() -- global variables are exported | ||||
|         -- do initialization here | ||||
|     end | ||||
| 
 | ||||
|     -- this is an internal function: local functions/variables | ||||
|     -- are not exported | ||||
|     local function usedByOnTick(unit) | ||||
|         -- ... | ||||
|     end | ||||
| 
 | ||||
|     function onTick() -- exported | ||||
|         for _,unit in ipairs(df.global.world.units.all) do | ||||
|             usedByOnTick(unit) | ||||
|         end | ||||
|     end | ||||
| 
 | ||||
| The `reqscript <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! | ||||
| @ -0,0 +1,20 @@ | ||||
| overlay | ||||
| ======= | ||||
| 
 | ||||
| .. dfhack-tool:: | ||||
|     :summary: Provide an on-screen clickable DFHack launcher button. | ||||
|     :tags: dfhack interface | ||||
| 
 | ||||
| This tool places a small button in the lower left corner of the screen that you | ||||
| can click to run DFHack commands with `gui/launcher`. | ||||
| 
 | ||||
| If you would rather always run `gui/launcher` with the hotkeys, or just don't | ||||
| want the DFHack button on-screen, just disable the plugin with | ||||
| ``disable overlay``. | ||||
| 
 | ||||
| Usage | ||||
| ----- | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     enable overlay | ||||
| @ -1 +1 @@ | ||||
| Subproject commit dc118c5e90aea6181a290e4bbf40e7f2974fb053 | ||||
| Subproject commit f5fab13fb652dd953e9d59e8deca032b26131208 | ||||
| @ -0,0 +1,338 @@ | ||||
| #include "df/viewscreen_adopt_regionst.h" | ||||
| #include "df/viewscreen_adventure_logst.h" | ||||
| #include "df/viewscreen_announcelistst.h" | ||||
| #include "df/viewscreen_assign_display_itemst.h" | ||||
| #include "df/viewscreen_barterst.h" | ||||
| #include "df/viewscreen_buildinglistst.h" | ||||
| #include "df/viewscreen_buildingst.h" | ||||
| #include "df/viewscreen_choose_start_sitest.h" | ||||
| #include "df/viewscreen_civlistst.h" | ||||
| #include "df/viewscreen_counterintelligencest.h" | ||||
| #include "df/viewscreen_createquotast.h" | ||||
| #include "df/viewscreen_customize_unitst.h" | ||||
| #include "df/viewscreen_dungeonmodest.h" | ||||
| #include "df/viewscreen_dungeon_monsterstatusst.h" | ||||
| #include "df/viewscreen_dungeon_wrestlest.h" | ||||
| #include "df/viewscreen_dwarfmodest.h" | ||||
| #include "df/viewscreen_entityst.h" | ||||
| #include "df/viewscreen_export_graphical_mapst.h" | ||||
| #include "df/viewscreen_export_regionst.h" | ||||
| #include "df/viewscreen_game_cleanerst.h" | ||||
| #include "df/viewscreen_image_creator_mode.h" | ||||
| #include "df/viewscreen_image_creatorst.h" | ||||
| #include "df/viewscreen_itemst.h" | ||||
| #include "df/viewscreen_joblistst.h" | ||||
| #include "df/viewscreen_jobmanagementst.h" | ||||
| #include "df/viewscreen_jobst.h" | ||||
| #include "df/viewscreen_justicest.h" | ||||
| #include "df/viewscreen_kitchenpref_page.h" | ||||
| #include "df/viewscreen_kitchenprefst.h" | ||||
| #include "df/viewscreen_layer_arena_creaturest.h" | ||||
| #include "df/viewscreen_layer_assigntradest.h" | ||||
| #include "df/viewscreen_layer_choose_language_namest.h" | ||||
| #include "df/viewscreen_layer_currencyst.h" | ||||
| #include "df/viewscreen_layer_export_play_mapst.h" | ||||
| #include "df/viewscreen_layer.h" | ||||
| #include "df/viewscreen_layer_militaryst.h" | ||||
| #include "df/viewscreen_layer_musicsoundst.h" | ||||
| #include "df/viewscreen_layer_noblelistst.h" | ||||
| #include "df/viewscreen_layer_overall_healthst.h" | ||||
| #include "df/viewscreen_layer_reactionst.h" | ||||
| #include "df/viewscreen_layer_squad_schedulest.h" | ||||
| #include "df/viewscreen_layer_stockpilest.h" | ||||
| #include "df/viewscreen_layer_stone_restrictionst.h" | ||||
| #include "df/viewscreen_layer_unit_actionst.h" | ||||
| #include "df/viewscreen_layer_unit_healthst.h" | ||||
| #include "df/viewscreen_layer_unit_relationshipst.h" | ||||
| #include "df/viewscreen_layer_world_gen_param_presetst.h" | ||||
| #include "df/viewscreen_layer_world_gen_paramst.h" | ||||
| #include "df/viewscreen_legendsst.h" | ||||
| #include "df/viewscreen_loadgamest.h" | ||||
| #include "df/viewscreen_locationsst.h" | ||||
| #include "df/viewscreen_meetingst.h" | ||||
| #include "df/viewscreen_movieplayerst.h" | ||||
| #include "df/viewscreen_noblest.h" | ||||
| #include "df/viewscreen_optionst.h" | ||||
| #include "df/viewscreen_overallstatusst.h" | ||||
| #include "df/viewscreen_petitionsst.h" | ||||
| #include "df/viewscreen_petst.h" | ||||
| #include "df/viewscreen_pricest.h" | ||||
| #include "df/viewscreen_reportlistst.h" | ||||
| #include "df/viewscreen_requestagreementst.h" | ||||
| #include "df/viewscreen_savegamest.h" | ||||
| #include "df/viewscreen_selectitemst.h" | ||||
| #include "df/viewscreen_setupadventurest.h" | ||||
| #include "df/viewscreen_setupdwarfgamest.h" | ||||
| #include "df/viewscreen_storesst.h" | ||||
| #include "df/viewscreen_textviewerst.h" | ||||
| #include "df/viewscreen_titlest.h" | ||||
| #include "df/viewscreen_topicmeeting_fill_land_holder_positionsst.h" | ||||
| #include "df/viewscreen_topicmeetingst.h" | ||||
| #include "df/viewscreen_topicmeeting_takerequestsst.h" | ||||
| #include "df/viewscreen_tradeagreementst.h" | ||||
| #include "df/viewscreen_tradegoodsst.h" | ||||
| #include "df/viewscreen_tradelistst.h" | ||||
| #include "df/viewscreen_treasurelistst.h" | ||||
| #include "df/viewscreen_unitlist_page.h" | ||||
| #include "df/viewscreen_unitlistst.h" | ||||
| #include "df/viewscreen_unitst.h" | ||||
| #include "df/viewscreen_update_regionst.h" | ||||
| #include "df/viewscreen_wagesst.h" | ||||
| #include "df/viewscreen_workquota_conditionst.h" | ||||
| #include "df/viewscreen_workquota_detailsst.h" | ||||
| #include "df/viewscreen_workshop_profilest.h" | ||||
| 
 | ||||
| #include "Debug.h" | ||||
| #include "PluginManager.h" | ||||
| #include "VTableInterpose.h" | ||||
| #include "uicommon.h" | ||||
| 
 | ||||
| #include "modules/Screen.h" | ||||
| 
 | ||||
| using namespace DFHack; | ||||
| 
 | ||||
| DFHACK_PLUGIN("overlay"); | ||||
| DFHACK_PLUGIN_IS_ENABLED(is_enabled); | ||||
| REQUIRE_GLOBAL(enabler); | ||||
| REQUIRE_GLOBAL(gps); | ||||
| 
 | ||||
| namespace DFHack { | ||||
|     DBG_DECLARE(overlay, log, DebugCategory::LINFO); | ||||
| } | ||||
| 
 | ||||
| static const std::string button_text = "[ DFHack Launcher ]"; | ||||
| static bool clicked = false; | ||||
| 
 | ||||
| static bool handle_click() { | ||||
|     if (!enabler->mouse_lbut_down || clicked) { | ||||
|         return false; | ||||
|     } | ||||
|     df::coord2d pos = Screen::getMousePos(); | ||||
|     DEBUG(log).print("clicked at screen coordinates (%d, %d)\n", pos.x, pos.y); | ||||
|     if (pos.y == gps->dimy - 1 && pos.x >= 1 && (size_t)pos.x <= button_text.size()) { | ||||
|         clicked = true; | ||||
|         Core::getInstance().setHotkeyCmd("gui/launcher"); | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static void draw_widgets() { | ||||
|     int x = 1; | ||||
|     int y = gps->dimy - 1; | ||||
|     OutputString(COLOR_GREEN, x, y, button_text); | ||||
| } | ||||
| 
 | ||||
| template<class T> | ||||
| struct viewscreen_overlay : T { | ||||
|     typedef T interpose_base; | ||||
| 
 | ||||
|     DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input)) { | ||||
|         if (!handle_click()) | ||||
|             INTERPOSE_NEXT(feed)(input); | ||||
|     } | ||||
|     DEFINE_VMETHOD_INTERPOSE(void, render, ()) { | ||||
|         INTERPOSE_NEXT(render)(); | ||||
|         draw_widgets(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| #define IMPLEMENT_HOOKS(screen) \ | ||||
|     typedef viewscreen_overlay<df::viewscreen_##screen##st> screen##_overlay; \ | ||||
|     template<> IMPLEMENT_VMETHOD_INTERPOSE(screen##_overlay, feed); \ | ||||
|     template<> IMPLEMENT_VMETHOD_INTERPOSE(screen##_overlay, render); | ||||
| 
 | ||||
| IMPLEMENT_HOOKS(adopt_region) | ||||
| IMPLEMENT_HOOKS(adventure_log) | ||||
| IMPLEMENT_HOOKS(announcelist) | ||||
| IMPLEMENT_HOOKS(assign_display_item) | ||||
| IMPLEMENT_HOOKS(barter) | ||||
| IMPLEMENT_HOOKS(buildinglist) | ||||
| IMPLEMENT_HOOKS(building) | ||||
| IMPLEMENT_HOOKS(choose_start_site) | ||||
| IMPLEMENT_HOOKS(civlist) | ||||
| IMPLEMENT_HOOKS(counterintelligence) | ||||
| IMPLEMENT_HOOKS(createquota) | ||||
| IMPLEMENT_HOOKS(customize_unit) | ||||
| IMPLEMENT_HOOKS(dungeonmode) | ||||
| IMPLEMENT_HOOKS(dungeon_monsterstatus) | ||||
| IMPLEMENT_HOOKS(dungeon_wrestle) | ||||
| IMPLEMENT_HOOKS(dwarfmode) | ||||
| IMPLEMENT_HOOKS(entity) | ||||
| IMPLEMENT_HOOKS(export_graphical_map) | ||||
| IMPLEMENT_HOOKS(export_region) | ||||
| IMPLEMENT_HOOKS(game_cleaner) | ||||
| IMPLEMENT_HOOKS(image_creator) | ||||
| IMPLEMENT_HOOKS(item) | ||||
| IMPLEMENT_HOOKS(joblist) | ||||
| IMPLEMENT_HOOKS(jobmanagement) | ||||
| IMPLEMENT_HOOKS(job) | ||||
| IMPLEMENT_HOOKS(justice) | ||||
| IMPLEMENT_HOOKS(kitchenpref) | ||||
| IMPLEMENT_HOOKS(layer_arena_creature) | ||||
| IMPLEMENT_HOOKS(layer_assigntrade) | ||||
| IMPLEMENT_HOOKS(layer_choose_language_name) | ||||
| IMPLEMENT_HOOKS(layer_currency) | ||||
| IMPLEMENT_HOOKS(layer_export_play_map) | ||||
| IMPLEMENT_HOOKS(layer_military) | ||||
| IMPLEMENT_HOOKS(layer_musicsound) | ||||
| IMPLEMENT_HOOKS(layer_noblelist) | ||||
| IMPLEMENT_HOOKS(layer_overall_health) | ||||
| IMPLEMENT_HOOKS(layer_reaction) | ||||
| IMPLEMENT_HOOKS(layer_squad_schedule) | ||||
| IMPLEMENT_HOOKS(layer_stockpile) | ||||
| IMPLEMENT_HOOKS(layer_stone_restriction) | ||||
| IMPLEMENT_HOOKS(layer_unit_action) | ||||
| IMPLEMENT_HOOKS(layer_unit_health) | ||||
| IMPLEMENT_HOOKS(layer_unit_relationship) | ||||
| IMPLEMENT_HOOKS(layer_world_gen_param_preset) | ||||
| IMPLEMENT_HOOKS(layer_world_gen_param) | ||||
| IMPLEMENT_HOOKS(legends) | ||||
| IMPLEMENT_HOOKS(loadgame) | ||||
| IMPLEMENT_HOOKS(locations) | ||||
| IMPLEMENT_HOOKS(meeting) | ||||
| IMPLEMENT_HOOKS(movieplayer) | ||||
| IMPLEMENT_HOOKS(noble) | ||||
| IMPLEMENT_HOOKS(option) | ||||
| IMPLEMENT_HOOKS(overallstatus) | ||||
| IMPLEMENT_HOOKS(petitions) | ||||
| IMPLEMENT_HOOKS(pet) | ||||
| IMPLEMENT_HOOKS(price) | ||||
| IMPLEMENT_HOOKS(reportlist) | ||||
| IMPLEMENT_HOOKS(requestagreement) | ||||
| IMPLEMENT_HOOKS(savegame) | ||||
| IMPLEMENT_HOOKS(selectitem) | ||||
| IMPLEMENT_HOOKS(setupadventure) | ||||
| IMPLEMENT_HOOKS(setupdwarfgame) | ||||
| IMPLEMENT_HOOKS(stores) | ||||
| IMPLEMENT_HOOKS(textviewer) | ||||
| IMPLEMENT_HOOKS(title) | ||||
| IMPLEMENT_HOOKS(topicmeeting_fill_land_holder_positions) | ||||
| IMPLEMENT_HOOKS(topicmeeting) | ||||
| IMPLEMENT_HOOKS(topicmeeting_takerequests) | ||||
| IMPLEMENT_HOOKS(tradeagreement) | ||||
| IMPLEMENT_HOOKS(tradegoods) | ||||
| IMPLEMENT_HOOKS(tradelist) | ||||
| IMPLEMENT_HOOKS(treasurelist) | ||||
| IMPLEMENT_HOOKS(unitlist) | ||||
| IMPLEMENT_HOOKS(unit) | ||||
| IMPLEMENT_HOOKS(update_region) | ||||
| IMPLEMENT_HOOKS(wages) | ||||
| IMPLEMENT_HOOKS(workquota_condition) | ||||
| IMPLEMENT_HOOKS(workquota_details) | ||||
| IMPLEMENT_HOOKS(workshop_profile) | ||||
| 
 | ||||
| #undef IMPLEMENT_HOOKS | ||||
| 
 | ||||
| DFhackCExport command_result plugin_onstatechange(color_ostream &, | ||||
|                                                   state_change_event evt) { | ||||
|     if (evt == SC_VIEWSCREEN_CHANGED) { | ||||
|         clicked = false; | ||||
|     } | ||||
|     return CR_OK; | ||||
| } | ||||
| 
 | ||||
| #define INTERPOSE_HOOKS_FAILED(screen) \ | ||||
|     !INTERPOSE_HOOK(screen##_overlay, feed).apply(enable) || \ | ||||
|     !INTERPOSE_HOOK(screen##_overlay, render).apply(enable) | ||||
| 
 | ||||
| DFhackCExport command_result plugin_enable(color_ostream &, bool enable) { | ||||
|     if (is_enabled == enable) | ||||
|         return CR_OK; | ||||
| 
 | ||||
|     if (enable != is_enabled) { | ||||
|         if (INTERPOSE_HOOKS_FAILED(adopt_region) || | ||||
|                 INTERPOSE_HOOKS_FAILED(adventure_log) || | ||||
|                 INTERPOSE_HOOKS_FAILED(announcelist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(assign_display_item) || | ||||
|                 INTERPOSE_HOOKS_FAILED(barter) || | ||||
|                 INTERPOSE_HOOKS_FAILED(buildinglist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(building) || | ||||
|                 INTERPOSE_HOOKS_FAILED(choose_start_site) || | ||||
|                 INTERPOSE_HOOKS_FAILED(civlist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(counterintelligence) || | ||||
|                 INTERPOSE_HOOKS_FAILED(createquota) || | ||||
|                 INTERPOSE_HOOKS_FAILED(customize_unit) || | ||||
|                 INTERPOSE_HOOKS_FAILED(dungeonmode) || | ||||
|                 INTERPOSE_HOOKS_FAILED(dungeon_monsterstatus) || | ||||
|                 INTERPOSE_HOOKS_FAILED(dungeon_wrestle) || | ||||
|                 INTERPOSE_HOOKS_FAILED(dwarfmode) || | ||||
|                 INTERPOSE_HOOKS_FAILED(entity) || | ||||
|                 INTERPOSE_HOOKS_FAILED(export_graphical_map) || | ||||
|                 INTERPOSE_HOOKS_FAILED(export_region) || | ||||
|                 INTERPOSE_HOOKS_FAILED(game_cleaner) || | ||||
|                 INTERPOSE_HOOKS_FAILED(image_creator) || | ||||
|                 INTERPOSE_HOOKS_FAILED(item) || | ||||
|                 INTERPOSE_HOOKS_FAILED(joblist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(jobmanagement) || | ||||
|                 INTERPOSE_HOOKS_FAILED(job) || | ||||
|                 INTERPOSE_HOOKS_FAILED(justice) || | ||||
|                 INTERPOSE_HOOKS_FAILED(kitchenpref) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_arena_creature) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_assigntrade) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_choose_language_name) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_currency) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_export_play_map) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_military) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_musicsound) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_noblelist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_overall_health) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_reaction) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_squad_schedule) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_stockpile) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_stone_restriction) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_unit_action) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_unit_health) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_unit_relationship) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_world_gen_param_preset) || | ||||
|                 INTERPOSE_HOOKS_FAILED(layer_world_gen_param) || | ||||
|                 INTERPOSE_HOOKS_FAILED(legends) || | ||||
|                 INTERPOSE_HOOKS_FAILED(loadgame) || | ||||
|                 INTERPOSE_HOOKS_FAILED(locations) || | ||||
|                 INTERPOSE_HOOKS_FAILED(meeting) || | ||||
|                 INTERPOSE_HOOKS_FAILED(movieplayer) || | ||||
|                 INTERPOSE_HOOKS_FAILED(noble) || | ||||
|                 INTERPOSE_HOOKS_FAILED(option) || | ||||
|                 INTERPOSE_HOOKS_FAILED(overallstatus) || | ||||
|                 INTERPOSE_HOOKS_FAILED(petitions) || | ||||
|                 INTERPOSE_HOOKS_FAILED(pet) || | ||||
|                 INTERPOSE_HOOKS_FAILED(price) || | ||||
|                 INTERPOSE_HOOKS_FAILED(reportlist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(requestagreement) || | ||||
|                 INTERPOSE_HOOKS_FAILED(savegame) || | ||||
|                 INTERPOSE_HOOKS_FAILED(selectitem) || | ||||
|                 INTERPOSE_HOOKS_FAILED(setupadventure) || | ||||
|                 INTERPOSE_HOOKS_FAILED(setupdwarfgame) || | ||||
|                 INTERPOSE_HOOKS_FAILED(stores) || | ||||
|                 INTERPOSE_HOOKS_FAILED(textviewer) || | ||||
|                 INTERPOSE_HOOKS_FAILED(title) || | ||||
|                 INTERPOSE_HOOKS_FAILED(topicmeeting_fill_land_holder_positions) || | ||||
|                 INTERPOSE_HOOKS_FAILED(topicmeeting) || | ||||
|                 INTERPOSE_HOOKS_FAILED(topicmeeting_takerequests) || | ||||
|                 INTERPOSE_HOOKS_FAILED(tradeagreement) || | ||||
|                 INTERPOSE_HOOKS_FAILED(tradegoods) || | ||||
|                 INTERPOSE_HOOKS_FAILED(tradelist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(treasurelist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(unitlist) || | ||||
|                 INTERPOSE_HOOKS_FAILED(unit) || | ||||
|                 INTERPOSE_HOOKS_FAILED(update_region) || | ||||
|                 INTERPOSE_HOOKS_FAILED(wages) || | ||||
|                 INTERPOSE_HOOKS_FAILED(workquota_condition) || | ||||
|                 INTERPOSE_HOOKS_FAILED(workquota_details) || | ||||
|                 INTERPOSE_HOOKS_FAILED(workshop_profile)) | ||||
|             return CR_FAILURE; | ||||
| 
 | ||||
|         is_enabled = enable; | ||||
|     } | ||||
|     return CR_OK; | ||||
| } | ||||
| 
 | ||||
| #undef INTERPOSE_HOOKS_FAILED | ||||
| 
 | ||||
| DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &) { | ||||
|     return plugin_enable(out, true); | ||||
| } | ||||
| 
 | ||||
| DFhackCExport command_result plugin_shutdown(color_ostream &out) { | ||||
|     return plugin_enable(out, false); | ||||
| } | ||||
| @ -1 +1 @@ | ||||
| Subproject commit 7bae77756f974aad765251096516a89d9d035977 | ||||
| Subproject commit 484988bb3d64f7aed9442af3b226faa77d8fd00d | ||||
		Loading…
	
		Reference in New Issue