Merge branch 'develop' into spectate

develop
Josh Cooper 2022-10-12 11:33:55 -07:00
commit ddf7850d90
55 changed files with 1460 additions and 272 deletions

@ -20,11 +20,11 @@ repos:
args: ['--fix=lf']
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.17.1
rev: 0.18.2
hooks:
- id: check-github-workflows
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.0
rev: v1.3.1
hooks:
- id: forbid-tabs
exclude_types:

@ -347,6 +347,10 @@ latex_toplevel_sectioning = 'part'
from sphinx.writers import text
text.MAXWIDTH = 52
# this value is arbitrary. it just needs to be bigger than the number of
# characters in the longest paragraph in the DFHack docs
text.MAXWIDTH = 1000000000
# this is the order that section headers will use the characters for underlines
# they are in the order of (subjective) text-mode readability
text_sectionchars = '=-~`+"*'

@ -865,8 +865,8 @@ Feel free to assign an unimportant animal to the pasture in the main entranceway
,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,`,~,,,,,,~,`,,,,,,,Cf,Cf,`,,`
,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,~,Cf,Cf,Cf,Cf,Cf,~,~,,,,,,,,Cf,`,,`
,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,~,,~,~,~,,~,~,,,,,Cf,Cf,,Cf,`,,`
,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,,Cf,,~,~,~,,Cf,,,,,,,,,Cf,`,,`
,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,`,,,~,~,~,,,`,,,,,,,Cf,Cf,`,,`
,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,,Cf,,~,~,~,,Cf,,,Cf,,,,,,Cf,`,,`
,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,`,,,~,~,~,,,`,,Cf,,,,,Cf,Cf,`,,`
,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,`
,,,`,Cf,,,,,,,,,Cf,,,,,~,,,,,Cf,,,,,,,,,Cf,`
,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`
@ -1143,8 +1143,8 @@ t1(37x33)
,,,`,,`,~,~,~,~,~,~,~,~,`,~,~,~,~,~,~,~,`,Cf,Cf,Cf,Cf,Cf,Cf,~,~,`,,`
,,,`,,`,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,`,,`
,,,`,,`,~,~,~,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,Cf,Cf,Cf,~,~,Cf,~,`,,`
,,,`,,`,Cf,~,Cf,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,`,,`
,,,`,,`,Cf,~,Cf,~,~,~,~,~,`,Cf,Cf,~,~,~,Cf,Cf,`,Cf,Cf,Cf,Cf,Cf,Cf,~,~,`,,`
,,,`,,`,Cf,~,Cf,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,~,Cf,Cf,Cf,Cf,Cf,~,`,,`
,,,`,,`,Cf,~,Cf,~,~,~,~,~,`,Cf,Cf,~,~,~,Cf,Cf,`,Cf,~,Cf,Cf,Cf,Cf,~,~,`,,`
,,,`,,`,`,`,`,`,`,`,`,`,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,`,`,`,`,`,`,`,`,`,`,,`
,,,`,~,,,,,,,,,,,Cf,Cf,Cf,~,Cf,Cf,Cf,,,,,,,,,,,~,`
,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`

Can't render this file because it has a wrong number of fields in line 56.

@ -72,6 +72,9 @@ keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity
# re-check manager orders
keybinding add Alt-R@jobmanagement/Main workorder-recheck
# workorder detail configuration
keybinding add D@workquota_details gui/workorder-details
# view combat reports for the selected unit/corpse/spatter
keybinding add Ctrl-Shift-R view-unit-reports

@ -1 +1 @@
Subproject commit ae19aebd795d6d91803e60f46de037b604593cb4
Subproject commit 6ed8aa46462ea01a1122fc49422840a2facc9757

