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'] args: ['--fix=lf']
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.17.1 rev: 0.18.2
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.0 rev: v1.3.1
hooks: hooks:
- id: forbid-tabs - id: forbid-tabs
exclude_types: exclude_types:

@ -347,6 +347,10 @@ latex_toplevel_sectioning = 'part'
from sphinx.writers import text 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 = '=-~`+"*' 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,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,~,~,~,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 # re-check manager orders
keybinding add Alt-R@jobmanagement/Main workorder-recheck 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 # view combat reports for the selected unit/corpse/spatter
keybinding add Ctrl-Shift-R view-unit-reports keybinding add Ctrl-Shift-R view-unit-reports

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

@ -2,7 +2,7 @@
"widgets": [ "widgets": [
{ {
"type": "weather", "type": "weather",
"x": 1, "x": 22,
"y": -1 "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 The following is a list of people who have contributed to DFHack, in
alphabetical order. alphabetical order.

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

@ -1,14 +1,14 @@
.. _documentation: .. _documentation:
########################### ###########################
DFHack Documentation System DFHack documentation system
########################### ###########################
DFHack documentation, like the file you are reading now, is created as a set of 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>`_ ``.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 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 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 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 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 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 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 displays help text), but documentation compilation is disabled by default due to the
Sphinx requirements. If you already have a version of the docs installed (say from a additional Python and Sphinx requirements. If you already have a version of the docs
downloaded release binary), then you only need to build the docs if you're changing them installed (say from a downloaded release binary), then you only need to build the docs
and want to see the changes reflected in your game. 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 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. 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 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. 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 .. contents:: Contents
:local: :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 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 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 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! write the documentation!
Try to keep lines within 80-100 characters, so it's readable in plain text Try to keep lines within 80-100 characters so it's readable in plain text in the
in the terminal - Sphinx (our documentation system) will make sure terminal - Sphinx (our documentation system) will make sure paragraphs flow.
paragraphs flow.
Short descriptions Short descriptions
------------------ ------------------
Each command that a user can run, as well as every plugin that can be enabled for some Each command that a user can run -- as well as every plugin -- needs to have a
lasting effect, needs to have a short (~54 character) descriptive string associated with short (~54 character) descriptive string associated with it. This description text is:
it. This description text is:
- used in-game by the `ls` command and DFHack UI screens that list commands - used in-game by the `ls` command and DFHack UI screens that list commands
- used in the generated index entries in the HTML docs - 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. ``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. 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 Short descriptions (the ~54 character short help) for scripts and plugins are taken from
the help text for scripts and plugins that can be enabled. This means that the help should the ``summary`` attribute of the ``dfhack-tool`` directive that each tool help document must
begin with a sentence fragment that begins with a capital letter and ends in a full stop have (see the `Header format`_ section below). Please make this brief but descriptive!
(``.``). Please make this brief but descriptive!
Short descriptions for commands provided by plugins are taken from the ``description`` Short descriptions for commands provided by plugins are taken from the ``description``
parameter passed to the ``PluginCommand`` constructor used when the command is registered 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 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: at least the following parameters:
* ``:summary:`` - a short, single-sentence description of the tool * ``: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 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 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 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 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:: option is to show usage formats together, with an explanation following the block::
Usage:: Usage
-----
::
build-now [<options>] build-now [<options>]
build-now here [<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 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:: and arguments in monospaced font. Then indent the next line and describe the effect::
Usage: Usage
-----
``build-now [<options>]`` ``build-now [<options>]``
Scan the entire map and build all unsuspended constructions 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 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 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 ``]``). 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 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 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 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 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 Examples should go in their own subheading. The examples themselves should be organized as in
themselves should be organized in a list, the same as in option 2 for Usage above. Here is an option 2 for Usage above. Here is an example ``Examples`` section::
example Examples section::
Examples Examples
-------- --------
@ -251,7 +258,7 @@ example Examples section::
Options 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:: examples::
Options Options
@ -278,7 +285,8 @@ scripts and plugins can use a different mechanism to at least make their help te
in-game. in-game.
Note that since help text for external scripts and plugins is not rendered by Sphinx, 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 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: (the comment marker and extra whitespace is stripped). For Lua, this would look like:
@ -303,27 +311,33 @@ entire script header::
-- [====[ -- [====[
gui/adv-inventory gui/adv-inventory
================= =================
Tags: adventure, items
Tags: adventure | items
Allows you to quickly move items between containers. This Allows you to quickly move items between containers. This
includes yourself and any followers you have. includes yourself and any followers you have.
Usage: Usage
-----
gui/adv-inventory [<options>] gui/adv-inventory [<options>]
Examples: Examples
--------
gui/adv-inventory gui/adv-inventory
Opens the GUI with nothing preselected Opens the GUI with nothing preselected
gui/adv-inventory take-all gui/adv-inventory take-all
Opens the GUI with all container items already selected and Opens the GUI with all container items already selected and
ready to move into the adventurer's inventory. ready to move into the adventurer's inventory.
Options: Options
-------
take-all take-all
Starts the GUI with container items pre-selected Starts the GUI with container items pre-selected
give-all give-all
Starts the GUI with your own items pre-selected Starts the GUI with your own items pre-selected
]====] ]====]
@ -340,10 +354,10 @@ Required dependencies
.. highlight:: shell .. highlight:: shell
In order to build the documentation, you must have Python with Sphinx 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 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``, 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. 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: :local:
:backlinks: none :backlinks: none
Linux Linux
----- -----
Most Linux distributions will include Python by default. If not, start by 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 sudo apt install python3
@ -394,21 +407,10 @@ latest Sphinx using pip::
brew install python3 brew install python3
pip3 install sphinx 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 Windows
------- -------
Python for Windows can be downloaded `from python.org <https://www.python.org/downloads/>`_. 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. You can also install Python and pip through the Chocolatey package manager.
After installing Chocolatey as outlined in the `Windows compilation instructions <compile-windows>`, 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 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 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 root DFHack folder, and they will both be installed to ``hack/docs`` when you
DFHack. install DFHack. The html and txt files will intermingle, but will not interfere
with one another.
Running Sphinx manually Running Sphinx manually
----------------------- -----------------------
@ -467,8 +470,8 @@ Running Sphinx manually
You can also build the documentation without running CMake - this is faster if 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 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 ``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 (HTML only by default) using the same command that CMake runs when building the
building the docs. Run the script with ``--help`` to see additional options. docs. Run the script with ``--help`` to see additional options.
Examples: Examples:
@ -494,7 +497,9 @@ or, to build plain-text output::
Sphinx has many options to enable clean builds, parallel builds, logging, and Sphinx has many options to enable clean builds, parallel builds, logging, and
more - run ``sphinx-build --help`` for details. If you specify a different 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 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 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 release like "0.44.05-r1" in changelog.txt will be listed under that release in
both the stable changelog and the development changelog. both the stable changelog and the development changelog.
Changelog syntax Changelog syntax
---------------- ----------------

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

@ -30,6 +30,7 @@ implemented by Lua files located in :file:`hack/lua/*`
:local: :local:
:depth: 2 :depth: 2
.. _lua-df:
========================= =========================
DF data structure wrapper DF data structure wrapper
@ -928,9 +929,9 @@ can be omitted.
The following examples are equivalent:: The following examples are equivalent::
dfhack.run_command({'ls', '-a'}) dfhack.run_command({'ls', 'quick'})
dfhack.run_command('ls', '-a') dfhack.run_command('ls', 'quick')
dfhack.run_command('ls -a') -- not recommended dfhack.run_command('ls quick') -- not recommended
* ``dfhack.run_command_silent(command[, ...])`` * ``dfhack.run_command_silent(command[, ...])``
@ -1087,6 +1088,11 @@ Announcements
Uses the type to look up options from announcements.txt, and calls the above Uses the type to look up options from announcements.txt, and calls the above
operations accordingly. The units are used to call ``addCombatReportAuto``. 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 Other
~~~~~ ~~~~~
@ -1992,6 +1998,12 @@ Functions:
Returns: *tile, tile_grayscale*, or *nil* if not found. Returns: *tile, tile_grayscale*, or *nil* if not found.
The values can then be used for the *tile* field of *pen* structures. 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()`` * ``dfhack.screen.clear()``
Fills the screen with blank background. 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. 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)`` * ``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 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 supported by the ``scroll`` method. The default is up/down arrows scrolling by one line and page
up/down scrolling by one 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``), 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 ``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``. (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)`` (``↑``). :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).
:down_arrow_icon: The symbol for scroll down arrow. Default is ``string.char(25)`` (``↓``). :scrollbar_bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN`` (the same as the native DF help screens).
:scroll_icon_pen: Specifies the pen for scroll icons. Default is ``COLOR_LIGHTCYAN``.
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 The text itself is represented as a complex structure, and passed
to the object via the ``text`` argument of the constructor, or via 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`` * :file:`hack/scripts/gui/teleport.lua` is invoked as ``gui/teleport``
.. note:: .. note::
Scripts placed in subdirectories can be run as described above, but are not In general, scripts should be placed in subfolders in the following
listed by the `ls` command unless ``-a`` is specified. In general, scripts situations:
should be placed in subfolders in the following situations:
* ``devel``: scripts that are intended exclusively for DFHack development, * ``devel``: scripts that are intended exclusively for DFHack development,
including examples, or scripts that are experimental and unstable 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 :source-scripts:`scripts repository <>` for local development). See
`script-paths` for more information on how to configure this behavior. `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 Scripts are read from disk when run for the first time, or if they have changed
since the last time they were run. since the last time they were run.
Each script has an isolated environment where global variables set by the script 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. 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. variables do *not* persist.
Arguments are passed in to the scripts via the ``...`` built-in quasi-variable; 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...])`` * ``dfhack.run_script(name[,args...])``
Run a Lua script in hack/scripts/, as if it were started from the DFHack Run a Lua script in :file:`hack/scripts/`, as if it were started from the
command-line. The ``name`` argument should be the name of the script without DFHack command-line. The ``name`` argument should be the name of the script
its extension, as it would be used on the command line. without its extension, as it would be used on the command line.
Example: Example:
@ -5157,10 +5170,10 @@ General script API
* ``dfhack.script_help([name, [extension]])`` * ``dfhack.script_help([name, [extension]])``
Returns the contents of the embedded documentation of the specified script. Returns the contents of the rendered (or embedded) `documentation` for the
``extension`` defaults to "lua", and ``name`` defaults to the name of the specified script. ``extension`` defaults to "lua", and ``name`` defaults to
script where this function was called. For example, the following can be used the name of the script where this function was called. For example, the
to print the current script's help text:: following can be used to print the current script's help text::
local args = {...} local args = {...}
if args[1] == 'help' then if args[1] == 'help' then
@ -5168,6 +5181,7 @@ General script API
return return
end end
.. _reqscript:
Importing scripts Importing scripts
================= =================
@ -5222,12 +5236,12 @@ Importing scripts
.. warning:: .. warning::
Avoid caching the table returned by ``reqscript()`` beyond storing it in Avoid caching the table returned by ``reqscript()`` beyond storing it in
a local or global variable as in the example above. ``reqscript()`` is fast a local variable as in the example above. ``reqscript()`` is fast for
for scripts that have previously been loaded and haven't changed. If you scripts that have previously been loaded and haven't changed. If you retain
retain a reference to a table returned by an old ``reqscript()`` call, this a reference to a table returned by an old ``reqscript()`` call, this may
may lead to unintended behavior if the location of the script changes lead to unintended behavior if the location of the script changes (e.g. if a
(e.g. if a save is loaded or unloaded, or if a `script path <script-paths>` save is loaded or unloaded, or if a `script path <script-paths>` is added in
is added in some other way). some other way).
.. admonition:: Tip .. admonition:: Tip

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

@ -1,7 +1,7 @@
.. _remote: .. _remote:
======================= =======================
DFHack Remote Interface DFHack remote interface
======================= =======================
DFHack provides a remote access interface that external tools can connect to and 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 1 RunCommand dfproto.CoreRunCommandRequest dfproto.EmptyMessage
=== ============ =============================== ======================= === ============ =============================== =======================
Conversation flow Conversation flow
----------------- -----------------

@ -62,6 +62,30 @@ fix/build-location
================== ==================
The corresponding DF :bug:`5991` was fixed in DF 0.40.05. 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:
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 ``blueprints`` folder in your DF installation, and instead of
``fortplan file.csv`` run ``quickfort run file.csv``. ``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:
gui/hack-wish gui/hack-wish

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

@ -16,6 +16,7 @@ class SphinxOutputFormat:
def args(self): def args(self):
output_dir = os.path.join('docs', self.name) 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 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 [ return [
*self.pre_args, *self.pre_args,
'.', # source dir '.', # source dir

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

@ -2,15 +2,25 @@ tags
==== ====
.. dfhack-tool:: .. 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 :tags: dfhack
You can find groups of related tools by passing the tag name to the `ls` DFHack tools are labeled with tags so you can find groups of related commands.
command. 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 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 ## 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``. - `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``. - `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 ## 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`. - `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 ## 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 - ``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 ## 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. - 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``. - 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 - `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) - `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. - `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 - `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. - `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 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`. - 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 ## 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 - Group DFHack tools by `tag <tools>` so similar tools are grouped and easy to find
## API ## API
@ -84,8 +92,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Lua ## 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. - 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 - ``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. - ``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 ``onsubmit2`` callback attribute is called when the user hits Shift-Enter.
- ``widgets.EditField``: new function: ``setCursor(position)`` sets the input cursor. - ``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 - ``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: .. _config-examples-guide:
.. _dfhack-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 The :source:`hack/examples <data/examples>` folder contains ready-to-use
examples of various DFHack configuration files. You can use them by copying them 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 The :source:`init/ <data/examples/init>` subfolder contains useful DFHack
`init-files` that you can copy into your main Dwarf Fortress folder -- the same `init-files` that you can copy into your :file:`dfhack-config/init` folder --
directory as ``dfhack.init``. the same directory as ``dfhack.init``.
.. _onMapLoad-dreamfort-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. - 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 - 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. 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 - Configures `prioritize` to automatically boost the priority of important and
time-sensitive tasks that could otherwise get ignored in busy forts, like time-sensitive tasks that could otherwise get ignored in busy forts, like
hauling food, tanning hides, storing items in vehicles, pulling levers, and 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. These pages are detailed guides covering DFHack tools.
.. toctree:: .. toctree::
:maxdepth: 1 :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-alias-guide:
Quickfort Keystroke Alias Guide Quickfort keystroke alias reference
=============================== ===================================
Aliases allow you to use simple words to represent complicated key sequences 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:: For example, say you have the following ``#build`` and ``#place`` blueprints::

@ -1,8 +1,8 @@
.. _blueprint-library-guide: .. _blueprint-library-guide:
.. _quickfort-library-guide: .. _quickfort-library-guide:
Blueprint Library Index Quickfort blueprint library
======================= ===========================
This guide contains a high-level overview of the blueprints available in the This guide contains a high-level overview of the blueprints available in the
:source:`quickfort blueprint library <data/blueprints/library>`. :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/embark.csv <data/blueprints/library/embark.csv>`
- :source:`library/pump_stack.csv <data/blueprints/library/pump_stack.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 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 and craftsdwarf) and a ``#place`` blueprint that lays down a pattern of useful
starting stockpiles. starting stockpiles.
Pump Stack Pump stack
~~~~~~~~~~ ~~~~~~~~~~
The pump stack blueprints help you move water and magma up to more convenient The pump stack blueprints help you move water and magma up to more convenient

@ -1,8 +1,8 @@
.. _quickfort-blueprint-guide: .. _quickfort-blueprint-guide:
.. _quickfort-user-guide: .. _quickfort-user-guide:
Quickfort Blueprint Editing Guide Quickfort blueprint creation guide
================================= ==================================
`Quickfort <quickfort>` is a DFHack script that helps you build fortresses from `Quickfort <quickfort>` is a DFHack script that helps you build fortresses from
"blueprint" .csv and .xlsx files. Many applications exist to edit these files, "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 command's documentation <quickfort>` for syntax. There are also many
ready-to-use blueprints available in the ``blueprints/library`` subfolder in ready-to-use blueprints available in the ``blueprints/library`` subfolder in
your DFHack installation. Browse them on your computer or your DFHack installation. Browse them on your computer or
:source:`online <data/blueprints/library>`, or run ``quickfort list`` at the :source:`online <data/blueprints/library>`, or run `gui/quickfort` to browse
``[DFHack]#`` prompt to list them, and then ``quickfort run`` to apply them to and apply them to your fort!
your fort!
Before you become an expert at writing blueprints, though, you should know that 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" 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. These are pages relevant to people developing for DFHack.