@ -2,7 +2,7 @@
"widgets": [
{
"type": "weather",
"x": 1,
"x": 22,
"y": -1
},
{

@ -1,4 +1,4 @@
List of Authors
List of authors
===============
The following is a list of people who have contributed to DFHack, in
alphabetical order.

@ -9,7 +9,7 @@ DFHack Core
:depth: 2
Command Implementation
Command implementation
======================
DFHack commands can be implemented in any of three ways:
@ -28,7 +28,7 @@ DFHack commands can be implemented in any of three ways:
All tools distributed with DFHack are documented `here <genindex>`.
Using DFHack Commands
Using DFHack commands
=====================
DFHack commands can be executed in a number of ways:
@ -38,7 +38,7 @@ DFHack commands can be executed in a number of ways:
#. From one of several `init-files`, automatically
#. Using `script` to run a batch of commands from a file
The DFHack Console
The DFHack console
------------------
The command line has some nice line editing capabilities, including history
that's preserved between different runs of DF - use :kbd:`↑` and :kbd:`↓`
@ -115,7 +115,7 @@ second (Windows) example uses `kill-lua` to stop a Lua script.
.. _dfhack-config:
Configuration Files
Configuration files
===================
Most DFHack settings can be changed by modifying files in the ``dfhack-config``
@ -125,7 +125,7 @@ necessary.
.. _init-files:
Init Files
Init files
----------
.. contents::
@ -260,7 +260,7 @@ modified programmatically at any time through the `Lua API <lua-api-internal>`.
.. _env-vars:
Environment Variables
Environment variables
=====================
DFHack's behavior can be adjusted with some environment variables. For example,
@ -306,7 +306,7 @@ Other (non-DFHack-specific) variables that affect DFHack:
sensitive), ``DF2CONSOLE()`` will produce UTF-8-encoded text. Note that this
should be the case in most UTF-8-capable \*nix terminal emulators already.
Miscellaneous Notes
Miscellaneous notes
===================
This section is for odd but important notes that don't fit anywhere else.

@ -1,14 +1,14 @@
.. _documentation:
###########################
DFHack Documentation System
DFHack documentation system
###########################
DFHack documentation, like the file you are reading now, is created as a set of
``.rst`` files in `reStructuredText (reST) <https://www.sphinx-doc.org/rest.html>`_
format. This is a documentation format common in the Python community. It is very
similar in concept - and in syntax - to Markdown, as found on GitHub and many other
similar in concept -- and in syntax -- to Markdown, as found on GitHub and many other
places. However it is more advanced than Markdown, with more features available when
compiled to HTML, such as automatic tables of contents, cross-linking, special
external links (forum, wiki, etc) and more. The documentation is compiled by a
@ -16,18 +16,20 @@ Python tool named `Sphinx <https://www.sphinx-doc.org>`_.
The DFHack build process will compile and install the documentation so it can be
displayed in-game by the `help` and `ls` commands (and any other command or GUI that
displays help text), but this is disabled by default due to the additional Python and
Sphinx requirements. If you already have a version of the docs installed (say from a
downloaded release binary), then you only need to build the docs if you're changing them
and want to see the changes reflected in your game.
displays help text), but documentation compilation is disabled by default due to the
additional Python and Sphinx requirements. If you already have a version of the docs
installed (say from a downloaded release binary), then you only need to build the docs
if you're changing them and want to see the changes reflected in your game.
You can also build the docs if you just want a local HTML- or text-rendered copy, though
you can always read the `online version <https://dfhack.readthedocs.org>`_ too.
The active development version of the documentation is tagged with ``latest`` and
is available `here <https://docs.dfhack.org/en/latest/index.html>`_
(Note that even if you do want a local copy, it is certainly not necessary to
Note that even if you do want a local copy, it is certainly not necessary to
compile the documentation in order to read it. Like Markdown, reST documents are
designed to be just as readable in a plain-text editor as they are in HTML format.
The main thing you lose in plain text format is hyperlinking.)
The main thing you lose in plain text format is hyperlinking.
.. contents:: Contents
:local:
@ -42,19 +44,17 @@ location in ``hack/docs`` under the DF directory.
When writing documentation, remember that everything should be documented! If it's not
clear *where* a particular thing should be documented, ask on Discord or in the DFHack
thread on Bay12 -- you'll not only be getting help, you'll also be providing valuable
feedback that makes it easier for future contributers to find documentation on how to
feedback that makes it easier for future contributors to find documentation on how to
write the documentation!
Try to keep lines within 80-100 characters, so it's readable in plain text
in the terminal - Sphinx (our documentation system) will make sure
paragraphs flow.
Try to keep lines within 80-100 characters so it's readable in plain text in the
terminal - Sphinx (our documentation system) will make sure paragraphs flow.
Short descriptions
------------------
Each command that a user can run, as well as every plugin that can be enabled for some
lasting effect, needs to have a short (~54 character) descriptive string associated with
it. This description text is:
Each command that a user can run -- as well as every plugin -- needs to have a
short (~54 character) descriptive string associated with it. This description text is:
- used in-game by the `ls` command and DFHack UI screens that list commands
- used in the generated index entries in the HTML docs
@ -114,10 +114,9 @@ in a file named ``docs/gui/foobar.rst`` in the scripts repo. Similarly, a plugin
``foobaz`` should be documented in a file named ``docs/plugins/foobaz.rst`` in the dfhack repo.
For plugins, all commands provided by that plugin should be documented in that same file.
Short descriptions (the ~54 character short help) are taken from the first "sentence" of
the help text for scripts and plugins that can be enabled. This means that the help should
begin with a sentence fragment that begins with a capital letter and ends in a full stop
(``.``). Please make this brief but descriptive!
Short descriptions (the ~54 character short help) for scripts and plugins are taken from
the ``summary`` attribute of the ``dfhack-tool`` directive that each tool help document must
have (see the `Header format`_ section below). Please make this brief but descriptive!
Short descriptions for commands provided by plugins are taken from the ``description``
parameter passed to the ``PluginCommand`` constructor used when the command is registered
@ -127,11 +126,11 @@ Header format
-------------
The docs **must** begin with a heading which exactly matches the script or plugin name, underlined
with ``=====`` to the same length. This should be followed by a ``.. dfhack-tool:`` directive with
with ``=====`` to the same length. This must be followed by a ``.. dfhack-tool:`` directive with
at least the following parameters:
* ``:summary:`` - a short, single-sentence description of the tool
* ``:tags:`` - a space-separated list of tags that apply to the tool
* ``:tags:`` - a space-separated list of `tags <tag-list>` that apply to the tool
By default, ``dfhack-tool`` generates both a description of a tool and a command
with the same name. For tools (specifically plugins) that do not provide exactly
@ -183,11 +182,14 @@ And documentation for the ``autodump`` plugin might look like::
Usage help
----------
The first section after the header and introductory text should be the usage block. You can
The first section after the header and introductory text should be the usage section. You can
choose between two formats, based on whatever is cleaner or clearer for your syntax. The first
option is to show usage formats together, with an explanation following the block::
Usage::
Usage
-----
::
build-now [<options>]
build-now here [<options>]
@ -206,7 +208,8 @@ option is to show usage formats together, with an explanation following the bloc
The second option is to arrange the usage options in a list, with the full command
and arguments in monospaced font. Then indent the next line and describe the effect::
Usage:
Usage
-----
``build-now [<options>]``
Scan the entire map and build all unsuspended constructions
@ -224,6 +227,10 @@ Note that in both options, the entire commandline syntax is written, including t
Literal text is written as-is (e.g. the word ``here`` in the above example), and text that
describes the kind of parameter that is being passed (e.g. ``pos`` or ``options``) is enclosed in
angle brackets (``<`` and ``>``). Optional elements are enclosed in square brackets (``[`` and ``]``).
If the command takes an arbitrary number of elements, use ``...``, for example::
prioritize [<options>] <job type> [<job type> ...]
quickfort <command>[,<command>...] <list_id>[,<list_id>...] [<options>]
Examples
--------
@ -233,11 +240,11 @@ Otherwise, please consider adding a section that shows some real, practical usag
many users, this will be the **only** section they will read. It is so important that it is a good
idea to include the ``Examples`` section **before** you describe any extended options your command
might take. Write examples for what you expect the popular use cases will be. Also be sure to write
examples showing specific, practical values being used for any parameter that takes a value.
examples showing specific, practical values being used for any parameter that takes a value or has
tricky formatting.
Examples should go in their own subheading with a single dash underline (``--------``). The examples
themselves should be organized in a list, the same as in option 2 for Usage above. Here is an
example Examples section::
Examples should go in their own subheading. The examples themselves should be organized as in
option 2 for Usage above. Here is an example ``Examples`` section::
Examples
--------
@ -251,7 +258,7 @@ example Examples section::
Options
-------
The options header should follow the examples, with each option in the same list format as the
The options header should follow the examples, with each option in the same format as the
examples::
Options
@ -278,7 +285,8 @@ scripts and plugins can use a different mechanism to at least make their help te
in-game.
Note that since help text for external scripts and plugins is not rendered by Sphinx,
it should be written in plain text. Any reStructuredText markup will not be processed.
it should be written in plain text. Any reStructuredText markup will not be processed and,
if present, will be shown verbatim to the player (which is probably not what you want).
For external scripts, the short description comes from a comment on the first line
(the comment marker and extra whitespace is stripped). For Lua, this would look like:
@ -303,27 +311,33 @@ entire script header::
-- [====[
gui/adv-inventory
=================
Tags: adventure, items
Tags: adventure | items
Allows you to quickly move items between containers. This
includes yourself and any followers you have.
Usage:
Usage
-----
gui/adv-inventory [<options>]
Examples:
Examples
--------
gui/adv-inventory
Opens the GUI with nothing preselected
gui/adv-inventory take-all
Opens the GUI with all container items already selected and
ready to move into the adventurer's inventory.
Options:
Options
-------
take-all
Starts the GUI with container items pre-selected
give-all
Starts the GUI with your own items pre-selected
]====]
@ -340,10 +354,10 @@ Required dependencies
.. highlight:: shell
In order to build the documentation, you must have Python with Sphinx
version |sphinx_min_version| or later. Python 3 is recommended.
version |sphinx_min_version| or later and Python 3.
When installing Sphinx from OS package managers, be aware that there is
another program called Sphinx, completely unrelated to documentation management.
another program called "Sphinx", completely unrelated to documentation management.
Be sure you are installing the right Sphinx; it may be called ``python-sphinx``,
for example. To avoid doubt, ``pip`` can be used instead as detailed below.
@ -357,11 +371,10 @@ For more detailed platform-specific instructions, see the sections below:
:local:
:backlinks: none
Linux
-----
Most Linux distributions will include Python by default. If not, start by
installing Python (preferably Python 3). On Debian-based distros::
installing Python 3. On Debian-based distros::
sudo apt install python3
@ -394,21 +407,10 @@ latest Sphinx using pip::
brew install python3
pip3 install sphinx
Alternatively, you can simply install Sphinx directly from Homebrew::
brew install sphinx-doc
This will install Sphinx for macOS's system Python 2.7, without needing pip.
Either method works; if you plan to use Python for other purposes, it might best
to install Homebrew's Python 3 so that you have the latest Python as well as pip.
If not, just installing sphinx-doc for macOS's system Python 2.7 is fine.
Windows
-------
Python for Windows can be downloaded `from python.org <https://www.python.org/downloads/>`_.
The latest version of Python 3 is recommended, as it includes pip already.
The latest version of Python 3 includes pip already.
You can also install Python and pip through the Chocolatey package manager.
After installing Chocolatey as outlined in the `Windows compilation instructions <compile-windows>`,
@ -458,8 +460,9 @@ ways to do this:
By default, both HTML and text docs are built by CMake. The generated
documentation is stored in ``docs/html`` and ``docs/text`` (respectively) in the
root DFHack folder, and will be installed to ``hack/docs`` when you install
DFHack.
root DFHack folder, and they will both be installed to ``hack/docs`` when you
install DFHack. The html and txt files will intermingle, but will not interfere
with one another.
Running Sphinx manually
-----------------------
@ -467,8 +470,8 @@ Running Sphinx manually
You can also build the documentation without running CMake - this is faster if
you only want to rebuild the documentation regardless of any code changes. The
``docs/build.py`` script will build the documentation in any specified formats
(HTML only by default) using essentially the same command that CMake runs when
building the docs. Run the script with ``--help`` to see additional options.
(HTML only by default) using the same command that CMake runs when building the
docs. Run the script with ``--help`` to see additional options.
Examples:
@ -494,7 +497,9 @@ or, to build plain-text output::
Sphinx has many options to enable clean builds, parallel builds, logging, and
more - run ``sphinx-build --help`` for details. If you specify a different
output path, be warned that Sphinx may overwrite existing files in the output
folder.
folder. Also be aware that when running ``sphinx-build`` directly, the
``docs/html`` folder may be polluted with intermediate build files that normally
get written in the cmake ``build`` directory.
Building a PDF version
----------------------
@ -539,7 +544,6 @@ closest stable release after 0.44.05-alpha1). An entry listed under a stable
release like "0.44.05-r1" in changelog.txt will be listed under that release in
both the stable changelog and the development changelog.
Changelog syntax
----------------

@ -1,7 +1,7 @@
.. _introduction:
#########################
Introduction and Overview
Introduction and overview
#########################
DFHack is a Dwarf Fortress memory access library, distributed with

@ -30,6 +30,7 @@ implemented by Lua files located in :file:`hack/lua/*`
:local:
:depth: 2
.. _lua-df:
=========================
DF data structure wrapper
@ -928,9 +929,9 @@ can be omitted.
The following examples are equivalent::
dfhack.run_command({'ls', '-a'})
dfhack.run_command('ls', '-a')
dfhack.run_command('ls -a') -- not recommended
dfhack.run_command({'ls', 'quick'})
dfhack.run_command('ls', 'quick')
dfhack.run_command('ls quick') -- not recommended
* ``dfhack.run_command_silent(command[, ...])``
@ -1087,6 +1088,11 @@ Announcements
Uses the type to look up options from announcements.txt, and calls the above
operations accordingly. The units are used to call ``addCombatReportAuto``.
* ``dfhack.gui.getMousePos()``
Returns the map coordinates of the map tile the mouse is over as a table of
``{x, y, z}``. If the cursor is not over the map, returns ``nil``.
Other
~~~~~
@ -1992,6 +1998,12 @@ Functions:
Returns: *tile, tile_grayscale*, or *nil* if not found.
The values can then be used for the *tile* field of *pen* structures.
* ``dfhack.screen.hideGuard(screen,callback[,args...])``
Removes screen from the viewscreen stack, calls the callback (with optional
supplied arguments), and then restores the screen on the top of the viewscreen
stack.
* ``dfhack.screen.clear()``
Fills the screen with blank background.
@ -3108,9 +3120,11 @@ Each entry has several properties associated with it:
Returns the short (~54 character) description for the given entry.
* ``helpdb.get_entry_long_help(entry)``
* ``helpdb.get_entry_long_help(entry[, width])``
Returns the full help text for the given entry.
Returns the full help text for the given entry. If ``width`` is specified, the
text will be wrapped at that width, preserving block indents. The wrap width
defaults to 80.
* ``helpdb.get_entry_tags(entry)``
@ -4031,13 +4045,17 @@ It has the following attributes:
keys to the number of lines to scroll as positive or negative integers or one of the keywords
supported by the ``scroll`` method. The default is up/down arrows scrolling by one line and page
up/down scrolling by one page.
:show_scroll_icons: Controls scroll icons' behaviour: ``false`` for no icons, ``'right'`` or ``'left'`` for
:show_scrollbar: Controls scrollbar display: ``false`` for no scrollbar, ``'right'`` or ``'left'`` for
icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``),
``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary
(if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``.
:up_arrow_icon: The symbol for scroll up arrow. Default is ``string.char(24)`` (``↑``).
:down_arrow_icon: The symbol for scroll down arrow. Default is ``string.char(25)`` (``↓``).
:scroll_icon_pen: Specifies the pen for scroll icons. Default is ``COLOR_LIGHTCYAN``.
:scrollbar_fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN`` (the same as the native DF help screens).
:scrollbar_bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN`` (the same as the native DF help screens).
If the scrollbar is shown, it will react to mouse clicks on the scrollbar itself.
Clicking on the arrows at the top or the bottom will scroll by one line, and
clicking on the unfilled portion of the scrollbar will scroll by a half page in
that direction.
The text itself is represented as a complex structure, and passed
to the object via the ``text`` argument of the constructor, or via
@ -5084,9 +5102,8 @@ the extension omitted. For example:
* :file:`hack/scripts/gui/teleport.lua` is invoked as ``gui/teleport``
.. note::
Scripts placed in subdirectories can be run as described above, but are not
listed by the `ls` command unless ``-a`` is specified. In general, scripts
should be placed in subfolders in the following situations:
In general, scripts should be placed in subfolders in the following
situations:
* ``devel``: scripts that are intended exclusively for DFHack development,
including examples, or scripts that are experimental and unstable
@ -5104,16 +5121,12 @@ folders can be added (for example, a copy of the
:source-scripts:`scripts repository <>` for local development). See
`script-paths` for more information on how to configure this behavior.
If the first line of the script is a one-line comment (starting with ``--``),
the content of the comment is used by the built-in ``ls`` and ``help`` commands.
Such a comment is required for every script in the official DFHack repository.
Scripts are read from disk when run for the first time, or if they have changed
since the last time they were run.
Each script has an isolated environment where global variables set by the script
are stored. Values of globals persist across script runs in the same DF session.
See `devel/lua-example` for an example of this behavior. Note that local
See `devel/lua-example` for an example of this behavior. Note that ``local``
variables do *not* persist.
Arguments are passed in to the scripts via the ``...`` built-in quasi-variable;
@ -5135,9 +5148,9 @@ General script API
* ``dfhack.run_script(name[,args...])``
Run a Lua script in hack/scripts/, as if it were started from the DFHack
command-line. The ``name`` argument should be the name of the script without
its extension, as it would be used on the command line.
Run a Lua script in :file:`hack/scripts/`, as if it were started from the
DFHack command-line. The ``name`` argument should be the name of the script
without its extension, as it would be used on the command line.
Example:
@ -5157,10 +5170,10 @@ General script API
* ``dfhack.script_help([name, [extension]])``
Returns the contents of the embedded documentation of the specified script.
``extension`` defaults to "lua", and ``name`` defaults to the name of the
script where this function was called. For example, the following can be used
to print the current script's help text::
Returns the contents of the rendered (or embedded) `documentation` for the
specified script. ``extension`` defaults to "lua", and ``name`` defaults to
the name of the script where this function was called. For example, the
following can be used to print the current script's help text::
local args = {...}
if args[1] == 'help' then
@ -5168,6 +5181,7 @@ General script API
return
end
.. _reqscript:
Importing scripts
=================
@ -5222,12 +5236,12 @@ Importing scripts
.. warning::
Avoid caching the table returned by ``reqscript()`` beyond storing it in
a local or global variable as in the example above. ``reqscript()`` is fast
for scripts that have previously been loaded and haven't changed. If you
retain a reference to a table returned by an old ``reqscript()`` call, this
may lead to unintended behavior if the location of the script changes
(e.g. if a save is loaded or unloaded, or if a `script path <script-paths>`
is added in some other way).
a local variable as in the example above. ``reqscript()`` is fast for
scripts that have previously been loaded and haven't changed. If you retain
a reference to a table returned by an old ``reqscript()`` call, this may
lead to unintended behavior if the location of the script changes (e.g. if a
save is loaded or unloaded, or if a `script path <script-paths>` is added in
some other way).
.. admonition:: Tip

@ -5,7 +5,7 @@
.. _dev-changelog:
#####################
Development Changelog
Development changelog
#####################
This file contains changes grouped by the release (stable or development) in

@ -1,7 +1,7 @@
.. _remote:
=======================
DFHack Remote Interface
DFHack remote interface
=======================
DFHack provides a remote access interface that external tools can connect to and
@ -103,8 +103,6 @@ ID Method Input Output
1 RunCommand dfproto.CoreRunCommandRequest dfproto.EmptyMessage
=== ============ =============================== =======================
Conversation flow
-----------------

@ -62,6 +62,30 @@ fix/build-location
==================
The corresponding DF :bug:`5991` was fixed in DF 0.40.05.
.. _fix/diplomats:
fix/diplomats
=============
The corresponding DF :bug:`3295` was fixed in DF 0.40.05.
.. _fix/fat-dwarves:
fix/fat-dwarves
===============
The corresponding DF :bug:`5971` was fixed in DF 0.40.05.
.. _fix/feeding-timers:
fix/feeding-timers
==================
The corresponding DF :bug:`2606` was fixed in DF 0.40.12.
.. _fix/merchants:
fix/merchants
=============
Humans can now make trade agreements. This fix is no longer necessary.
.. _fortplan:
fortplan
@ -72,6 +96,14 @@ script instead. You can use your existing .csv files. Just move them to the
``blueprints`` folder in your DF installation, and instead of
``fortplan file.csv`` run ``quickfort run file.csv``.
.. _gui/assign-rack:
gui/assign-rack
===============
This script is no longer useful in current DF versions. The script required a
binpatch <binpatches/needs-patch>`, which has not been available since DF
0.34.11.
.. _gui/hack-wish:
gui/hack-wish

@ -1,5 +1,5 @@
====================
DFHack API Reference
DFHack API reference
====================
.. toctree::

@ -16,6 +16,7 @@ class SphinxOutputFormat:
def args(self):
output_dir = os.path.join('docs', self.name)
artifacts_dir = os.path.join('build', 'docs', self.name) # for artifacts not part of the final documentation
os.makedirs(artifacts_dir, mode=0o755, exist_ok=True)
return [
*self.pre_args,
'.', # source dir

@ -25,11 +25,13 @@ Usage
Examples
--------
- ``ls adventure``
Lists all commands with the ``adventure`` tag.
- ``ls --dev trigger``
Lists all commands, including developer and modding commands, that match the
substring "trigger"
``ls quick``
List all commands that match the substring "quick".
``ls adventure``
List all commands with the ``adventure`` tag.
``ls --dev trigger``
List all commands, including developer and modding commands, that match the
substring "trigger".
Options
-------

@ -2,15 +2,25 @@ tags
====
.. dfhack-tool::
:summary: List the strings that DFHack tools can be tagged with.
:summary: List the categories of DFHack tools or the tools with those tags.
:tags: dfhack
You can find groups of related tools by passing the tag name to the `ls`
command.
DFHack tools are labeled with tags so you can find groups of related commands.
This builtin command lists the tags that you can explore, or, if called with the
name of a tag, lists the tools that have that tag.
Usage
-----
::
``tags``
List the categories of DFHack tools and a description of those categories.
``tags <tag>``
List the tools that are tagged with the given tag.
tags
Examples
--------
``tags``
List the defined tags.
``tags design``
List all the tools that have the ``design`` tag.

@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## New Plugins
- `autonestbox`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autonestbox start`` to ``enable autonestbox``.
- `autobutcher`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autobutcher start`` to ``enable autobutcher``.
- `overlay`: display a "DFHack" button in the lower left corner that you can click to start the new GUI command launcher.
## New Tweaks
@ -43,12 +44,17 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `tags`: new built-in command to list the tool category tags and their definitions. tags associated with each tool are visible in the tool help and in the output of `ls`.
## Fixes
- `autochop`: designate largest trees for chopping first, instead of the smallest
- ``dfhack.run_script``: ensure the arguments passed to scripts are always strings. This allows other scripts to call ``run_script`` with numeric args and it won't break parameter parsing.
- `dig-now`: Fix direction of smoothed walls when adjacent to a door or floodgate
- ``job.removeJob()``: ensure jobs are removed from the world list when they are canceled
- `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set: declare the hospital zone before building the coffer; otherwise DF fails to stock the hospital with materials
## Misc Improvements
- Init scripts: ``dfhack.init`` and other init scripts have moved to ``dfhack-config/init/``. If you have customized your ``dfhack.init`` file and want to keep your changes, please move the part that you have customized to the new location at ``dfhack-config/init/dfhack.init``. If you do not have changes that you want to keep, do not copy anything, and the new defaults will be used automatically.
- History files: ``dfhack.history``, ``tiletypes.history``, ``lua.history``, and ``liquids.history`` have moved to the ``dfhack-config`` directory. If you'd like to keep the contents of your current history files, please move them to ``dfhack-config``.
- `do-job-now`: new global keybinding for boosting the priority of the jobs associated with the selected building/work order/unit/item etc.: Alt-N
- `gui/workorder-details`: new keybinding on the workorder details screen: ``D``
- `keybinding`: support backquote (\`) as a hotkey (and assign the hotkey to the new `gui/launcher` interface)
- `ls`: can now filter tools by substring or tag. note that dev scripts are hidden by default. pass the ``--dev`` option to show them.
- `manipulator`: add a library of useful default professions
@ -60,10 +66,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `spectate`: ``spectate auto-unpause`` is a new feature that will auto-dismiss pause events when enabled. This does not affect the player's ability to pause at a whim.
- UX: You can now move the cursor around in DFHack text fields in ``gui/`` scripts (e.g. `gui/blueprint`, `gui/quickfort`, or `gui/gm-editor`). You can move the cursor by clicking where you want it to go with the mouse or using the Left/Right arrow keys. Ctrl+Left/Right will move one word at a time, and Alt+Left/Right will move to the beginning/end of the text.
- UX: You can now click on the hotkey hint text in many ``gui/`` script windows to activate the hotkey, like a button. Not all scripts have been updated to use the clickable widget yet, but you can try it in `gui/blueprint` or `gui/quickfort`.
- `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set improvements: set traffic designations to encourage dwarves to eat in the grand hall instead of the manager's office and to eat cooked food instead of raw ingredients
- UX: Label widget scroll icons are replaced with scrollbars that represent the percentage of text on the screen and move with the position of the visible text, just like web browser scrollbars.
- `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set improvements: set traffic designations to encourage dwarves to eat cooked food instead of raw ingredients
## Documentation
- Update all DFHack tool documentation with standard syntax formatting, usage examples, and overall clarified text.
- Added `modding-guide`
- Update all DFHack tool documentation (300+ pages) with standard syntax formatting, usage examples, and overall clarified text.
- Group DFHack tools by `tag <tools>` so similar tools are grouped and easy to find
## API
@ -84,8 +92,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Lua
- History: added ``dfhack.getCommandHistory(history_id, history_filename)`` and ``dfhack.addCommandToHistory(history_id, history_filename, command)`` so gui scripts can access a commandline history without requiring a terminal.
- Added ``dfhack.screen.hideGuard()``: exposes the C++ ``Screen::Hide`` to Lua
- ``helpdb``: database and query interface for DFHack tool help text
- ``tile-material``: fix the order of declarations. The ``GetTileMat`` function now returns the material as intended (always returned nil before). Also changed the license info, with permission of the original author.
- ``utils.df_expr_to_ref()``: fixed some errors that could occur when navigating tables
- ``widgets.EditField``: new ``onsubmit2`` callback attribute is called when the user hits Shift-Enter.
- ``widgets.EditField``: new function: ``setCursor(position)`` sets the input cursor.
- ``widgets.EditField``: new attribute: ``ignore_keys`` lets you ignore specified characters if you want to use them as hotkeys

@ -1,8 +1,8 @@
.. _config-examples-guide:
.. _dfhack-examples-guide:
DFHack Example Configuration File Index
=======================================
DFHack config file examples
===========================
The :source:`hack/examples <data/examples>` folder contains ready-to-use
examples of various DFHack configuration files. You can use them by copying them
@ -14,8 +14,8 @@ The ``init/`` subfolder
-----------------------
The :source:`init/ <data/examples/init>` subfolder contains useful DFHack
`init-files` that you can copy into your main Dwarf Fortress folder -- the same
directory as ``dfhack.init``.
`init-files` that you can copy into your :file:`dfhack-config/init` folder --
the same directory as ``dfhack.init``.
.. _onMapLoad-dreamfort-init:
@ -35,7 +35,7 @@ it is useful (and customizable) for any fort. It includes the following config:
- Periodically enqueues orders to shear and milk shearable and milkable pets.
- Sets up `autofarm` to grow 30 units of every crop, except for pig tails, which
is set to 150 units to support the textile industry.
- Sets up `seedwatch` to keep 30 of every type of seed.
- Sets up `seedwatch` to protect 30 of every type of seed.
- Configures `prioritize` to automatically boost the priority of important and
time-sensitive tasks that could otherwise get ignored in busy forts, like
hauling food, tanning hides, storing items in vehicles, pulling levers, and

@ -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!

@ -1,10 +1,11 @@
.. _quickfort-alias-guide:
Quickfort Keystroke Alias Guide
===============================
Quickfort keystroke alias reference
===================================
Aliases allow you to use simple words to represent complicated key sequences
when configuring buildings and stockpiles in quickfort ``#query`` blueprints.
when configuring buildings and stockpiles in quickfort ``#query`` and
``#config`` blueprints.
For example, say you have the following ``#build`` and ``#place`` blueprints::

@ -1,8 +1,8 @@
.. _blueprint-library-guide:
.. _quickfort-library-guide:
Blueprint Library Index
=======================
Quickfort blueprint library
===========================
This guide contains a high-level overview of the blueprints available in the
:source:`quickfort blueprint library <data/blueprints/library>`.
@ -194,7 +194,7 @@ Extra blueprints that are useful in specific situations.
- :source:`library/embark.csv <data/blueprints/library/embark.csv>`
- :source:`library/pump_stack.csv <data/blueprints/library/pump_stack.csv>`
Light Aquifer Tap
Light aquifer tap
~~~~~~~~~~~~~~~~~
The aquifer tap helps you create a safe, everlasting source of fresh water from
@ -216,7 +216,7 @@ blueprint that builds important starting workshops (mason, carpenter, mechanic,
and craftsdwarf) and a ``#place`` blueprint that lays down a pattern of useful
starting stockpiles.
Pump Stack
Pump stack
~~~~~~~~~~
The pump stack blueprints help you move water and magma up to more convenient

@ -1,8 +1,8 @@
.. _quickfort-blueprint-guide:
.. _quickfort-user-guide:
Quickfort Blueprint Editing Guide
=================================
Quickfort blueprint creation guide
==================================
`Quickfort <quickfort>` is a DFHack script that helps you build fortresses from
"blueprint" .csv and .xlsx files. Many applications exist to edit these files,
@ -29,9 +29,8 @@ For those just looking to apply existing blueprints, check out the `quickfort
command's documentation <quickfort>` for syntax. There are also many
ready-to-use blueprints available in the ``blueprints/library`` subfolder in
your DFHack installation. Browse them on your computer or
:source:`online <data/blueprints/library>`, or run ``quickfort list`` at the
``[DFHack]#`` prompt to list them, and then ``quickfort run`` to apply them to
your fort!
:source:`online <data/blueprints/library>`, or run `gui/quickfort` to browse
and apply them to your fort!
Before you become an expert at writing blueprints, though, you should know that
the easiest way to make a quickfort blueprint is to build your plan "for real"

@ -1,5 +1,5 @@
========================
DFHack Development Guide
DFHack development guide
========================
These are pages relevant to people developing for DFHack.

@ -2,7 +2,7 @@ infiniteSky
===========
.. dfhack-tool::
:summary: Automatically allocates new z-levels of sky
:summary: Automatically allocate new z-levels of sky
:tags: fort design map
If enabled, this plugin will automatically allocate new z-levels of sky at the

@ -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

@ -2,7 +2,7 @@ workflow
========
.. dfhack-tool::
:summary: Manage repeat jobs according to stock levels.
:summary: Manage automated item production rules.
:tags: fort auto jobs
.. dfhack-command:: fix-job-postings

@ -114,7 +114,11 @@ class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription):
if self.arguments:
return self.arguments[0]
else:
return self.env.docname.split('/')[-1]
parts = self.env.docname.split('/')
if 'tools' in parts:
return '/'.join(parts[parts.index('tools') + 1:])
else:
return parts[-1]
@staticmethod
def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition:

@ -600,7 +600,7 @@ void help_helper(color_ostream &con, const string &entry_name) {
}
}
void tags_helper(color_ostream &con) {
void tags_helper(color_ostream &con, const string &tag) {
CoreSuspender suspend;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
@ -611,7 +611,9 @@ void tags_helper(color_ostream &con) {
return;
}
if (!Lua::SafeCall(con, L, 0, 0)) {
Lua::Push(L, tag);
if (!Lua::SafeCall(con, L, 1, 0)) {
con.printerr("Failed Lua call to helpdb.tags.\n");
}
}
@ -712,7 +714,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
}
else if (first == "tags")
{
tags_helper(con);
tags_helper(con, parts.size() ? parts[0] : "");
}
else if (first == "load" || first == "unload" || first == "reload")
{

@ -1523,6 +1523,16 @@ static int gui_getDwarfmodeViewDims(lua_State *state)
return 1;
}
static int gui_getMousePos(lua_State *L)
{
auto pos = Gui::getMousePos();
if (pos.isValid())
Lua::Push(L, pos);
else
lua_pushnil(L);
return 1;
}
static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getCurViewscreen),
WRAPM(Gui, getFocusString),
@ -1555,6 +1565,7 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
static const luaL_Reg dfhack_gui_funcs[] = {
{ "getDwarfmodeViewDims", gui_getDwarfmodeViewDims },
{ "getMousePos", gui_getMousePos },
{ NULL, NULL }
};
@ -2282,18 +2293,12 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = {
static int screen_getMousePos(lua_State *L)
{
auto pos = Screen::getMousePos();
lua_pushinteger(L, pos.x);
lua_pushinteger(L, pos.y);
return 2;
return Lua::PushPosXY(L, Screen::getMousePos());
}
static int screen_getWindowSize(lua_State *L)
{
auto pos = Screen::getWindowSize();
lua_pushinteger(L, pos.x);
lua_pushinteger(L, pos.y);
return 2;
return Lua::PushPosXY(L, Screen::getWindowSize());
}
static int screen_paintTile(lua_State *L)
@ -2370,6 +2375,21 @@ static int screen_findGraphicsTile(lua_State *L)
}
}
static int screen_hideGuard(lua_State *L) {
df::viewscreen *screen = dfhack_lua_viewscreen::get_pointer(L, 1, false);
luaL_checktype(L, 2, LUA_TFUNCTION);
// remove screen from the stack so it doesn't get returned as an output
lua_remove(L, 1);
Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP);
int nargs = lua_gettop(L) - 1;
lua_call(L, nargs, LUA_MULTRET);
return lua_gettop(L);
}
namespace {
int screen_show(lua_State *L)
@ -2472,6 +2492,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
{ "paintString", screen_paintString },
{ "fillRect", screen_fillRect },
{ "findGraphicsTile", screen_findGraphicsTile },
CWRAP(hideGuard, screen_hideGuard),
CWRAP(show, screen_show),
CWRAP(dismiss, screen_dismiss),
CWRAP(isDismissed, screen_isDismissed),

@ -126,10 +126,11 @@ namespace DFHack
DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL);
/*
* Cursor and window coords
* Cursor and window map coords
*/
DFHACK_EXPORT df::coord getViewportPos();
DFHACK_EXPORT df::coord getCursorPos();
DFHACK_EXPORT df::coord getMousePos();
static const int AREA_MAP_WIDTH = 23;
static const int MENU_WIDTH = 30;
@ -162,8 +163,6 @@ namespace DFHack
DFHACK_EXPORT bool getDesignationCoords (int32_t &x, int32_t &y, int32_t &z);
DFHACK_EXPORT bool setDesignationCoords (const int32_t x, const int32_t y, const int32_t z);
DFHACK_EXPORT bool getMousePos (int32_t & x, int32_t & y);
// The distance from the z-level of the tile at map coordinates (x, y) to the closest ground z-level below
// Defaults to 0, unless overriden by plugins
DFHACK_EXPORT int getDepthAt (int32_t x, int32_t y);