@ -2,7 +2,7 @@ infiniteSky
=========== ===========
.. dfhack-tool:: .. dfhack-tool::
:summary: Automatically allocates new z-levels of sky :summary: Automatically allocate new z-levels of sky
:tags: fort design map :tags: fort design map
If enabled, this plugin will automatically allocate new z-levels of sky at the 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:: .. dfhack-tool::
:summary: Manage repeat jobs according to stock levels. :summary: Manage automated item production rules.
:tags: fort auto jobs :tags: fort auto jobs
.. dfhack-command:: fix-job-postings .. dfhack-command:: fix-job-postings

@ -114,7 +114,11 @@ class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription):
if self.arguments: if self.arguments:
return self.arguments[0] return self.arguments[0]
else: 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 @staticmethod
def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition: 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; CoreSuspender suspend;
auto L = Lua::Core::State; auto L = Lua::Core::State;
Lua::StackUnwinder top(L); Lua::StackUnwinder top(L);
@ -611,7 +611,9 @@ void tags_helper(color_ostream &con) {
return; 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"); 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") else if (first == "tags")
{ {
tags_helper(con); tags_helper(con, parts.size() ? parts[0] : "");
} }
else if (first == "load" || first == "unload" || first == "reload") else if (first == "load" || first == "unload" || first == "reload")
{ {

@ -1523,6 +1523,16 @@ static int gui_getDwarfmodeViewDims(lua_State *state)
return 1; 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[] = { static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getCurViewscreen), WRAPM(Gui, getCurViewscreen),
WRAPM(Gui, getFocusString), WRAPM(Gui, getFocusString),
@ -1555,6 +1565,7 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
static const luaL_Reg dfhack_gui_funcs[] = { static const luaL_Reg dfhack_gui_funcs[] = {
{ "getDwarfmodeViewDims", gui_getDwarfmodeViewDims }, { "getDwarfmodeViewDims", gui_getDwarfmodeViewDims },
{ "getMousePos", gui_getMousePos },
{ NULL, NULL } { NULL, NULL }
}; };
@ -2282,18 +2293,12 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = {
static int screen_getMousePos(lua_State *L) static int screen_getMousePos(lua_State *L)
{ {
auto pos = Screen::getMousePos(); return Lua::PushPosXY(L, Screen::getMousePos());
lua_pushinteger(L, pos.x);
lua_pushinteger(L, pos.y);
return 2;
} }
static int screen_getWindowSize(lua_State *L) static int screen_getWindowSize(lua_State *L)
{ {
auto pos = Screen::getWindowSize(); return Lua::PushPosXY(L, Screen::getWindowSize());
lua_pushinteger(L, pos.x);
lua_pushinteger(L, pos.y);
return 2;
} }
static int screen_paintTile(lua_State *L) 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 { namespace {
int screen_show(lua_State *L) int screen_show(lua_State *L)
@ -2472,6 +2492,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
{ "paintString", screen_paintString }, { "paintString", screen_paintString },
{ "fillRect", screen_fillRect }, { "fillRect", screen_fillRect },
{ "findGraphicsTile", screen_findGraphicsTile }, { "findGraphicsTile", screen_findGraphicsTile },
CWRAP(hideGuard, screen_hideGuard),
CWRAP(show, screen_show), CWRAP(show, screen_show),
CWRAP(dismiss, screen_dismiss), CWRAP(dismiss, screen_dismiss),
CWRAP(isDismissed, screen_isDismissed), 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); 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 getViewportPos();
DFHACK_EXPORT df::coord getCursorPos(); DFHACK_EXPORT df::coord getCursorPos();
DFHACK_EXPORT df::coord getMousePos();
static const int AREA_MAP_WIDTH = 23; static const int AREA_MAP_WIDTH = 23;
static const int MENU_WIDTH = 30; 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 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 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 // 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 // Defaults to 0, unless overriden by plugins
DFHACK_EXPORT int getDepthAt (int32_t x, int32_t y); DFHACK_EXPORT int getDepthAt (int32_t x, int32_t y);

@ -5,6 +5,13 @@
#pragma once #pragma once
namespace DFHack { namespace Renderer { 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 { struct DFHACK_EXPORT renderer_wrap : public df::renderer {
void set_to_null(); void set_to_null();
void copy_from_parent(); void copy_from_parent();

@ -789,7 +789,11 @@ function dfhack.run_script_with_env(envVars, name, flags, ...)
end end
scripts[file].env = env scripts[file].env = env
scripts[file].run = script_code 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 end
function dfhack.current_script_name() function dfhack.current_script_name()

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

@ -263,6 +263,7 @@ function EditField:onRenderBody(dc)
end_pos == #txt and '' or string.char(26)) end_pos == #txt and '' or string.char(26))
end end
dc:advance(self.text_offset):string(txt) dc:advance(self.text_offset):string(txt)
dc:string((' '):rep(dc.clip_x2 - dc.x))
end end
function EditField:onInput(keys) function EditField:onInput(keys)
@ -544,12 +545,10 @@ Label.ATTRS{
auto_width = false, auto_width = false,
on_click = DEFAULT_NIL, on_click = DEFAULT_NIL,
on_rclick = DEFAULT_NIL, on_rclick = DEFAULT_NIL,
--
scroll_keys = STANDARDSCROLL, scroll_keys = STANDARDSCROLL,
show_scroll_icons = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false
up_arrow_icon = string.char(24), scrollbar_fg = COLOR_LIGHTGREEN,
down_arrow_icon = string.char(25), scrollbar_bg = COLOR_CYAN
scroll_icon_pen = COLOR_LIGHTCYAN,
} }
function Label:init(args) function Label:init(args)
@ -573,16 +572,16 @@ function Label:setText(text)
end end
function Label:update_scroll_inset() function Label:update_scroll_inset()
if self.show_scroll_icons == nil then if self.show_scrollbar == nil then
self._show_scroll_icons = self:getTextHeight() > self.frame_body.height and 'right' or false self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false
else else
self._show_scroll_icons = self.show_scroll_icons self._show_scrollbar = self.show_scrollbar
end end
if self._show_scroll_icons then if self._show_scrollbar then
-- here self._show_scroll_icons can only be either -- here self._show_scrollbar can only be either
-- 'left' or any true value which we interpret as right -- 'left' or any true value which we interpret as right
local l,t,r,b = gui.parse_inset(self.frame_inset) 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 l = 1
elseif r <= 0 then elseif r <= 0 then
r = 1 r = 1
@ -591,14 +590,54 @@ function Label:update_scroll_inset()
end end
end end
function Label:render_scroll_icons(dc, x, y1, y2) -- the position is the number of tiles of empty space above the top of the
if self.start_line_num ~= 1 then -- scrollbar, and the height is the number of tiles the scrollbar should occupy
dc:seek(x, y1):char(self.up_arrow_icon, self.scroll_icon_pen) -- 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 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 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 end
function Label:computeFrame(parent_rect) function Label:computeFrame(parent_rect)
@ -644,13 +683,11 @@ function Label:onRenderBody(dc)
end end
function Label:onRenderFrame(dc, rect) function Label:onRenderFrame(dc, rect)
if self._show_scroll_icons if self._show_scrollbar then
and self:getTextHeight() > self.frame_body.height local x = self._show_scrollbar == 'left'
then
local x = self._show_scroll_icons == 'left'
and self.frame_body.x1-dc.x1-1 and self.frame_body.x1-dc.x1-1
or self.frame_body.x2-dc.x1+1 or self.frame_body.x2-dc.x1+1
self:render_scroll_icons(dc, self:render_scrollbar(dc,
x, x,
self.frame_body.y1-dc.y1, self.frame_body.y1-dc.y1,
self.frame_body.y2-dc.y1 self.frame_body.y2-dc.y1
@ -658,7 +695,35 @@ function Label:onRenderFrame(dc, rect)
end end
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) function Label:scroll(nlines)
if not nlines then return end
if type(nlines) == 'string' then if type(nlines) == 'string' then
if nlines == '+page' then if nlines == '+page' then
nlines = self.frame_body.height 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.min(n, self:getTextHeight() - self.frame_body.height + 1)
n = math.max(n, 1) n = math.max(n, 1)
self.start_line_num = n self.start_line_num = n
return nlines
end end
function Label:onInput(keys) function Label:onInput(keys)
if is_disabled(self) then return false end 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() self:on_click()
end end
end
if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then
self:on_rclick() self:on_rclick()
end end

@ -432,6 +432,37 @@ local function ensure_db()
index_tags() index_tags()
end 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 -- get API
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
@ -482,9 +513,10 @@ function get_entry_short_help(entry)
return get_db_property(entry, 'short_help') return get_db_property(entry, 'short_help')
end end
-- returns the full help documentation associated with the entry -- returns the full help documentation associated with the entry, optionally
function get_entry_long_help(entry) -- wrapped to the specified width (80 if not specified).
return get_db_property(entry, 'long_help') function get_entry_long_help(entry, width)
return rewrap(get_db_property(entry, 'long_help'), width or 80)
end end
-- returns the set of tags associated with the entry -- returns the set of tags associated with the entry
@ -687,14 +719,6 @@ local function print_columns(col1text, col2text)
end end
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 -- prints the requested entries to the console. include and exclude filters are
-- defined as in search_entries() above. -- defined as in search_entries() above.
local function list_entries(skip_tags, include, exclude) 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'}) show_dev_commands and {} or {tag='dev'})
end 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 return _ENV

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

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

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

@ -31,6 +31,7 @@ distribution.
#include <set> #include <set>
using namespace std; using namespace std;
#include "modules/Renderer.h"
#include "modules/Screen.h" #include "modules/Screen.h"
#include "modules/GuiHooks.h" #include "modules/GuiHooks.h"
#include "MemAccess.h" #include "MemAccess.h"
@ -75,12 +76,14 @@ using std::string;
* Screen painting API. * Screen painting API.
*/ */
// returns text grid coordinates, even if the game map is scaled differently
df::coord2d Screen::getMousePos() 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(-1, -1);
}
return df::coord2d(gps->mouse_x, gps->mouse_y); return df::coord2d(x, y);
} }
df::coord2d Screen::getWindowSize() 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) { if (enabler->mouse_lbut_down) {
lua_pushboolean(L, true); 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(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(nestboxes nestboxes.cpp)
dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static)
dfhack_plugin(overlay overlay.cpp)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)
dfhack_plugin(petcapRemover petcapRemover.cpp) dfhack_plugin(petcapRemover petcapRemover.cpp)
dfhack_plugin(plants plants.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 count = 0;
int estimated_yield = get_log_count(); 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) if (skipped)
{ {

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

@ -424,26 +424,30 @@ static bool is_smooth_wall(MapExtras::MapCache &map, const DFCoord &pos) {
&& tileShape(tt) == df::tiletype_shape::WALL; && tileShape(tt) == df::tiletype_shape::WALL;
} }
static bool is_smooth_wall_or_door(MapExtras::MapCache &map, static bool is_connector(MapExtras::MapCache &map, const DFCoord &pos) {
const DFCoord &pos) {
if (is_smooth_wall(map, pos))
return true;
df::building *bld = Buildings::findAtTile(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 // adds adjacent smooth walls and doors to the given tdir
static TileDirection get_adjacent_smooth_walls(MapExtras::MapCache &map, static TileDirection get_adjacent_smooth_walls(MapExtras::MapCache &map,
const DFCoord &pos, const DFCoord &pos,
TileDirection tdir) { 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; 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; 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; 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; tdir.east = 1;
return tdir; return tdir;
} }
@ -469,7 +473,7 @@ static bool adjust_smooth_wall_dir(MapExtras::MapCache &map,
const DFCoord &pos, const DFCoord &pos,
TileDirection tdir = BLANK_TILE_DIRECTION) { TileDirection tdir = BLANK_TILE_DIRECTION) {
if (!is_smooth_wall(map, pos)) if (!is_smooth_wall(map, pos))
return false; return is_connector(map, pos);
tdir = ensure_valid_tdir(get_adjacent_smooth_walls(map, pos, tdir)); 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( commands.push_back(PluginCommand(
"infiniteSky", "infiniteSky",
"Creates new sky levels on request, or as needed.", "Create new sky levels on request, or as needed.",
infiniteSky)); infiniteSky));
return CR_OK; return CR_OK;
} }

@ -20,6 +20,7 @@
#include "uicommon.h" #include "uicommon.h"
#include "TileTypes.h" #include "TileTypes.h"
#include "DataFuncs.h" #include "DataFuncs.h"
#include "Debug.h"
DFHACK_PLUGIN("mousequery"); DFHACK_PLUGIN("mousequery");
REQUIRE_GLOBAL(enabler); REQUIRE_GLOBAL(enabler);
@ -32,6 +33,10 @@ using namespace df::enums::ui_sidebar_mode;
#define PLUGIN_VERSION 0.18 #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_clicked_x, last_clicked_y, last_clicked_z;
static int32_t last_pos_x, last_pos_y, last_pos_z; static int32_t last_pos_x, last_pos_y, last_pos_z;
static df::coord last_move_pos; 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) static df::coord get_mouse_pos(int32_t &mx, int32_t &my)
{ {
df::coord pos; df::coord pos = Gui::getMousePos();
pos.x = -30000; pos.z -= Gui::getDepthAt(pos.x, pos.y);
if (!enabler->tracking_on) df::coord vpos = Gui::getViewportPos();
return pos; mx = pos.x - vpos.x + 1;
my = pos.y - vpos.y + 1;
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);
return pos; return pos;
} }
@ -536,6 +531,7 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
if (mpos.x == x && mpos.y == y && mpos.z == z) if (mpos.x == x && mpos.y == y && mpos.z == z)
return; return;
DEBUG(log).print("moving cursor to %d, %d, %d\n", x, y, z);
Gui::setCursorCoords(mpos.x, mpos.y, mpos.z); Gui::setCursorCoords(mpos.x, mpos.y, mpos.z);
Gui::refreshSidebar(); Gui::refreshSidebar();
} }
@ -575,8 +571,6 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
int32_t mx, my; int32_t mx, my;
auto mpos = get_mouse_pos(mx, my); auto mpos = get_mouse_pos(mx, my);
bool mpos_valid = mpos.x != -30000 && mpos.y != -30000 && mpos.z != -30000; 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 // Check if in lever binding mode
if (Gui::getFocusString(Core::getTopViewscreen()) == 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; 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 end
local o = fs{} 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 end
function test.correct_frame_body_with_few_text_lines() function test.correct_frame_body_with_few_text_lines()
@ -50,10 +50,10 @@ function test.correct_frame_body_with_few_text_lines()
end end
local o = fs{} 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 end
function test.correct_frame_body_without_show_scroll_icons() function test.correct_frame_body_without_show_scrollbar()
local t = {} local t = {}
for i = 1, 12 do for i = 1, 12 do
t[#t+1] = tostring(i) t[#t+1] = tostring(i)
@ -66,13 +66,13 @@ function test.correct_frame_body_without_show_scroll_icons()
view_id = 'text', view_id = 'text',
frame_inset = 0, frame_inset = 0,
text = t, text = t,
show_scroll_icons = false, show_scrollbar = false,
}, },
} }
end end
local o = fs{} 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 end
function test.scroll() function test.scroll()

@ -398,8 +398,25 @@ function test.get_entry_short_help()
end end
function test.get_entry_long_help() 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 -- 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'], expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
h.get_entry_long_help('hascommands')) h.get_entry_long_help('hascommands'))
expect.eq(files['hack/docs/docs/tools/hascommands.txt'], expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
@ -605,6 +622,20 @@ function test.tags()
end) end)
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() function test.ls()
local mock_print = mock.func() local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function() mock.patch(h, 'print', mock_print, function()

@ -54,3 +54,62 @@ function test.invert_overwrite()
expect.eq(i.b, 2) expect.eq(i.b, 2)
expect.eq(i.a, 3) expect.eq(i.a, 3)
end 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