@ -5,6 +5,13 @@
#pragma once
namespace DFHack { namespace Renderer {
// If the the 'x' parameter points to this value, then the 'y' parameter will
// be interpreted as a boolean flag for whether to return map coordinates (false)
// or text tile coordinates (true). Only TWBT implements this logic, and this
// sentinel value can be removed once DF provides an API for retrieving the
// two sets of coordinates.
DFHACK_EXPORT extern const int32_t GET_MOUSE_COORDS_SENTINEL;
struct DFHACK_EXPORT renderer_wrap : public df::renderer {
void set_to_null();
void copy_from_parent();

@ -789,7 +789,11 @@ function dfhack.run_script_with_env(envVars, name, flags, ...)
end
scripts[file].env = env
scripts[file].run = script_code
return script_code(...), env
local args = {...}
for i,v in ipairs(args) do
args[i] = tostring(v) -- ensure passed parameters are strings
end
return script_code(table.unpack(args)), env
end
function dfhack.current_script_name()

@ -104,7 +104,7 @@ function getCursorPos()
end
function setCursorPos(cursor)
df.global.cursor = cursor
df.global.cursor = copyall(cursor)
end
function clearCursorPos()

@ -263,6 +263,7 @@ function EditField:onRenderBody(dc)
end_pos == #txt and '' or string.char(26))
end
dc:advance(self.text_offset):string(txt)
dc:string((' '):rep(dc.clip_x2 - dc.x))
end
function EditField:onInput(keys)
@ -544,12 +545,10 @@ Label.ATTRS{
auto_width = false,
on_click = DEFAULT_NIL,
on_rclick = DEFAULT_NIL,
--
scroll_keys = STANDARDSCROLL,
show_scroll_icons = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false
up_arrow_icon = string.char(24),
down_arrow_icon = string.char(25),
scroll_icon_pen = COLOR_LIGHTCYAN,
show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false
scrollbar_fg = COLOR_LIGHTGREEN,
scrollbar_bg = COLOR_CYAN
}
function Label:init(args)
@ -573,16 +572,16 @@ function Label:setText(text)
end
function Label:update_scroll_inset()
if self.show_scroll_icons == nil then
self._show_scroll_icons = self:getTextHeight() > self.frame_body.height and 'right' or false
if self.show_scrollbar == nil then
self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false
else
self._show_scroll_icons = self.show_scroll_icons
self._show_scrollbar = self.show_scrollbar
end
if self._show_scroll_icons then
-- here self._show_scroll_icons can only be either
if self._show_scrollbar then
-- here self._show_scrollbar can only be either
-- 'left' or any true value which we interpret as right
local l,t,r,b = gui.parse_inset(self.frame_inset)
if self._show_scroll_icons == 'left' and l <= 0 then
if self._show_scrollbar == 'left' and l <= 0 then
l = 1
elseif r <= 0 then
r = 1
@ -591,14 +590,54 @@ function Label:update_scroll_inset()
end
end
function Label:render_scroll_icons(dc, x, y1, y2)
if self.start_line_num ~= 1 then
dc:seek(x, y1):char(self.up_arrow_icon, self.scroll_icon_pen)
-- the position is the number of tiles of empty space above the top of the
-- scrollbar, and the height is the number of tiles the scrollbar should occupy
-- to represent the percentage of text that is on the screen.
local function get_scrollbar_pos_and_height(label)
local first_visible_line = label.start_line_num
local text_height = label:getTextHeight()
local last_visible_line = first_visible_line + label.frame_body.height - 1
local scrollbar_body_height = label.frame_body.height - 2
local displayed_lines = last_visible_line - first_visible_line
local height = math.floor(((displayed_lines-1) * scrollbar_body_height) /
text_height)
local max_pos = scrollbar_body_height - height
local pos = math.ceil(((first_visible_line-1) * max_pos) /
(text_height - label.frame_body.height))
return pos, height
end
local UP_ARROW_CHAR = string.char(24)
local DOWN_ARROW_CHAR = string.char(25)
local NO_ARROW_CHAR = string.char(32)
local BAR_CHAR = string.char(7)
local BAR_BG_CHAR = string.char(179)
function Label:render_scrollbar(dc, x, y1, y2)
-- render up arrow if we're not at the top
dc:seek(x, y1):char(
self.start_line_num == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR,
self.scrollbar_fg, self.scrollbar_bg)
-- render scrollbar body
local pos, height = get_scrollbar_pos_and_height(self)
local starty = y1 + pos + 1
local endy = y1 + pos + height
for y=y1+1,y2-1 do
if y >= starty and y <= endy then
dc:seek(x, y):char(BAR_CHAR, self.scrollbar_fg)
else
dc:seek(x, y):char(BAR_BG_CHAR, self.scrollbar_bg)
end
local last_visible_line = self.start_line_num + self.frame_body.height - 1
if last_visible_line < self:getTextHeight() then
dc:seek(x, y2):char(self.down_arrow_icon, self.scroll_icon_pen)
end
-- render down arrow if we're not at the bottom
local last_visible_line = self.start_line_num + self.frame_body.height - 1
dc:seek(x, y2):char(
last_visible_line >= self:getTextHeight() and
NO_ARROW_CHAR or DOWN_ARROW_CHAR,
self.scrollbar_fg, self.scrollbar_bg)
end
function Label:computeFrame(parent_rect)
@ -644,13 +683,11 @@ function Label:onRenderBody(dc)
end
function Label:onRenderFrame(dc, rect)
if self._show_scroll_icons
and self:getTextHeight() > self.frame_body.height
then
local x = self._show_scroll_icons == 'left'
if self._show_scrollbar then
local x = self._show_scrollbar == 'left'
and self.frame_body.x1-dc.x1-1
or self.frame_body.x2-dc.x1+1
self:render_scroll_icons(dc,
self:render_scrollbar(dc,
x,
self.frame_body.y1-dc.y1,
self.frame_body.y2-dc.y1
@ -658,7 +695,35 @@ function Label:onRenderFrame(dc, rect)
end
end
function Label:click_scrollbar()
if not self._show_scrollbar then return end
local rect = self.frame_body
local x, y = dscreen.getMousePos()
if self._show_scrollbar == 'left' and x ~= rect.x1-1 or x ~= rect.x2+1 then
return
end
if y < rect.y1 or y > rect.y2 then
return
end
if y == rect.y1 then
return -1
elseif y == rect.y2 then
return 1
else
local pos, height = get_scrollbar_pos_and_height(self)
if y <= rect.y1 + pos then
return '-halfpage'
elseif y > rect.y1 + pos + height then
return '+halfpage'
end
end
return nil
end
function Label:scroll(nlines)
if not nlines then return end
if type(nlines) == 'string' then
if nlines == '+page' then
nlines = self.frame_body.height
@ -676,13 +741,17 @@ function Label:scroll(nlines)
n = math.min(n, self:getTextHeight() - self.frame_body.height + 1)
n = math.max(n, 1)
self.start_line_num = n
return nlines
end
function Label:onInput(keys)
if is_disabled(self) then return false end
if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then
if keys._MOUSE_L_DOWN then
if not self:scroll(self:click_scrollbar()) and
self:getMousePos() and self.on_click then
self:on_click()
end
end
if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then
self:on_rclick()
end

@ -432,6 +432,37 @@ local function ensure_db()
index_tags()
end
local function parse_blocks(text)
local blocks = {}
for line in text:gmatch('[^\n]*') do
local _,indent = line:find('^ *')
table.insert(blocks, {line=line:trim(), indent=indent})
end
return blocks
end
local function format_block(line, indent, width)
local wrapped = line:wrap(width - indent)
if indent == 0 then return wrapped end
local padding = (' '):rep(indent)
local indented_lines = {}
for line in wrapped:gmatch('[^\n]*') do
table.insert(indented_lines, padding .. line)
end
return table.concat(indented_lines, '\n')
end
-- wraps the unwrapped source help at the specified width, preserving block
-- indents
local function rewrap(text, width)
local formatted_blocks = {}
for _,block in ipairs(parse_blocks(text)) do
table.insert(formatted_blocks,
format_block(block.line, block.indent, width))
end
return table.concat(formatted_blocks, '\n')
end
---------------------------------------------------------------------------
-- get API
---------------------------------------------------------------------------
@ -482,9 +513,10 @@ function get_entry_short_help(entry)
return get_db_property(entry, 'short_help')
end
-- returns the full help documentation associated with the entry
function get_entry_long_help(entry)
return get_db_property(entry, 'long_help')
-- returns the full help documentation associated with the entry, optionally
-- wrapped to the specified width (80 if not specified).
function get_entry_long_help(entry, width)
return rewrap(get_db_property(entry, 'long_help'), width or 80)
end
-- returns the set of tags associated with the entry
@ -687,14 +719,6 @@ local function print_columns(col1text, col2text)
end
end
-- implements the 'tags' builtin command
function tags()
local tags = get_tags()
for _,tag in ipairs(tags) do
print_columns(tag, get_tag_data(tag).description)
end
end
-- prints the requested entries to the console. include and exclude filters are
-- defined as in search_entries() above.
local function list_entries(skip_tags, include, exclude)
@ -732,4 +756,27 @@ function ls(filter_str, skip_tags, show_dev_commands)
show_dev_commands and {} or {tag='dev'})
end
local function list_tags()
local tags = get_tags()
for _,tag in ipairs(tags) do
print_columns(tag, get_tag_data(tag).description)
end
end
-- implements the 'tags' builtin command
function tags(tag)
if tag and #tag > 0 and not is_tag(tag) then
dfhack.printerr(('unrecognized tag: "%s"'):format(tag))
end
if not is_tag(tag) then
list_tags()
return
end
local skip_tags = true
local include = {entry_type={ENTRY_TYPES.COMMAND}, tag=tag}
list_entries(skip_tags, include)
end
return _ENV

@ -608,19 +608,21 @@ df_env = df_shortcut_env()
function df_expr_to_ref(expr)
expr = expr:gsub('%["(.-)"%]', function(field) return '.' .. field end)
:gsub('%[\'(.-)\'%]', function(field) return '.' .. field end)
:gsub('%[(%d+)]', function(field) return '.' .. field end)
:gsub('%[(%-?%d+)%]', function(field) return '.' .. field end)
local parts = expr:split('.', true)
local obj = df_env[parts[1]]
for i = 2, #parts do
local key = tonumber(parts[i]) or parts[i]
local cur = obj[key]
if i == #parts and ((type(cur) ~= 'userdata') or
type(cur) == 'userdata' and getmetatable(cur) == nil) then
obj = obj:_field(key)
else
obj = obj[key]
if i == #parts then
local ok, ret = pcall(function()
return obj:_field(key)
end)
if ok then
return ret
end
end
obj = obj[key]
end
return obj
end

@ -1831,17 +1831,22 @@ bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t
return true;
}
bool Gui::getMousePos (int32_t & x, int32_t & y)
{
if (gps) {
x = gps->mouse_x;
y = gps->mouse_y;
}
else {
x = -1;
y = -1;
}
return (x == -1) ? false : true;
// returns the map coordinates that the mouse cursor is over
df::coord Gui::getMousePos()
{
df::coord pos;
if (gps && gps->mouse_x > -1) {
// return invalid coords if the cursor is not over the map
DwarfmodeDims dims = getDwarfmodeViewDims();
if (gps->mouse_x < dims.map_x1 || gps->mouse_x > dims.map_x2 ||
gps->mouse_y < dims.map_y1 || gps->mouse_y > dims.map_y2) {
return pos;
}
pos = getViewportPos();
pos.x += gps->mouse_x - 1;
pos.y += gps->mouse_y - 1;
}
return pos;
}
int getDepthAt_default (int32_t x, int32_t y)

@ -9,6 +9,8 @@ using DFHack::Renderer::renderer_wrap;
static renderer_wrap *original_renderer = NULL;
const int32_t Renderer::GET_MOUSE_COORDS_SENTINEL = 0xcd1aa471;
bool init()
{
if (!original_renderer)

@ -31,6 +31,7 @@ distribution.
#include <set>
using namespace std;
#include "modules/Renderer.h"
#include "modules/Screen.h"
#include "modules/GuiHooks.h"
#include "MemAccess.h"
@ -75,12 +76,14 @@ using std::string;
* Screen painting API.
*/
// returns text grid coordinates, even if the game map is scaled differently
df::coord2d Screen::getMousePos()
{
if (!gps || (enabler && !enabler->tracking_on))
int32_t x = Renderer::GET_MOUSE_COORDS_SENTINEL, y = (int32_t)true;
if (!enabler || !enabler->renderer->get_mouse_coords(&x, &y)) {
return df::coord2d(-1, -1);
return df::coord2d(gps->mouse_x, gps->mouse_y);
}
return df::coord2d(x, y);
}
df::coord2d Screen::getWindowSize()
@ -769,7 +772,7 @@ int dfhack_lua_viewscreen::do_input(lua_State *L)
}
}
if (enabler && enabler->tracking_on)
if (enabler)
{
if (enabler->mouse_lbut_down) {
lua_pushboolean(L, true);

@ -1 +1 @@
Subproject commit dc118c5e90aea6181a290e4bbf40e7f2974fb053
Subproject commit f5fab13fb652dd953e9d59e8deca032b26131208

@ -144,6 +144,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.cpp)
dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static)
dfhack_plugin(overlay overlay.cpp)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)
dfhack_plugin(petcapRemover petcapRemover.cpp)
dfhack_plugin(plants plants.cpp)

@ -313,7 +313,7 @@ static int do_chop_designation(bool chop, bool count_only, int *skipped = nullpt
{
int count = 0;
int estimated_yield = get_log_count();
multimap<int, df::plant *> trees_by_size;
multimap<int, df::plant *, std::greater<int>> trees_by_size;
if (skipped)
{

@ -135,13 +135,12 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
last_designation[2] = desig_z;
out.print("Designation: %d %d %d\n",desig_x, desig_y, desig_z);
}
int mouse_x, mouse_y;
Gui::getMousePos(mouse_x,mouse_y);
if(mouse_x != last_mouse[0] || mouse_y != last_mouse[1])
df::coord mousePos = Gui::getMousePos();
if(mousePos.x != last_mouse[0] || mousePos.y != last_mouse[1])
{
last_mouse[0] = mouse_x;
last_mouse[1] = mouse_y;
out.print("Mouse: %d %d\n",mouse_x, mouse_y);
last_mouse[0] = mousePos.x;
last_mouse[1] = mousePos.y;
out.print("Mouse: %d %d\n",mousePos.x, mousePos.y);
}
}
return CR_OK;

@ -424,26 +424,30 @@ static bool is_smooth_wall(MapExtras::MapCache &map, const DFCoord &pos) {
&& tileShape(tt) == df::tiletype_shape::WALL;
}
static bool is_smooth_wall_or_door(MapExtras::MapCache &map,
const DFCoord &pos) {
if (is_smooth_wall(map, pos))
return true;
static bool is_connector(MapExtras::MapCache &map, const DFCoord &pos) {
df::building *bld = Buildings::findAtTile(pos);
return bld && bld->getType() == df::building_type::Door;
return bld &&
(bld->getType() == df::building_type::Door ||
bld->getType() == df::building_type::Floodgate);
}
static bool is_smooth_wall_or_connector(MapExtras::MapCache &map,
const DFCoord &pos) {
return is_smooth_wall(map, pos) || is_connector(map, pos);
}
// adds adjacent smooth walls and doors to the given tdir
static TileDirection get_adjacent_smooth_walls(MapExtras::MapCache &map,
const DFCoord &pos,
TileDirection tdir) {
if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y-1, pos.z)))
if (is_smooth_wall_or_connector(map, DFCoord(pos.x, pos.y-1, pos.z)))
tdir.north = 1;
if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y+1, pos.z)))
if (is_smooth_wall_or_connector(map, DFCoord(pos.x, pos.y+1, pos.z)))
tdir.south = 1;
if (is_smooth_wall_or_door(map, DFCoord(pos.x-1, pos.y, pos.z)))
if (is_smooth_wall_or_connector(map, DFCoord(pos.x-1, pos.y, pos.z)))
tdir.west = 1;
if (is_smooth_wall_or_door(map, DFCoord(pos.x+1, pos.y, pos.z)))
if (is_smooth_wall_or_connector(map, DFCoord(pos.x+1, pos.y, pos.z)))
tdir.east = 1;
return tdir;
}
@ -469,7 +473,7 @@ static bool adjust_smooth_wall_dir(MapExtras::MapCache &map,
const DFCoord &pos,
TileDirection tdir = BLANK_TILE_DIRECTION) {
if (!is_smooth_wall(map, pos))
return false;
return is_connector(map, pos);
tdir = ensure_valid_tdir(get_adjacent_smooth_walls(map, pos, tdir));

@ -32,7 +32,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
{
commands.push_back(PluginCommand(
"infiniteSky",
"Creates new sky levels on request, or as needed.",
"Create new sky levels on request, or as needed.",
infiniteSky));
return CR_OK;
}

@ -20,6 +20,7 @@
#include "uicommon.h"
#include "TileTypes.h"
#include "DataFuncs.h"
#include "Debug.h"
DFHACK_PLUGIN("mousequery");
REQUIRE_GLOBAL(enabler);
@ -32,6 +33,10 @@ using namespace df::enums::ui_sidebar_mode;
#define PLUGIN_VERSION 0.18
namespace DFHack {
DBG_DECLARE(mousequery,log,DebugCategory::LINFO);
}
static int32_t last_clicked_x, last_clicked_y, last_clicked_z;
static int32_t last_pos_x, last_pos_y, last_pos_z;
static df::coord last_move_pos;
@ -53,22 +58,12 @@ static enum { None, Left, Right } drag_mode;
static df::coord get_mouse_pos(int32_t &mx, int32_t &my)
{
df::coord pos;
pos.x = -30000;
df::coord pos = Gui::getMousePos();
pos.z -= Gui::getDepthAt(pos.x, pos.y);
if (!enabler->tracking_on)
return pos;
if (!Gui::getMousePos(mx, my))
return pos;
int32_t vx, vy, vz;
if (!Gui::getViewCoords(vx, vy, vz))
return pos;
pos.x = vx + mx - 1;
pos.y = vy + my - 1;
pos.z = vz - Gui::getDepthAt(mx, my);
df::coord vpos = Gui::getViewportPos();
mx = pos.x - vpos.x + 1;
my = pos.y - vpos.y + 1;
return pos;
}
@ -536,6 +531,7 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
if (mpos.x == x && mpos.y == y && mpos.z == z)
return;
DEBUG(log).print("moving cursor to %d, %d, %d\n", x, y, z);
Gui::setCursorCoords(mpos.x, mpos.y, mpos.z);
Gui::refreshSidebar();
}
@ -575,8 +571,6 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
int32_t mx, my;
auto mpos = get_mouse_pos(mx, my);
bool mpos_valid = mpos.x != -30000 && mpos.y != -30000 && mpos.z != -30000;
if (mx < 1 || mx > dims.map_x2 || my < 1 || my > dims.map_y2)
mpos_valid = false;
// Check if in lever binding mode
if (Gui::getFocusString(Core::getTopViewscreen()) ==
@ -723,7 +717,7 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
}
}
OutputString(color, mx, my, "X", false, 0, 0, true);
Screen::paintTile(Screen::Pen('X', color), mx, my, true);
return;
}

@ -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

@ -29,7 +29,7 @@ function test.correct_frame_body_with_scroll_icons()
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scroll_icons.")
expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scrollbar.")
end
function test.correct_frame_body_with_few_text_lines()
@ -50,10 +50,10 @@ function test.correct_frame_body_with_few_text_lines()
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.")
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.")
end
function test.correct_frame_body_without_show_scroll_icons()
function test.correct_frame_body_without_show_scrollbar()
local t = {}
for i = 1, 12 do
t[#t+1] = tostring(i)
@ -66,13 +66,13 @@ function test.correct_frame_body_without_show_scroll_icons()
view_id = 'text',
frame_inset = 0,
text = t,
show_scroll_icons = false,
show_scrollbar = false,
},
}
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.")
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.")
end
function test.scroll()

@ -398,8 +398,25 @@ function test.get_entry_short_help()
end
function test.get_entry_long_help()
local expected = [[
basic
*****
**Tags:** map
**Command:**
"basic"
Documented
basic.
Documented
full help.
]]
expect.eq(expected, h.get_entry_long_help('basic', 13))
-- long help for plugins/commands that have doc files should match the
-- contents of those files exactly
-- contents of those files exactly (test data is already wrapped)
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
h.get_entry_long_help('hascommands'))
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
@ -605,6 +622,20 @@ function test.tags()
end)
end
function test.tags_tag()
local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.tags('armok')
expect.eq(3, mock_print.call_count)
expect.eq('bindboxers Bind your boxers.',
mock_print.call_args[1][1])
expect.eq('boxbinders Box your binders.',
mock_print.call_args[2][1])
expect.eq('samename Samename.',
mock_print.call_args[3][1])
end)
end
function test.ls()
local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()

@ -54,3 +54,62 @@ function test.invert_overwrite()
expect.eq(i.b, 2)
expect.eq(i.a, 3)
end
function test.df_expr_to_ref()
-- userdata field
expect.eq(utils.df_expr_to_ref('df.global.world.engravings'), df.global.world.engravings)
expect.eq(utils.df_expr_to_ref('df.global.world.engravings'), df.global.world:_field('engravings'))
-- primitive field
expect.eq(utils.df_expr_to_ref('df.global.world.original_save_version'), df.global.world:_field('original_save_version'))
-- table field
expect.eq(utils.df_expr_to_ref('df.global.world'), df.global.world)
expect.eq(utils.df_expr_to_ref('df.global'), df.global)
-- table
expect.eq(utils.df_expr_to_ref('df'), df)
-- userdata object
expect.eq(utils.df_expr_to_ref('scr'), dfhack.gui.getCurViewscreen())
local fake_unit
mock.patch(dfhack.gui, 'getSelectedUnit', function() return fake_unit end, function()
-- lightuserdata field
fake_unit = {
null_field=df.NULL,
}
expect.eq(utils.df_expr_to_ref('unit'), fake_unit)
expect.eq(utils.df_expr_to_ref('unit.null_field'), fake_unit.null_field)
dfhack.with_temp_object(df.unit:new(), function(u)
fake_unit = u
-- userdata field
expect.eq(utils.df_expr_to_ref('unit.name'), fake_unit.name)
expect.eq(utils.df_expr_to_ref('unit.name'), fake_unit:_field('name'))
-- primitive field
expect.eq(utils.df_expr_to_ref('unit.profession'), fake_unit:_field('profession'))
end)
-- vector items
dfhack.with_temp_object(df.new('ptr-vector'), function(vec)
fake_unit = vec
vec:insert('#', df.global.world)
vec:insert('#', df.global.ui)
expect.eq(utils.df_expr_to_ref('unit'), vec)
expect.eq(utils.df_expr_to_ref('unit[0]'), utils.df_expr_to_ref('unit.0'))
expect.eq(df.reinterpret_cast(df.world, utils.df_expr_to_ref('unit[0]').value), df.global.world)
expect.eq(utils.df_expr_to_ref('unit[1]'), utils.df_expr_to_ref('unit.1'))
expect.eq(df.reinterpret_cast(df.ui, utils.df_expr_to_ref('unit[1]').value), df.global.ui)
expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit.2') end)
expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit[2]') end)
expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit.-1') end)
expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit[-1]') end)
expect.error_match('not found', function() utils.df_expr_to_ref('unit.a') end)
end)
end)
end