Merge branch 'develop' into spectate

develop
Josh Cooper 2022-10-12 14:40:56 -07:00
commit 8d8ef023fd
76 changed files with 1037 additions and 643 deletions

@ -20,7 +20,7 @@ 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.18.2 rev: 0.18.3
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

@ -0,0 +1,22 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-20.04
tools:
python: "3"
submodules:
include: all
sphinx:
configuration: conf.py
formats: all
python:
install:
- requirements: .readthedocs.requirements.txt

@ -192,7 +192,7 @@ endif()
# set up versioning. # set up versioning.
set(DF_VERSION "0.47.05") set(DF_VERSION "0.47.05")
set(DFHACK_RELEASE "r6") set(DFHACK_RELEASE "r7")
set(DFHACK_PRERELEASE FALSE) set(DFHACK_PRERELEASE FALSE)
set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}")

@ -58,9 +58,6 @@ def doc_dir(dirname, files, prefix):
def doc_all_dirs(): def doc_all_dirs():
"""Collect the commands and paths to include in our docs.""" """Collect the commands and paths to include in our docs."""
tools = [] tools = []
# TODO: as we scan the docs, parse out the tags and short descriptions and
# build a map for use in generating the tags pages and links in the tool
# doc footers
for root, _, files in os.walk('docs/builtins'): for root, _, files in os.walk('docs/builtins'):
tools.extend(doc_dir(root, files, os.path.relpath(root, 'docs/builtins'))) tools.extend(doc_dir(root, files, os.path.relpath(root, 'docs/builtins')))
for root, _, files in os.walk('docs/plugins'): for root, _, files in os.walk('docs/plugins'):
@ -69,76 +66,30 @@ def doc_all_dirs():
tools.extend(doc_dir(root, files, os.path.relpath(root, 'scripts/docs'))) tools.extend(doc_dir(root, files, os.path.relpath(root, 'scripts/docs')))
return tuple(tools) return tuple(tools)
DOC_ALL_DIRS = doc_all_dirs()
def get_tags():
groups = {}
group_re = re.compile(r'"([^"]+)"')
tag_re = re.compile(r'- `tag/([^`]+)`: (.*)')
with open('docs/Tags.rst') as f:
lines = f.readlines()
for line in lines:
line = line.strip()
m = re.match(group_re, line)
if m:
group = m.group(1)
groups[group] = []
continue
m = re.match(tag_re, line)
if m:
tag = m.group(1)
desc = m.group(2)
groups[group].append((tag, desc))
return groups
def generate_tag_indices():
os.makedirs('docs/tags', mode=0o755, exist_ok=True)
tag_groups = get_tags()
for tag_group in tag_groups:
with write_file_if_changed(('docs/tags/by{group}.rst').format(group=tag_group)) as topidx:
for tag_tuple in tag_groups[tag_group]:
tag = tag_tuple[0]
with write_file_if_changed(('docs/tags/{name}.rst').format(name=tag)) as tagidx:
tagidx.write('TODO: add links to the tools that have this tag')
topidx.write(('.. _tag/{name}:\n\n').format(name=tag))
topidx.write(('{name}\n').format(name=tag))
topidx.write(('{underline}\n').format(underline='*'*len(tag)))
topidx.write(('{desc}\n\n').format(desc=tag_tuple[1]))
topidx.write(('.. include:: /docs/tags/{name}.rst\n\n').format(name=tag))
def write_tool_docs(): def write_tool_docs():
""" """
Creates a file for each tool with the ".. include::" directives to pull in Creates a file for each tool with the ".. include::" directives to pull in
the original documentation. Then we generate a label and useful info in the the original documentation.
footer.
""" """
for k in DOC_ALL_DIRS: for k in doc_all_dirs():
header = ':orphan:\n'
label = ('.. _{name}:\n\n').format(name=k[0]) label = ('.. _{name}:\n\n').format(name=k[0])
include = ('.. include:: /{path}\n\n').format(path=k[1]) include = ('.. include:: /{path}\n\n').format(path=k[1])
# TODO: generate a footer with links to tools that share at least one
# tag with this tool. Just the tool names, strung across the bottom of
# the page in one long wrapped line, similar to how the wiki does it
os.makedirs(os.path.join('docs/tools', os.path.dirname(k[0])), os.makedirs(os.path.join('docs/tools', os.path.dirname(k[0])),
mode=0o755, exist_ok=True) mode=0o755, exist_ok=True)
with write_file_if_changed('docs/tools/{}.rst'.format(k[0])) as outfile: with write_file_if_changed('docs/tools/{}.rst'.format(k[0])) as outfile:
outfile.write(header)
if k[0] != 'search': if k[0] != 'search':
outfile.write(label) outfile.write(label)
outfile.write(include) outfile.write(include)
# Actually call the docs generator and run test
write_tool_docs() write_tool_docs()
generate_tag_indices()
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = '1.8' needs_sphinx = '3.4.3'
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -315,11 +266,9 @@ html_sidebars = {
] ]
} }
# If false, no module index is generated. # generate domain indices but not the (unused) genindex
html_domain_indices = False html_use_index = False
html_domain_indices = True
# If false, no genindex.html is generated.
html_use_index = True
# don't link to rst sources in the generated pages # don't link to rst sources in the generated pages
html_show_sourcelink = False html_show_sourcelink = False

@ -1,6 +1,7 @@
# This dfhack config file automates common tasks for your forts. # This dfhack config file automates common tasks for your forts.
# It was written for the Dreamfort set of quickfort blueprints, but the # It was written for the Dreamfort set of quickfort blueprints, but the
# configuration here is useful for any fort! Feed free to edit or override # configuration here is useful for any fort! Copy this file to your
# dfhack-config/init directory to use. Feed free to edit or override
# to your liking. # to your liking.
# Uncomment this next line if you want buildingplan (and quickfort) to use only # Uncomment this next line if you want buildingplan (and quickfort) to use only
@ -48,13 +49,7 @@ seedwatch all 30
# ensures important tasks get assigned to workers. # ensures important tasks get assigned to workers.
# otherwise these job types can get ignored in busy forts. # otherwise these job types can get ignored in busy forts.
prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel PullLever prioritize -aq defaults
prioritize -a StoreItemInLocation StoreItemInHospital
prioritize -a DestroyBuilding RemoveConstruction RecoverWounded DumpItem
prioritize -a CleanSelf SlaughterAnimal PrepareRawFish ExtractFromRawFish
prioritize -a TradeAtDepot BringItemToDepot CleanTrap ManageWorkOrders
prioritize -a --haul-labor=Food,Body StoreItemInStockpile
prioritize -a --reaction-name=TAN_A_HIDE CustomReaction
# autobutcher settings are saved in the savegame, so we only need to set them once. # autobutcher settings are saved in the savegame, so we only need to set them once.
# this way, any custom settings you set during gameplay are not overwritten # this way, any custom settings you set during gameplay are not overwritten
@ -78,7 +73,7 @@ on-new-fortress autobutcher target 50 50 14 2 BIRD_GOOSE
on-new-fortress autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA on-new-fortress autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA
# pigs give milk and meat and are zero-maintenance. # pigs give milk and meat and are zero-maintenance.
on-new-fortress autobutcher target 5 5 6 2 PIG on-new-fortress autobutcher target 5 5 6 2 PIG
# butcher all unprofitable animals # immediately butcher all unprofitable animals
on-new-fortress autobutcher target 0 0 0 0 HORSE YAK DONKEY WATER_BUFFALO GOAT CAVY BIRD_DUCK BIRD_GUINEAFOWL on-new-fortress autobutcher target 0 0 0 0 HORSE YAK DONKEY WATER_BUFFALO GOAT CAVY BIRD_DUCK BIRD_GUINEAFOWL
# watch for new animals # watch for new animals
on-new-fortress autobutcher autowatch on-new-fortress autobutcher autowatch

@ -289,7 +289,7 @@ DF, which causes DF to use your system libstdc++ instead::
rm libs/libstdc++.so.6 rm libs/libstdc++.so.6
Note that distributing binaries compiled with newer GCC versions may result in Note that distributing binaries compiled with newer GCC versions may result in
the opposite compatibily issue: users with *older* GCC versions may encounter the opposite compatibility issue: users with *older* GCC versions may encounter
similar errors. This is why DFHack distributes both GCC 4.8 and GCC 7 builds. If similar errors. This is why DFHack distributes both GCC 4.8 and GCC 7 builds. If
you are planning on distributing binaries to other users, we recommend using an you are planning on distributing binaries to other users, we recommend using an
older GCC (but still at least 4.8) version if possible. older GCC (but still at least 4.8) version if possible.
@ -314,7 +314,7 @@ Notes for GCC 8+ or OS X 10.10+ users
If none of these situations apply to you, skip to `osx-setup`. If none of these situations apply to you, skip to `osx-setup`.
If you have issues building on OS X 10.10 (Yosemite) or above, try definining If you have issues building on OS X 10.10 (Yosemite) or above, try defining
the following environment variable:: the following environment variable::
export MACOSX_DEPLOYMENT_TARGET=10.9 export MACOSX_DEPLOYMENT_TARGET=10.9
@ -503,7 +503,7 @@ in their name. If this redirect doesn't occur, just copy, paste, and enter the
download link again and you should see the options. You need to get: download link again and you should see the options. You need to get:
Visual C++ Build Tools for Visual Studio 2015 with Update 3. Visual C++ Build Tools for Visual Studio 2015 with Update 3.
Click the download button next to it and a dropdown of download formats will appear. Click the download button next to it and a dropdown of download formats will appear.
Select the DVD format to download an ISO file. When the donwload is complete, Select the DVD format to download an ISO file. When the download is complete,
click on the ISO file and a folder will popup with the following contents: click on the ISO file and a folder will popup with the following contents:
* packages (folder) * packages (folder)
@ -561,7 +561,7 @@ Additional dependencies: installing with the Chocolatey Package Manager
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The remainder of dependencies - Git, CMake, StrawberryPerl, and Python - can be The remainder of dependencies - Git, CMake, StrawberryPerl, and Python - can be
most easily installed using the Chocolatey Package Manger. Chocolatey is a most easily installed using the Chocolatey Package Manager. Chocolatey is a
\*nix-style package manager for Windows. It's fast, small (8-20MB on disk) \*nix-style package manager for Windows. It's fast, small (8-20MB on disk)
and very capable. Think "``apt-get`` for Windows." and very capable. Think "``apt-get`` for Windows."

@ -185,7 +185,7 @@ where ``*`` can be any string, including the empty string.
A world being loaded can mean a fortress, an adventurer, or legends mode. A world being loaded can mean a fortress, an adventurer, or legends mode.
These files are best used for non-persistent commands, such as setting These files are best used for non-persistent commands, such as setting
a `tag/bugfix` script to run on `repeat`. a `bugfix-tag-index` script to run on `repeat`.
.. _onMapLoad.init: .. _onMapLoad.init:

@ -76,7 +76,7 @@ replaced with the corresponding title and linked: e.g. ```autolabor```
=> `autolabor`. Scripts and plugins have link targets that match their names => `autolabor`. Scripts and plugins have link targets that match their names
created for you automatically. created for you automatically.
If you want to link to a heading in your own page, you can specifiy it like this:: If you want to link to a heading in your own page, you can specify it like this::
`Heading text exactly as written`_ `Heading text exactly as written`_

@ -28,7 +28,7 @@ or for coexistence in a single DF install, even with incompatible components.
For developers, DFHack unites the various ways tools access DF memory and For developers, DFHack unites the various ways tools access DF memory and
allows easier development of new tools. As an open-source project under allows easier development of new tools. As an open-source project under
`various open-source licences <license>`, contributions are welcome. `various open-source licenses <license>`, contributions are welcome.
.. contents:: Contents .. contents:: Contents
@ -54,7 +54,7 @@ used by the DFHack console.
to be used this way. to be used this way.
* Commands can also run at startup via `init files <init-files>`, * Commands can also run at startup via `init files <init-files>`,
on in batches at other times with the `script` command. or in batches at other times with the `script` command.
* Finally, some commands are persistent once enabled, and will sit in the * Finally, some commands are persistent once enabled, and will sit in the
background managing or changing some aspect of the game if you `enable` them. background managing or changing some aspect of the game if you `enable` them.

@ -740,7 +740,7 @@ Functions:
* ``dfhack.matinfo.decode(type,index)`` * ``dfhack.matinfo.decode(type,index)``
Looks up material info for the given number pair; if not found, returs *nil*. Looks up material info for the given number pair; if not found, returns *nil*.
* ``....decode(matinfo)``, ``....decode(item)``, ``....decode(obj)`` * ``....decode(matinfo)``, ``....decode(item)``, ``....decode(obj)``
@ -1099,7 +1099,7 @@ Other
* ``dfhack.gui.getDepthAt(x, y)`` * ``dfhack.gui.getDepthAt(x, y)``
Returns the distance from the z-level of the tile at map coordinates (x, y) to Returns 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. the closest ground z-level below. Defaults to 0, unless overridden by plugins.
Job module Job module
---------- ----------
@ -1869,7 +1869,7 @@ Among them are:
- ``full_rectangle = true`` - ``full_rectangle = true``
For buildings like stockpiles or farm plots that can normally For buildings like stockpiles or farm plots that can normally
accomodate individual tile exclusion, forces an error if any accommodate individual tile exclusion, forces an error if any
tiles within the specified width*height are obstructed. tiles within the specified width*height are obstructed.
- ``items = { item, item ... }``, or ``filters = { {...}, {...}... }`` - ``items = { item, item ... }``, or ``filters = { {...}, {...}... }``
@ -2169,7 +2169,7 @@ Supported callbacks and fields are:
Called when keyboard or mouse events are available. Called when keyboard or mouse events are available.
If any keys are pressed, the keys argument is a table mapping them to *true*. If any keys are pressed, the keys argument is a table mapping them to *true*.
Note that this refers to logical keybingings computed from real keys via Note that this refers to logical keybindings computed from real keys via
options; if multiple interpretations exist, the table will contain multiple keys. options; if multiple interpretations exist, the table will contain multiple keys.
The table also may contain special keys: The table also may contain special keys:
@ -2494,7 +2494,7 @@ Core context specific functions:
unit of time used, and may be one of ``'frames'`` (raw FPS), unit of time used, and may be one of ``'frames'`` (raw FPS),
``'ticks'`` (unpaused FPS), ``'days'``, ``'months'``, ``'ticks'`` (unpaused FPS), ``'days'``, ``'months'``,
``'years'`` (in-game time). All timers other than ``'years'`` (in-game time). All timers other than
``'frames'`` are cancelled when the world is unloaded, ``'frames'`` are canceled when the world is unloaded,
and cannot be queued until it is loaded again. and cannot be queued until it is loaded again.
Returns the timer id, or *nil* if unsuccessful due to Returns the timer id, or *nil* if unsuccessful due to
world being unloaded. world being unloaded.
@ -2677,7 +2677,7 @@ environment by the mandatory init file dfhack.lua:
.. _lua-string: .. _lua-string:
String class extentions String class extensions
----------------------- -----------------------
DFHack extends Lua's basic string class to include a number of convenience DFHack extends Lua's basic string class to include a number of convenience
@ -2698,7 +2698,7 @@ functions. These are invoked just like standard string functions, e.g.::
* ``string:split([delimiter[, plain]])`` * ``string:split([delimiter[, plain]])``
Split a string by the given delimiter. If no delimiter is specified, space Split a string by the given delimiter. If no delimiter is specified, space
(``' '``) is used. The delimter is treated as a pattern unless a ``plain`` is (``' '``) is used. The delimiter is treated as a pattern unless a ``plain`` is
specified and set to ``true``. To treat multiple successive delimiter specified and set to ``true``. To treat multiple successive delimiter
characters as a single delimiter, e.g. to avoid getting empty string elements, characters as a single delimiter, e.g. to avoid getting empty string elements,
pass a pattern like ``' +'``. Be aware that passing patterns that match empty pass a pattern like ``' +'``. Be aware that passing patterns that match empty
@ -2975,10 +2975,10 @@ parameters.
(e.g. combining the previous two examples into ``-abcdparam``) (e.g. combining the previous two examples into ``-abcdparam``)
Long options focus on clarity. They are usually entire words, or several words Long options focus on clarity. They are usually entire words, or several words
combined with hypens (``-``) or underscores (``_``). If they take an argument, combined with hyphens (``-``) or underscores (``_``). If they take an
the argument can be separated from the option name by a space or an equals argument, the argument can be separated from the option name by a space or an
sign (``=``). For example, the following two commandlines are equivalent: equals sign (``=``). For example, the following two commandlines are
``yourscript --style pretty`` and ``yourscript --style=pretty``. equivalent: ``yourscript --style pretty`` and ``yourscript --style=pretty``.
Another reason to use long options is if they represent an esoteric parameter Another reason to use long options is if they represent an esoteric parameter
that you don't expect to be commonly used and that you don't want to "waste" a that you don't expect to be commonly used and that you don't want to "waste" a
@ -3150,8 +3150,8 @@ Each entry has several properties associated with it:
alphabetized by their last path component, with populated path components alphabetized by their last path component, with populated path components
coming before null path components (e.g. ``autobutcher`` will immediately coming before null path components (e.g. ``autobutcher`` will immediately
follow ``gui/autobutcher``). follow ``gui/autobutcher``).
The optional ``include`` and ``exclude`` filter params are maps with the The optional ``include`` and ``exclude`` filter params are maps (or lists of
following elements: maps) with the following elements:
:str: if a string, filters by the given substring. if a table of strings, :str: if a string, filters by the given substring. if a table of strings,
includes entry names that match any of the given substrings. includes entry names that match any of the given substrings.
@ -3160,6 +3160,13 @@ Each entry has several properties associated with it:
:entry_type: if a string, matches entries of the given type. if a table of :entry_type: if a string, matches entries of the given type. if a table of
strings, includes entries that match any of the given types. strings, includes entries that match any of the given types.
Elements in a map are ANDed together (e.g. if both ``str`` and ``tag`` are
specified, the match is on any of the ``str`` elements AND any of the ``tag``
elements).
If lists of filters are passed instead of a single map, the maps are ORed
(that is, the match succeeds if any of the filters match).
If ``include`` is ``nil`` or empty, then all entries are included. If If ``include`` is ``nil`` or empty, then all entries are included. If
``exclude`` is ``nil`` or empty, then no entries are filtered out. ``exclude`` is ``nil`` or empty, then no entries are filtered out.
@ -3172,7 +3179,7 @@ create profiler objects which can be used to profile and generate report.
* ``profiler.newProfiler([variant[, sampling_frequency]])`` * ``profiler.newProfiler([variant[, sampling_frequency]])``
Returns an profile object with ``variant`` either ``'time'`` or ``'call'``. Returns a profile object with ``variant`` either ``'time'`` or ``'call'``.
``'time'`` variant takes optional ``sampling_frequency`` parameter to select ``'time'`` variant takes optional ``sampling_frequency`` parameter to select
lua instruction counts between samples. Default is ``'time'`` variant with lua instruction counts between samples. Default is ``'time'`` variant with
``10*1000`` frequency. ``10*1000`` frequency.
@ -3257,7 +3264,7 @@ Implements a trivial single-inheritance class system.
The main difference is that attributes are processed as a separate The main difference is that attributes are processed as a separate
initialization step, before any ``init`` methods are called. They initialization step, before any ``init`` methods are called. They
also make the directy relation between instance fields and constructor also make the direct relation between instance fields and constructor
arguments more explicit. arguments more explicit.
* ``new_obj = Class{ foo = arg, bar = arg, ... }`` * ``new_obj = Class{ foo = arg, bar = arg, ... }``
@ -3267,8 +3274,8 @@ Implements a trivial single-inheritance class system.
1. An empty instance table is created, and its metatable set. 1. An empty instance table is created, and its metatable set.
2. The ``preinit`` methods are called via ``invoke_before`` (see below) 2. The ``preinit`` methods are called via ``invoke_before`` (see below)
with the table used as argument to the class. These methods are intended with the table used as the argument to the class. These methods are
for validating and tweaking that argument table. intended for validating and tweaking that argument table.
3. Declared ATTRS are initialized from the argument table or their default values. 3. Declared ATTRS are initialized from the argument table or their default values.
4. The ``init`` methods are called via ``invoke_after`` with the argument table. 4. The ``init`` methods are called via ``invoke_after`` with the argument table.
This is the main constructor method. This is the main constructor method.
@ -3339,7 +3346,7 @@ A module for reading custom tokens added to the raws by mods.
Where ``typeInstance`` is a unit, entity, item, job, projectile, building, plant, or interaction Where ``typeInstance`` is a unit, entity, item, job, projectile, building, plant, or interaction
instance. Gets ``typeDefinition`` and then returns the same as ``getToken(typeDefinition, token)``. instance. Gets ``typeDefinition`` and then returns the same as ``getToken(typeDefinition, token)``.
For units, it gets the token from the race or caste instead if appplicable. For plants growth items, For units, it gets the token from the race or caste instead if applicable. For plants growth items,
it gets the token from the plant or plant growth instead if applicable. For plants it does the same it gets the token from the plant or plant growth instead if applicable. For plants it does the same
but with growth number -1. but with growth number -1.
@ -3661,7 +3668,7 @@ It also always has the following fields:
These fields are computed by the layout process: These fields are computed by the layout process:
:frame_parent_rect: The ViewRect represeting the client area of the parent view. :frame_parent_rect: The ViewRect representing the client area of the parent view.
:frame_rect: The ``mkdims`` rect of the outer frame in parent-local coordinates. :frame_rect: The ``mkdims`` rect of the outer frame in parent-local coordinates.
:frame_body: The ViewRect representing the body part of the View's own frame. :frame_body: The ViewRect representing the body part of the View's own frame.
@ -3897,13 +3904,13 @@ Base of all the widgets. Inherits from View and has the following attributes:
:r: gap between the right edges of the frame and the parent. :r: gap between the right edges of the frame and the parent.
:b: gap between the bottom edges of the frame and the parent. :b: gap between the bottom edges of the frame and the parent.
:w: maximum width of the frame. :w: maximum width of the frame.
:h: maximum heigth of the frame. :h: maximum height of the frame.
:xalign: X alignment of the frame. :xalign: X alignment of the frame.
:yalign: Y alignment of the frame. :yalign: Y alignment of the frame.
First the ``l,t,r,b`` fields restrict the available area for First the ``l,t,r,b`` fields restrict the available area for
placing the frame. If ``w`` and ``h`` are not specified or placing the frame. If ``w`` and ``h`` are not specified or
larger then the computed area, it becomes the frame. Otherwise larger than the computed area, it becomes the frame. Otherwise
the smaller frame is placed within the are based on the the smaller frame is placed within the are based on the
``xalign/yalign`` fields. If the align hints are omitted, they ``xalign/yalign`` fields. If the align hints are omitted, they
are assumed to be 0, 1, or 0.5 based on which of the ``l/r/t/b`` are assumed to be 0, 1, or 0.5 based on which of the ``l/r/t/b``
@ -4025,6 +4032,52 @@ following keyboard hotkeys:
- Ctrl-Left/Right arrow: move the cursor one word to the left or right. - Ctrl-Left/Right arrow: move the cursor one word to the left or right.
- Alt-Left/Right arrow: move the cursor to the beginning/end of the text. - Alt-Left/Right arrow: move the cursor to the beginning/end of the text.
Scrollbar class
---------------
This Widget subclass implements mouse-interactive scrollbars whose bar sizes
represent the amount of content currently visible in an associated display
widget (like a `Label class`_ or a `List class`_). By default they are styled
like scrollbars used in the vanilla DF help screens, but they are configurable.
Scrollbars have the following attributes:
:fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN``.
:bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN``.
:on_scroll: A callback called when the scrollbar is scrolled. It will be called with a single string parameter with a value of "up_large", "down_large", "up_small", or "down_small".
The Scrollbar widget implements the following methods:
* ``scrollbar:update(top_elem, elems_per_page, num_elems)``
Updates the info about the widget that the scrollbar is paired with.
The ``top_elem`` param is the (one-based) index of the first visible element.
The ``elems_per_page`` param is the maximum number of elements that can be
shown at one time. The ``num_elems`` param is the total number of elements
that the paried widget can scroll through. The scrollbar will adjust its
scrollbar size and position accordingly.
Clicking on the arrows at the top or the bottom of a scrollbar will scroll an
associated widget by a small amount. Clicking on the unfilled portion of the
scrollbar above or below the filled area will scroll by a larger amount in that
direction. The amount of scrolling done in each case in determined by the
associated widget, and after scrolling is complete, the associated widget must
call ``scrollbar:update()`` with updated new display info.
You can click and drag the scrollbar to scroll to a specific spot, or you can
click and hold on the end arrows or in the unfilled portion of the scrollbar to
scroll multiple times, just like in a normal browser scrollbar. The speed of
scroll events when the mouse button is held down is controlled by two global
variables:
:``SCROLL_INITIAL_DELAY_MS``: The delay before the second scroll event.
:``SCROLL_DELAY_MS``: The delay between further scroll events.
The defaults are 300 and 20, respectively, but they can be overridden by the
user in their :file:`dfhack-config/init/dfhack.init` file, for example::
:lua require('gui.widgets').SCROLL_DELAY_MS = 100
Label class Label class
----------- -----------
@ -4045,17 +4098,6 @@ 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_scrollbar: Controls scrollbar display: ``false`` for no scrollbar, ``'right'`` or ``'left'`` for
icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``),
``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary
(if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``.
:scrollbar_fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN`` (the same as the native DF help screens).
:scrollbar_bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN`` (the same as the native DF help screens).
If the scrollbar is shown, it will react to mouse clicks on the scrollbar itself.
Clicking on the arrows at the top or the bottom will scroll by one line, and
clicking on the unfilled portion of the scrollbar will scroll by a half page in
that direction.
The text itself is represented as a complex structure, and passed 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
@ -4148,7 +4190,8 @@ The Label widget implements the following methods:
This method takes the number of lines to scroll as positive or negative This method takes the number of lines to scroll as positive or negative
integers or one of the following keywords: ``+page``, ``-page``, integers or one of the following keywords: ``+page``, ``-page``,
``+halfpage``, or ``-halfpage``. ``+halfpage``, or ``-halfpage``. It returns the number of lines that were
actually scrolled (negative for scrolling up).
WrappedLabel class WrappedLabel class
------------------ ------------------
@ -4276,7 +4319,6 @@ Every list item may be specified either as a string, or as a lua table
with the following fields: with the following fields:
:text: Specifies the label text in the same format as the Label text. :text: Specifies the label text in the same format as the Label text.
:caption, [1]: Deprecated legacy aliases for **text**.
:text_*: Reserved for internal use. :text_*: Reserved for internal use.
:key: Specifies a keybinding that acts as a shortcut for the specified item. :key: Specifies a keybinding that acts as a shortcut for the specified item.
:icon: Specifies an icon string, or a pen to paint a single character. May be a callback. :icon: Specifies an icon string, or a pen to paint a single character. May be a callback.
@ -4437,7 +4479,7 @@ Functions
.. note:: this is the only mandatory field. .. note:: this is the only mandatory field.
:fix_impassible: :fix_impassible:
if true make impassible tiles impassible to liquids too if true make impassable tiles impassable to liquids too
:consume: :consume:
how much machine power is needed to work. how much machine power is needed to work.
Disables reactions if not supplied enough and ``needs_power==1`` Disables reactions if not supplied enough and ``needs_power==1``
@ -4461,7 +4503,7 @@ Functions
:canBeRoomSubset: :canBeRoomSubset:
a flag if this building can be counted in room. 1 means it can, 0 means it can't and -1 default building behaviour a flag if this building can be counted in room. 1 means it can, 0 means it can't and -1 default building behaviour
:auto_gears: :auto_gears:
a flag that automatically fills up gears and animate. It looks over building definition for gear icons and maps them. a flag that automatically fills up gears and animations. It looks over the building definition for gear icons and maps them.
Animate table also might contain: Animate table also might contain:
@ -4472,7 +4514,7 @@ Functions
``getPower(building)`` returns two number - produced and consumed power if building can be modified and returns nothing otherwise ``getPower(building)`` returns two number - produced and consumed power if building can be modified and returns nothing otherwise
``setPower(building,produced,consumed)`` sets current productiona and consumption for a building. ``setPower(building,produced,consumed)`` sets current power production and consumption for a building.
Examples Examples
-------- --------
@ -4506,7 +4548,7 @@ Native functions provided by the `buildingplan` plugin:
* ``bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the buildingplan UI is enabled for the specified building type. * ``bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the buildingplan UI is enabled for the specified building type.
* ``bool isPlannedBuilding(df::building *bld)`` returns whether the given building is managed by buildingplan. * ``bool isPlannedBuilding(df::building *bld)`` returns whether the given building is managed by buildingplan.
* ``void addPlannedBuilding(df::building *bld)`` suspends the building jobs and adds the building to the monitor list. * ``void addPlannedBuilding(df::building *bld)`` suspends the building jobs and adds the building to the monitor list.
* ``void doCycle()`` runs a check for whether buildlings in the monitor list can be assigned items and unsuspended. This method runs automatically twice a game day, so you only need to call it directly if you want buildingplan to do a check right now. * ``void doCycle()`` runs a check for whether buildings in the monitor list can be assigned items and unsuspended. This method runs automatically twice a game day, so you only need to call it directly if you want buildingplan to do a check right now.
* ``void scheduleCycle()`` schedules a cycle to be run during the next non-paused game frame. Can be called multiple times while the game is paused and only one cycle will be scheduled. * ``void scheduleCycle()`` schedules a cycle to be run during the next non-paused game frame. Can be called multiple times while the game is paused and only one cycle will be scheduled.
burrows burrows
@ -4740,7 +4782,7 @@ List of events
1. ``onReactionCompleting(reaction,reaction_product,unit,input_items,input_reagents,output_items,call_native)`` 1. ``onReactionCompleting(reaction,reaction_product,unit,input_items,input_reagents,output_items,call_native)``
Is called once per reaction product, before reaction had a chance to call native code for item creation. Is called once per reaction product, before the reaction has a chance to call native code for item creation.
Setting ``call_native.value=false`` cancels further processing: no items are created and ``onReactionComplete`` is not called. Setting ``call_native.value=false`` cancels further processing: no items are created and ``onReactionComplete`` is not called.
2. ``onReactionComplete(reaction,reaction_product,unit,input_items,input_reagents,output_items)`` 2. ``onReactionComplete(reaction,reaction_product,unit,input_items,input_reagents,output_items)``
@ -4796,7 +4838,7 @@ These events are straight from EventManager module. Each of them first needs to
4. ``onJobCompleted(job)`` 4. ``onJobCompleted(job)``
Gets called when job is finished. The job that is passed to this function is a copy. Requires a frequency of 0 in order to distinguish between workshop jobs that were cancelled by the user and workshop jobs that completed successfully. Gets called when job is finished. The job that is passed to this function is a copy. Requires a frequency of 0 in order to distinguish between workshop jobs that were canceled by the user and workshop jobs that completed successfully.
5. ``onUnitDeath(unit_id)`` 5. ``onUnitDeath(unit_id)``
@ -4855,7 +4897,7 @@ Functions
5. ``registerSidebar(shop_name,callback)`` 5. ``registerSidebar(shop_name,callback)``
Enable callback when sidebar for ``shop_name`` is drawn. Usefull for custom workshop views e.g. using gui.dwarfmode lib. Also accepts a ``class`` instead of function Enable callback when sidebar for ``shop_name`` is drawn. Useful for custom workshop views e.g. using gui.dwarfmode lib. Also accepts a ``class`` instead of function
as callback. Best used with ``gui.dwarfmode`` class ``WorkshopOverlay``. as callback. Best used with ``gui.dwarfmode`` class ``WorkshopOverlay``.
Examples Examples
@ -4898,7 +4940,7 @@ luasocket
========= =========
A way to access csocket from lua. The usage is made similar to luasocket in vanilla lua distributions. Currently A way to access csocket from lua. The usage is made similar to luasocket in vanilla lua distributions. Currently
only subset of functions exist and only tcp mode is implemented. only a subset of the functions exist and only tcp mode is implemented.
.. contents:: .. contents::
:local: :local:
@ -4977,7 +5019,7 @@ Functions
- ``render_map_rect(x,y,z,w,h)`` - ``render_map_rect(x,y,z,w,h)``
returns a table with w*h*4 entries of rendered tiles. The format is same as ``df.global.gps.screen`` (tile,foreground,bright,background). returns a table with w*h*4 entries of rendered tiles. The format is the same as ``df.global.gps.screen`` (tile,foreground,bright,background).
.. _pathable-api: .. _pathable-api:
@ -5094,7 +5136,7 @@ Scripts
:local: :local:
Any files with the ``.lua`` extension placed into the :file:`hack/scripts` folder Any files with the ``.lua`` extension placed into the :file:`hack/scripts` folder
are automatically made avaiable as DFHack commands. The command corresponding to are automatically made available as DFHack commands. The command corresponding to
a script is simply the script's filename, relative to the scripts folder, with a script is simply the script's filename, relative to the scripts folder, with
the extension omitted. For example: the extension omitted. For example:

@ -63,7 +63,7 @@ are not built by default, but can be built by setting the ``BUILD_DEVEL``
Scripts Scripts
~~~~~~~ ~~~~~~~
Several `development tools <tag/dev>` can be useful for memory research. Several `development tools <dev-tag-index>` can be useful for memory research.
These include (but are not limited to): These include (but are not limited to):
- `devel/dump-offsets` - `devel/dump-offsets`

@ -22,6 +22,6 @@ See `dev-changelog` for a list of changes grouped by development releases.
Older Changelogs Older Changelogs
================ ================
Are kept in a seperate file: `History` Are kept in a separate file: `History`
.. that's ``docs/History.rst``, if you're reading the raw text. .. that's ``docs/History.rst``, if you're reading the raw text.

@ -18,7 +18,7 @@ layout changes, and will need to be recompiled for every new DF version.
Addresses of DF global objects and vtables are stored in a separate file, Addresses of DF global objects and vtables are stored in a separate file,
:file:`symbols.xml`. Since these are only absolute addresses, they do not need :file:`symbols.xml`. Since these are only absolute addresses, they do not need
to be compiled in to DFHack code, and are instead loaded at runtime. This makes to be compiled into DFHack code, and are instead loaded at runtime. This makes
fixes and additions to global addresses possible without recompiling DFHack. fixes and additions to global addresses possible without recompiling DFHack.
In an installed copy of DFHack, this file can be found at the root of the In an installed copy of DFHack, this file can be found at the root of the
``hack`` folder. ``hack`` folder.

@ -1,3 +0,0 @@
:orphan:
Please continue to the new `support` page.

@ -13,36 +13,36 @@ for the tag assignment spreadsheet.
"when" tags "when" tags
----------- -----------
- `tag/adventure`: Tools that are useful while in adventure mode. Note that some tools only tagged with "fort" might also work in adventure mode, but not always in expected ways. Feel free to experiment, though! - `adventure <adventure-tag-index>`: Tools that are useful while in adventure mode. Note that some tools only tagged with "fort" might also work in adventure mode, but not always in expected ways. Feel free to experiment, though!
- `tag/dfhack`: Tools that you use to run DFHack commands or interact with the DFHack library. This tag also includes tools that help you manage the DF game itself (e.g. settings, saving, etc.) - `dfhack <dfhack-tag-index>`: Tools that you use to run DFHack commands or interact with the DFHack library. This tag also includes tools that help you manage the DF game itself (e.g. settings, saving, etc.)
- `tag/embark`: Tools that are useful while on the fort embark screen or while creating an adventurer. - `embark <embark-tag-index>`: Tools that are useful while on the fort embark screen or while creating an adventurer.
- `tag/fort`: Tools that are useful while in fort mode. - `fort <fort-tag-index>`: Tools that are useful while in fort mode.
- `tag/legends`: Tools that are useful while in legends mode. - `legends <legends-tag-index>`: Tools that are useful while in legends mode.
"why" tags "why" tags
---------- ----------
- `tag/armok`: Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden. - `armok <armok-tag-index>`: Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.
- `tag/auto`: Tools that run in the background and automatically manage routine, toilsome aspects of your fortress. - `auto <auto-tag-index>`: Tools that run in the background and automatically manage routine, toilsome aspects of your fortress.
- `tag/bugfix`: Tools that fix specific bugs, either permanently or on-demand. - `bugfix <bugfix-tag-index>`: Tools that fix specific bugs, either permanently or on-demand.
- `tag/design`: Tools that help you design your fort. - `design <design-tag-index>`: Tools that help you design your fort.
- `tag/dev`: Tools that are useful when developing scripts or mods. - `dev <dev-tag-index>`: Tools that are useful when developing scripts or mods.
- `tag/fps`: Tools that help you manage FPS drop. - `fps <fps-tag-index>`: Tools that help you manage FPS drop.
- `tag/gameplay`: Tools that introduce new gameplay elements. - `gameplay <gameplay-tag-index>`: Tools that introduce new gameplay elements.
- `tag/inspection`: Tools that let you view information that is otherwise difficult to find. - `inspection <inspection-tag-index>`: Tools that let you view information that is otherwise difficult to find.
- `tag/productivity`: Tools that help you do things that you could do manually, but using the tool is better and faster. - `productivity <productivity-tag-index>`: Tools that help you do things that you could do manually, but using the tool is better and faster.
"what" tags "what" tags
----------- -----------
- `tag/animals`: Tools that interact with animals. - `animals <animals-tag-index>`: Tools that interact with animals.
- `tag/buildings`: Tools that interact with buildings and furniture. - `buildings <buildings-tag-index>`: Tools that interact with buildings and furniture.
- `tag/graphics`: Tools that interact with game graphics. - `graphics <graphics-tag-index>`: Tools that interact with game graphics.
- `tag/interface`: Tools that interact with or extend the DF user interface. - `interface <interface-tag-index>`: Tools that interact with or extend the DF user interface.
- `tag/items`: Tools that interact with in-game items. - `items <items-tag-index>`: Tools that interact with in-game items.
- `tag/jobs`: Tools that interact with jobs. - `jobs <jobs-tag-index>`: Tools that interact with jobs.
- `tag/labors`: Tools that deal with labor assignment. - `labors <labors-tag-index>`: Tools that deal with labor assignment.
- `tag/map`: Tools that interact with the game map. - `map <map-tag-index>`: Tools that interact with the game map.
- `tag/military`: Tools that interact with the military. - `military <military-tag-index>`: Tools that interact with the military.
- `tag/plants`: Tools that interact with trees, shrubs, and crops. - `plants <plants-tag-index>`: Tools that interact with trees, shrubs, and crops.
- `tag/stockpiles`: Tools that interact wtih stockpiles. - `stockpiles <stockpiles-tag-index>`: Tools that interact with stockpiles.
- `tag/units`: Tools that interact with units. - `units <units-tag-index>`: Tools that interact with units.
- `tag/workorders`: Tools that interact with workorders. - `workorders <workorders-tag-index>`: Tools that interact with workorders.

@ -30,9 +30,11 @@ guildhalls and temples that you have agreed to.
Finding the tool you need Finding the tool you need
------------------------- -------------------------
DFHack tools are tagged with categories to make them easier to find. Note that a DFHack tools are tagged with categories to make them easier to find. These
tool can belong to more than one category. If you'd like to see the full list of categories are listed in the next few sections. Note that a tool can belong to
tools in one flat list, please refer to the `index <genindex>`. more than one category. If you already know what you're looking for, try the
`search` or Ctrl-F on this page. If you'd like to see the full list of tools in
one flat list, please refer to the `annotated index <all-tag-index>`.
DFHack tools by game mode DFHack tools by game mode
------------------------- -------------------------
@ -48,3 +50,14 @@ DFHack tools by what they affect
-------------------------------- --------------------------------
.. include:: tags/bywhat.rst .. include:: tags/bywhat.rst
All DFHack tools alphabetically
-------------------------------
.. toctree::
:glob:
:maxdepth: 1
:titlesonly:
tools/*
tools/*/*

@ -40,3 +40,5 @@ Options
Don't print out the tags associated with each command. Don't print out the tags associated with each command.
``--dev`` ``--dev``
Include commands intended for developers and modders. Include commands intended for developers and modders.
``--exclude <string>[,<string>...]``
Exclude commands that match any of the given strings.

@ -33,12 +33,31 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
# Future # Future
## New Plugins
## Fixes
## Misc Improvements
- `ls`: indent tag listings and wrap them in the right column for better readability
- `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output.
- `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down
- UX: List widgets now have mouse-interactive scrollbars
- UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times.
- UX: You can now drag the scrollbar to scroll to a specific spot
## Documentation
## API
## Lua
- ``widgets.Scrollbar``: new scrollbar widget that can be paired with an associated scrollable widget. Integrated with ``widgets.Label`` and ``widgets.List``.
# 0.47.05-r7
## 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. - `overlay`: display a "DFHack" button in the lower left corner that you can click to start the new GUI command launcher. The `dwarfmonitor` weather display had to be moved to make room for the button. If you are seeing the weather indicator rendered over the overlay button, please remove the ``dfhack-config/dwarfmonitor.json`` file to fix the weather indicator display offset.
## New Tweaks
## New Internal Commands ## New Internal Commands
- `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`.
@ -49,6 +68,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `dig-now`: Fix direction of smoothed walls when adjacent to a door or floodgate - `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 - `quickfort`: `Dreamfort <quickfort-blueprint-guide>` blueprint set: declare the hospital zone before building the coffer; otherwise DF fails to stock the hospital with materials
- ``dfhack.buildings.findCivzonesAt``: no longer return duplicate civzones after loading a save with existing civzones
## 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.
@ -86,6 +106,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- Removed ``Windows`` module (C++-only) - unused. - Removed ``Windows`` module (C++-only) - unused.
- ``Constructions`` module (C++-only): removed ``t_construction``, ``isValid()``, ``getCount()``, ``getConstruction()``, and ``copyConstruction()``. Access ``world.constructions`` directly instead. - ``Constructions`` module (C++-only): removed ``t_construction``, ``isValid()``, ``getCount()``, ``getConstruction()``, and ``copyConstruction()``. Access ``world.constructions`` directly instead.
- ``Gui::getSelectedItem()``, ``Gui::getAnyItem()``: added support for the artifacts screen - ``Gui::getSelectedItem()``, ``Gui::getAnyItem()``: added support for the artifacts screen
- ``Units::teleport()``: now sets ``unit.idle_area`` to discourage units from walking back to their original location (or teleporting back, if using `fastdwarf`)
## 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.

@ -129,7 +129,7 @@ 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 ``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. 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 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 check once a tick. Be careful not to do this gratuitously, though, since
running that often can slow down the game! running that often can slow down the game!
``eventful``, on the other hand, is much more performance-friendly since it will ``eventful``, on the other hand, is much more performance-friendly since it will
@ -176,10 +176,10 @@ you can react to with ``eventful``.
Now, you may have noticed that you won't be able to register multiple callbacks 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 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 functions you want from a single registered callback. Alternately, you can
multiple callbacks using different keys, using your mod ID as a key name prefix. create multiple callbacks using different keys, using your mod ID as a key name
If you do register multiple callbacks, though, there are no guarantees about the prefix. If you do register multiple callbacks, though, there are no guarantees
call order. about the call order.
Custom raw tokens Custom raw tokens
----------------- -----------------
@ -348,6 +348,9 @@ timer::
The structure of a full mod The structure of a full mod
--------------------------- ---------------------------
For reference, `Tachy Guns <https://www.github.com/wolfboyft/tachy-guns>`__ is a
full mod that conforms to this guide.
Create a folder for mod projects somewhere outside your Dwarf Fortress Create a folder for mod projects somewhere outside your Dwarf Fortress
installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the
names for the mod folders within it. In the example below, we'll use a mod ID of names for the mod folders within it. In the example below, we'll use a mod ID of
@ -425,13 +428,18 @@ Ok, you're all set up! Now, let's take a look at an example
moduleA.onLoad() moduleA.onLoad()
moduleB.onLoad() moduleB.onLoad()
-- register your callbacks -- multiple functions in the same repeat callback
repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', function()
moduleA.every1Tick) moduleA.every1Tick()
moduleB.every1Tick()
end)
-- one function per repeat callback (you can put them in the
-- above format if you prefer)
repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames',
moduleD.every100Frames) moduleD.every100Frames)
-- multiple functions in the same callback -- multiple functions in the same eventful callback
eventful.onReactionComplete[modId] = function(reaction, eventful.onReactionComplete[modId] = function(reaction,
reaction_product, unit, input_items, input_reagents, reaction_product, unit, input_items, input_reagents,
output_items) output_items)
@ -442,7 +450,7 @@ Ok, you're all set up! Now, let's take a look at an example
unit, input_items, input_reagents, output_items) unit, input_items, input_reagents, output_items)
end end
-- one function per callback (you can put them in the -- one function per eventful callback (you can put them in the
-- above format if you prefer) -- above format if you prefer)
eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement
eventful.onProjUnitCheckMovement[modId] = moduleD.onProjUnitCheckMovement eventful.onProjUnitCheckMovement[modId] = moduleD.onProjUnitCheckMovement

@ -80,7 +80,7 @@ sequence, potentially with other aliases. If the alias is the only text in the
cell, the alias name is matched and its expansion is used. If the alias has cell, the alias name is matched and its expansion is used. If the alias has
other keys before or after it, the alias name must be surrounded in curly other keys before or after it, the alias name must be surrounded in curly
brackets (:kbd:`{` and :kbd:`}`). An alias can be surrounded in curly brackets brackets (:kbd:`{` and :kbd:`}`). An alias can be surrounded in curly brackets
even if it is the only text in the cell, it just isn't necesary. For example, even if it is the only text in the cell, it just isn't necessary. For example,
the following blueprint uses the ``aliasname`` alias by itself in the first the following blueprint uses the ``aliasname`` alias by itself in the first
two rows and uses it as part of a longer sequence in the third row:: two rows and uses it as part of a longer sequence in the third row::
@ -454,7 +454,7 @@ be used for either the ``quantum_enable`` or ``route_enable`` sub-aliases.
Experienced Dwarf Fortress players may be wondering how the same aliases can Experienced Dwarf Fortress players may be wondering how the same aliases can
work in both contexts since the keys for entering the configuration screen work in both contexts since the keys for entering the configuration screen
differ. Fear not! There is some sub-alias magic at work here. If you define differ. Fear not! There is some sub-alias magic at work here. If you define
your own stockpile configuraiton aliases, you can use the magic yourself by your own stockpile configuration aliases, you can use the magic yourself by
building your aliases on the ``*prefix`` aliases described later in this building your aliases on the ``*prefix`` aliases described later in this
guide. guide.
@ -652,7 +652,7 @@ sheetprefix enablesheet disablesheet
Then, for each item category, there are aliases that manipulate interesting Then, for each item category, there are aliases that manipulate interesting
subsets of that category: subsets of that category:
* Exclusive aliases forbid everthing within a category and then enable only * Exclusive aliases forbid everything within a category and then enable only
the named item type (or named class of items) the named item type (or named class of items)
* ``forbid*`` aliases forbid the named type and leave the rest of the * ``forbid*`` aliases forbid the named type and leave the rest of the
stockpile untouched. stockpile untouched.

@ -198,7 +198,7 @@ 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
a light aquifer. See the step-by-step guide, including informaton on how to a light aquifer. See the step-by-step guide, including information on how to
create a drainage system so your dwarves don't drown when digging the tap, by create a drainage system so your dwarves don't drown when digging the tap, by
running ``quickfort run library/aquifer_tap.csv -n /help``. running ``quickfort run library/aquifer_tap.csv -n /help``.

@ -190,8 +190,8 @@ dug-out area::
Cw Cw Cw # Cw Cw Cw #
# # # # # # # # # #
Note my generosity -- in addition to the bed (:kbd:`b`) I've built a chest Note my generosity -- in addition to the bed (:kbd:`b`) I've built a container
(:kbd:`c`) here for the dwarf as well. You must use the full series of keys (:kbd:`h`) here for the dwarf as well. You must use the full series of keys
needed to build something in each cell, e.g. :kbd:`C`:kbd:`w` indicates we needed to build something in each cell, e.g. :kbd:`C`:kbd:`w` indicates we
should enter DF's constructions submenu (:kbd:`C`) and select walls (:kbd:`w`). should enter DF's constructions submenu (:kbd:`C`) and select walls (:kbd:`w`).
@ -245,7 +245,7 @@ If there weren't an alias named ``booze`` then the literal characters
spell those aliases correctly! spell those aliases correctly!
You can save a lot of time and effort by using aliases instead of adding all You can save a lot of time and effort by using aliases instead of adding all
key seqences directly to your blueprints. For more details, check out the key sequences directly to your blueprints. For more details, check out the
`quickfort-alias-guide`. You can also see examples of aliases being used in the `quickfort-alias-guide`. You can also see examples of aliases being used in the
query blueprints in the query blueprints in the
:source:`DFHack blueprint library <data/blueprints/library>`. You can create :source:`DFHack blueprint library <data/blueprints/library>`. You can create
@ -683,7 +683,7 @@ three vertical tiles like this::
` end here ` # ` end here ` #
# # # # # # # #
Then to carve the cross, you'd do a horizonal segment:: Then to carve the cross, you'd do a horizontal segment::
` ` ` # ` ` ` #
start here ` end here # start here ` end here #
@ -740,7 +740,7 @@ Or you could use the aliases to specify tile by tile::
# # # # # # # #
The aliases can also be used to designate a solid block of track. This is The aliases can also be used to designate a solid block of track. This is
epecially useful for obliterating low-quality engravings so you can re-smooth especially useful for obliterating low-quality engravings so you can re-smooth
and re-engrave with higher quality. For example, you could use the following and re-engrave with higher quality. For example, you could use the following
sequence of blueprints to ensure a 10x10 floor area contains only masterwork sequence of blueprints to ensure a 10x10 floor area contains only masterwork
engravings:: engravings::
@ -1157,7 +1157,7 @@ blueprint::
"#meta label(help) message(This is the help text for the blueprint set "#meta label(help) message(This is the help text for the blueprint set
contained in this file. contained in this file.
First, make sure that you embark in...) blueprint set walkthough" First, make sure that you embark in...) blueprint set walkthrough"
could more naturally be written as a ``#notes`` blueprint:: could more naturally be written as a ``#notes`` blueprint::
@ -1739,7 +1739,7 @@ priorities <quickfort-dig-priorities>`.
Use dig priorities to control ramp creation. Use dig priorities to control ramp creation.
We can `ensure <https://docs.google.com/spreadsheets/d/1IBy6_pGEe6WSBCLukDz_5I-4vi_mpHuJJyOp2j6SJlY/edit#gid=962076234>`__ We can `ensure <https://docs.google.com/spreadsheets/d/1IBy6_pGEe6WSBCLukDz_5I-4vi_mpHuJJyOp2j6SJlY/edit#gid=962076234>`__
the bottom level is carved out before the layer above is channelled by assigning the bottom level is carved out before the layer above is channeled by assigning
the channel designations lower priorities (the ``h5``\s in the third layer -- the channel designations lower priorities (the ``h5``\s in the third layer --
scroll down). scroll down).

@ -25,10 +25,10 @@ Example
3dveins 3dveins
New veins are generated using 3D Perlin noise in order to produce a layout that New veins are generated using natural-looking 3D Perlin noise in order to
flows smoothly between z-levels. The vein distribution is based on the world produce a layout that flows smoothly between z-levels. The vein distribution is
seed, so running the command for the second time should produce no change. It is based on the world seed, so running the command for the second time should
best to run it just once immediately after embark. produce no change. It is best to run it just once immediately after embark.
This command is intended as only a cosmetic change, so it takes care to exactly This command is intended as only a cosmetic change, so it takes care to exactly
preserve the mineral counts reported by ``prospect all``. The amounts of layer preserve the mineral counts reported by ``prospect all``. The amounts of layer

@ -12,3 +12,6 @@ names starting with ``SPATTER_ADD_``, so there are no commands to run to use it.
These reactions will then produce contaminants on items instead of improvements. These reactions will then produce contaminants on items instead of improvements.
The contaminants are immune to being washed away by water or destroyed by The contaminants are immune to being washed away by water or destroyed by
`clean`. `clean`.
You must have a mod installed that adds the appropriate tokens in order for this
plugin to do anything.

@ -6,7 +6,7 @@ autobutcher
:tags: fort auto fps animals :tags: fort auto fps animals
This plugin monitors how many pets you have of each gender and age and assigns This plugin monitors how many pets you have of each gender and age and assigns
excess lifestock for slaughter. It requires that you add the target race(s) to a excess livestock for slaughter. It requires that you add the target race(s) to a
watch list. Units will be ignored if they are: watch list. Units will be ignored if they are:
* Untamed * Untamed
@ -44,7 +44,7 @@ Usage
- fa = number of female adults - fa = number of female adults
- ma = number of female adults - ma = number of female adults
If you specify ``all``, then this command will set the counts for all races If you specify ``all``, then this command will set the counts for all races
on your current watchlist (including the races which are currenly set to on your current watchlist (including the races which are currently set to
'unwatched') and sets the new default for future watch commands. If you 'unwatched') and sets the new default for future watch commands. If you
specify ``new``, then this command just sets the new default counts for specify ``new``, then this command just sets the new default counts for
future watch commands without changing your current watchlist. Otherwise, future watch commands without changing your current watchlist. Otherwise,

@ -5,7 +5,7 @@ autohauler
:summary: Automatically manage hauling labors. :summary: Automatically manage hauling labors.
:tags: fort auto labors :tags: fort auto labors
Similar to `autolabor`, but instead of managing all labors, ``autohauler`` only Similar to `autolabor`, but instead of managing all labors, autohauler only
addresses hauling labors, leaving the assignment of skilled labors entirely up addresses hauling labors, leaving the assignment of skilled labors entirely up
to you. You can use the in-game `manipulator` UI or an external tool like Dwarf to you. You can use the in-game `manipulator` UI or an external tool like Dwarf
Therapist to do so. Therapist to do so.
@ -19,7 +19,7 @@ assignment, with most skilled labors only being assigned to just a few dwarves
and almost every non-military dwarf having at least one skilled labor assigned. and almost every non-military dwarf having at least one skilled labor assigned.
Autohauler allows a skill to be used as a flag to exempt a dwarf from Autohauler allows a skill to be used as a flag to exempt a dwarf from
``autohauler``'s effects. By default, this is the unused ALCHEMIST labor, but it autohauler's effects. By default, this is the unused ALCHEMIST labor, but it
can be changed by the user. can be changed by the user.
Usage Usage

@ -11,7 +11,7 @@ dwarves to specialize in specific skills.
Autolabor frequently checks how many jobs of each type are available and sets Autolabor frequently checks how many jobs of each type are available and sets
labors proportionally in order to get them all done quickly. Labors with labors proportionally in order to get them all done quickly. Labors with
equipment -- mining, hunting, and woodcutting -- which are abandoned if labors equipment -- mining, hunting, and woodcutting -- which are abandoned if labors
change mid-job, are handled slightly differently to minimise churn. change mid-job, are handled slightly differently to minimize churn.
Dwarves on active military duty or dwarves assigned to burrows are left Dwarves on active military duty or dwarves assigned to burrows are left
untouched by autolabor. untouched by autolabor.
@ -39,16 +39,16 @@ and filling ponds. Other jobs are automatically assigned as described above.
Each of these settings can be adjusted. Each of these settings can be adjusted.
Jobs are rarely assigned to nobles with responsibilities for meeting diplomats Jobs are rarely assigned to nobles with responsibilities for meeting diplomats
or merchants, never to the chief medical dwarf, and less often to the bookeeper or merchants, never to the chief medical dwarf, and less often to the bookkeeper
and manager. and manager.
Hunting is never assigned without a butchery, and fishing is never assigned Hunting is never assigned without a butchery, and fishing is never assigned
without a fishery. without a fishery.
For each labor, a preference order is calculated based on skill, biased against For each labor, a preference order is calculated based on skill, excluding those
masters of other trades and excluding those who can't do the job. The labor is who can't do the job. Dwarves who are masters of a skill are deprioritized for
then added to the best <minimum> dwarves for that labor, then to additional other skills. The labor is then added to the best <minimum> dwarves for that
dwarfs that meet any of these conditions: labor, then to additional dwarfs that meet any of these conditions:
* The dwarf is idle and there are no idle dwarves assigned to this labor * The dwarf is idle and there are no idle dwarves assigned to this labor
* The dwarf has non-zero skill associated with the labor * The dwarf has non-zero skill associated with the labor
@ -62,7 +62,7 @@ Examples
``autolabor MINE 5`` ``autolabor MINE 5``
Keep at least 5 dwarves with mining enabled. Keep at least 5 dwarves with mining enabled.
``autolabor CUT_GEM 1 1`` ``autolabor CUT_GEM 1 1``
Keep exactly 1 dwarf with gemcutting enabled. Keep exactly 1 dwarf with gem cutting enabled.
``autolabor COOK 1 1 3`` ``autolabor COOK 1 1 3``
Keep 1 dwarf with cooking enabled, selected only from the top 3. Keep 1 dwarf with cooking enabled, selected only from the top 3.
``autolabor FEED_WATER_CIVILIANS haulers`` ``autolabor FEED_WATER_CIVILIANS haulers``

@ -48,6 +48,6 @@ construction menu after selecting materials, it returns you back to this screen.
If you use this along with several autoselect enabled materials, you should be If you use this along with several autoselect enabled materials, you should be
able to place complex constructions more conveniently. able to place complex constructions more conveniently.
The ``automaterial`` plugin also enables extra contruction placement modes, such The ``automaterial`` plugin also enables extra construction placement modes,
as designating areas larger than 10x10 and allowing you to designate hollow such as designating areas larger than 10x10 and allowing you to designate hollow
rectangles instead of the default filled ones. rectangles instead of the default filled ones.

@ -5,13 +5,13 @@ autonestbox
:summary: Auto-assign egg-laying female pets to nestbox zones. :summary: Auto-assign egg-laying female pets to nestbox zones.
:tags: fort auto animals :tags: fort auto animals
To use this feature, you must create pen/pasture zones above nestboxes. If the To use this feature, you must create pen/pasture zones on the same tiles as
pen is bigger than 1x1, the nestbox must be in the top left corner. Only 1 unit built nestboxes. If the pen is bigger than 1x1, the nestbox must be in the top
will be assigned per pen, regardless of the size. Egg layers who are also left corner. Only 1 unit will be assigned per pen, regardless of the size. Egg
grazers will be ignored, since confining them to a 1x1 pasture is not a good layers who are also grazers will be ignored, since confining them to a 1x1
idea. Only tame and domesticated own units are processed since pasturing pasture is not a good idea. Only tame and domesticated own units are processed
half-trained wild egg layers could destroy your neat nestbox zones when they since pasturing half-trained wild egg layers could destroy your neat nestbox
revert to wild. zones when they revert to wild.
Note that the age of the units is not checked, so you might get some egg-laying Note that the age of the units is not checked, so you might get some egg-laying
kids assigned to the nestbox zones. Most birds grow up quite fast, though, so kids assigned to the nestbox zones. Most birds grow up quite fast, though, so

@ -103,6 +103,10 @@ Options
to surround the parameter string in double quotes: to surround the parameter string in double quotes:
``"-s10,10,central stairs"`` or ``--playback-start "10,10,central stairs"`` ``"-s10,10,central stairs"`` or ``--playback-start "10,10,central stairs"``
or ``"--playback-start=10,10,central stairs"``. or ``"--playback-start=10,10,central stairs"``.
``--smooth``
Record all smooth tiles in the ``smooth`` phase. If this parameter is not
specified, only tiles that will later be carved into fortifications or
engraved will be smoothed.
``-t``, ``--splitby <strategy>`` ``-t``, ``--splitby <strategy>``
Split blueprints into multiple files. See the `Splitting output into Split blueprints into multiple files. See the `Splitting output into
multiple files`_ section below for details. If not specified, defaults to multiple files`_ section below for details. If not specified, defaults to

@ -34,7 +34,7 @@ Usage
``burrow remove-units target-burrow <burrow> [<burrow> ...]`` ``burrow remove-units target-burrow <burrow> [<burrow> ...]``
Remove units in source burrows from the target. Remove units in source burrows from the target.
``burrow set-tiles target-burrow <burrow> [<burrow> ...]`` ``burrow set-tiles target-burrow <burrow> [<burrow> ...]``
Clear target burrow tiles and adds tiles from the names source burrows. Clear target burrow tiles and add tiles from the names source burrows.
``burrow add-tiles target-burrow <burrow> [<burrow> ...]`` ``burrow add-tiles target-burrow <burrow> [<burrow> ...]``
Add tiles from the source burrows to the target. Add tiles from the source burrows to the target.
``burrow remove-tiles target-burrow <burrow> [<burrow> ...]`` ``burrow remove-tiles target-burrow <burrow> [<burrow> ...]``

@ -35,7 +35,7 @@ Examples
``changelayer GRANITE`` ``changelayer GRANITE``
Convert the layer at the cursor position into granite. Convert the layer at the cursor position into granite.
``changelayer SILTY_CLAY force`` ``changelayer SILTY_CLAY force``
Convert teh layer at the cursor position into clay, even if it's stone. Convert the layer at the cursor position into clay, even if it's stone.
``changelayer MARBLE all_biomes all_layers`` ``changelayer MARBLE all_biomes all_layers``
Convert all layers of all biomes which are not soil into marble. Convert all layers of all biomes which are not soil into marble.

@ -5,7 +5,7 @@ changevein
:summary: Change the material of a mineral inclusion. :summary: Change the material of a mineral inclusion.
:tags: fort armok map :tags: fort armok map
You can change a vein to any incorganic material RAW id. Note that this command You can change a vein to any inorganic material RAW id. Note that this command
only affects tiles within the current 16x16 block - for large veins and only affects tiles within the current 16x16 block - for large veins and
clusters, you will need to use this command multiple times. clusters, you will need to use this command multiple times.

@ -13,7 +13,7 @@ Usage
command-prompt [entry] command-prompt [entry]
If called with parameters, it starts with that text in the command edit area. If called with parameters, it starts with that text in the command edit area.
This is most useful for developers, who can set a keybinding to open a laungage This is most useful for developers, who can set a keybinding to open a language
interpreter for lua or Ruby by starting with the `:lua <lua>` or `:rb <rb>` interpreter for lua or Ruby by starting with the `:lua <lua>` or `:rb <rb>`
portions of the command already filled in. portions of the command already filled in.

@ -33,7 +33,7 @@ Examples
- ``cursecheck detail all`` - ``cursecheck detail all``
Give detailed info about all cursed creatures including deceased ones. Give detailed info about all cursed creatures including deceased ones.
- ``cursecheck nick`` - ``cursecheck nick``
Give a nickname all living/active cursed creatures. Give a nickname to all living/active cursed creatures.
.. note:: .. note::

@ -42,13 +42,13 @@ Usage
----- -----
``debugfilter category [<plugin regex>] [<category regex>]`` ``debugfilter category [<plugin regex>] [<category regex>]``
List available debug plugin and category names. If filters aren't givenm List available debug plugin and category names. If filters aren't given
then all plugins/categories are matched. This command is a good way to test then all plugins/categories are matched. This command is a good way to test
regex parameters before you pass them to ``set``. regex parameters before you pass them to ``set``.
``debugfilter filter [<id>]`` ``debugfilter filter [<id>]``
List active and passive debug print level changes. The optional ``id`` List active and passive debug print level changes. The optional ``id``
parameter is the id listed as first column in the filter list. If ``id`` is parameter is the id listed as the first column in the filter list. If ``id``
given, then the command shows extended information for the given filter is given, then the command shows extended information for the given filter
only. only.
``debugfilter set [<level>] [<plugin regex>] [<category regex>]`` ``debugfilter set [<level>] [<plugin regex>] [<category regex>]``
Create a new debug filter to set category verbosity levels. This filter Create a new debug filter to set category verbosity levels. This filter
@ -61,7 +61,7 @@ Usage
``debugfilter disable <id> [<id> ...]`` ``debugfilter disable <id> [<id> ...]``
Disable a space separated list of filters but keep it in the filter list. Disable a space separated list of filters but keep it in the filter list.
``debugfilter enable <id> [<id> ...]`` ``debugfilter enable <id> [<id> ...]``
Enable a space sperate list of filters. Enable a space separated list of filters.
``debugfilter header [enable] | [disable] [<element> ...]`` ``debugfilter header [enable] | [disable] [<element> ...]``
Control which header metadata is shown along with each log message. Run it Control which header metadata is shown along with each log message. Run it
without parameters to see the list of configurable elements. Include an without parameters to see the list of configurable elements. Include an

@ -25,7 +25,7 @@ dig
:summary: Designate circles. :summary: Designate circles.
.. dfhack-command:: digtype .. dfhack-command:: digtype
:summary: Designate all vein tiles of the selected type. :summary: Designate all vein tiles of the same type as the selected tile.
.. dfhack-command:: digexp .. dfhack-command:: digexp
:summary: Designate dig patterns for exploratory mining. :summary: Designate dig patterns for exploratory mining.
@ -50,9 +50,9 @@ Usage
Designate circles. The diameter is the number of tiles across the center of Designate circles. The diameter is the number of tiles across the center of
the circle that you want to dig. See the `digcircle`_ section below for the circle that you want to dig. See the `digcircle`_ section below for
options. options.
``digtype [<designation>] [-p<number>]`` ``digtype [<designation>] [-p<number>] [-z]``
Designate all vein tiles of the selected type. See the `digtype`_ section Designate all vein tiles of the same type as the selected tile. See the
below for options. `digtype`_ section below for options.
``digexp [<pattern>] [<filter>] [-p<number>]`` ``digexp [<pattern>] [<filter>] [-p<number>]``
Designate dig patterns for exploratory mining. See the `digexp`_ section Designate dig patterns for exploratory mining. See the `digexp`_ section
below for options. below for options.
@ -143,6 +143,10 @@ Designation options:
``clear`` ``clear``
Clear any designations. Clear any designations.
You can also pass a ``-z`` option, which restricts designations to the current
z-level and down. This is useful when you don't want to designate tiles on the
same z-levels as your carefully dug fort above.
digexp digexp
------ ------

@ -87,7 +87,7 @@ Some widgets support additional options:
* ``cursor`` widget: * ``cursor`` widget:
* ``format``: Specifies the format. ``X``, ``x``, ``Y``, and ``y`` are * ``format``: Specifies the format. ``X``, ``x``, ``Y``, and ``y`` are
replaced with the corresponding cursor cordinates, while all other replaced with the corresponding cursor coordinates, while all other
characters are unmodified. characters are unmodified.
* ``show_invalid``: If set to ``true``, the mouse coordinates will both be * ``show_invalid``: If set to ``true``, the mouse coordinates will both be
displayed as ``-1`` when the cursor is outside of the DF window; otherwise, displayed as ``-1`` when the cursor is outside of the DF window; otherwise,

@ -5,12 +5,12 @@ dwarfvet
:summary: Allows animals to be treated at animal hospitals. :summary: Allows animals to be treated at animal hospitals.
:tags: fort gameplay animals :tags: fort gameplay animals
Annoyed your dragons become useless after a minor injury? Well, with dwarfvet, Annoyed that your dragons become useless after a minor injury? Well, with
injured animals will be treated at an animal hospital, which is simply a hospital dwarfvet, injured animals will be treated at an animal hospital, which is simply
that is also an animal training zone. Dwarfs with the Animal Caretaker labor a hospital that is also an animal training zone. Dwarfs with the Animal
enabled will come to the hospital to treat animals. Normal medical skills are Caretaker labor enabled will come to the hospital to treat animals. Normal
used (and trained), but no experience is given to the Animal Caretaker skill medical skills are used (and trained), but no experience is given to the Animal
itself. Caretaker skill itself.
Usage Usage
----- -----

@ -14,7 +14,7 @@ Usage
embark-tools enable|disable all embark-tools enable|disable all
embark-tools enable|disable <tool> [<tool> ...] embark-tools enable|disable <tool> [<tool> ...]
Avaliable tools are: Available tools are:
``anywhere`` ``anywhere``
Allows embarking anywhere (including sites, mountain-only biomes, and Allows embarking anywhere (including sites, mountain-only biomes, and

@ -18,7 +18,7 @@ Usage
fix-unit-occupancy interval <num_ticks> fix-unit-occupancy interval <num_ticks>
When run without arguments (or with just the ``here`` or ``-n`` parameters), When run without arguments (or with just the ``here`` or ``-n`` parameters),
the fix just runs once. You can also have it run periodically by enbling the the fix just runs once. You can also have it run periodically by enabling the
plugin. plugin.
Examples Examples
@ -35,7 +35,7 @@ Options
``here`` ``here``
Only operate on the tile at the cursor. Only operate on the tile at the cursor.
``-n`` ``-n``
Report issues, but do not any write changes to the map. Report issues, but do not write any changes to the map.
``interval <num_ticks>`` ``interval <num_ticks>``
Set how often the plugin will check for and fix issues when it is enabled. Set how often the plugin will check for and fix issues when it is enabled.
The default is 1200 ticks, or 1 game day. The default is 1200 ticks, or 1 game day.

@ -64,7 +64,7 @@ Examples
``forceequip v bp QQQ`` ``forceequip v bp QQQ``
List the bodyparts of the selected unit. List the bodyparts of the selected unit.
``forceequip bp LH`` ``forceequip bp LH``
Equips an appopriate item onto the unit's left hand. Equips an appropriate item onto the unit's left hand.
``forceequip m bp LH`` ``forceequip m bp LH``
Equips ALL appropriate items onto the unit's left hand. The unit may end up Equips ALL appropriate items onto the unit's left hand. The unit may end up
wearing a dozen left-handed mittens. Use with caution, and remember that wearing a dozen left-handed mittens. Use with caution, and remember that

@ -37,7 +37,7 @@ explicitly disable it, even if you save and reload your game.
The default priorities for each labor vary (some labors are higher priority by The default priorities for each labor vary (some labors are higher priority by
default than others). The way the plugin works is that, once it determines how default than others). The way the plugin works is that, once it determines how
many jobs of each labor is needed, it then sorts them by adjusted priority. many jobs of each labor are needed, it then sorts them by adjusted priority.
(Labors other than hauling have a bias added to them based on how long it's been (Labors other than hauling have a bias added to them based on how long it's been
since they were last used to prevent job starvation.) The labor with the highest since they were last used to prevent job starvation.) The labor with the highest
priority is selected, the "best fit" dwarf for that labor is assigned to that priority is selected, the "best fit" dwarf for that labor is assigned to that
@ -112,7 +112,7 @@ Advanced usage
``labormanager priority <labor> <value>`` ``labormanager priority <labor> <value>``
Set the priority value for labor <labor> to <value>. Set the priority value for labor <labor> to <value>.
``labormanager max <labor> <value>`` ``labormanager max <labor> <value>``
Set maximum number of dwarves that can be assigned to a labor. Set the maximum number of dwarves that can be assigned to a labor.
``labormanager max <labor> none`` ``labormanager max <labor> none``
Unrestrict the number of dwarves that can be assigned to a labor. Unrestrict the number of dwarves that can be assigned to a labor.
``labormanager max <labor> disable`` ``labormanager max <labor> disable``

@ -11,7 +11,7 @@ Usage
----- -----
``lair`` ``lair``
Mark the map as monster lair. Mark the map as a monster lair.
``lair reset`` ``lair reset``
Mark the map as ordinary (not lair). Mark the map as ordinary (not lair).

@ -102,7 +102,7 @@ and how many you are likely to need in a mature fort. These are just
approximations. Your playstyle may demand more or fewer of each profession. approximations. Your playstyle may demand more or fewer of each profession.
- ``Chef`` (needed: 0, 3) - ``Chef`` (needed: 0, 3)
Buchery, Tanning, and Cooking. It is important to focus just a few dwarves Butchery, Tanning, and Cooking. It is important to focus just a few dwarves
on cooking since well-crafted meals make dwarves very happy. They are also on cooking since well-crafted meals make dwarves very happy. They are also
an excellent trade good. an excellent trade good.
- ``Craftsdwarf`` (needed: 0, 4-6) - ``Craftsdwarf`` (needed: 0, 4-6)

@ -2,7 +2,7 @@ map-render
========== ==========
.. dfhack-tool:: .. dfhack-tool::
:summary: Provides a Lua API for rerendering portions of the map. :summary: Provides a Lua API for re-rendering portions of the map.
:tags: dev graphics :tags: dev graphics
:no-command: :no-command:

@ -2,7 +2,7 @@ power-meter
=========== ===========
.. dfhack-tool:: .. dfhack-tool::
:summary: Allow presure plates to measure power. :summary: Allow pressure plates to measure power.
:tags: fort gameplay buildings :tags: fort gameplay buildings
:no-command: :no-command:

@ -2,7 +2,7 @@ regrass
======= =======
.. dfhack-tool:: .. dfhack-tool::
:summary: Regrows all the grass. :summary: Regrow all the grass.
:tags: adventure fort armok animals map :tags: adventure fort armok animals map
Use this command if your grazers have eaten everything down to the dirt. Use this command if your grazers have eaten everything down to the dirt.

@ -12,7 +12,7 @@ Usage
----- -----
``rendermax light`` ``rendermax light``
Light the map tiles realisitically. Outside tiles are light during the day Light the map tiles realistically. Outside tiles are light during the day
and dark at night. Inside tiles are always dark unless a nearby unit is and dark at night. Inside tiles are always dark unless a nearby unit is
lighting it up, as if they were carrying torches. lighting it up, as if they were carrying torches.
``rendermax light sun <hour>|cycle`` ``rendermax light sun <hour>|cycle``

@ -12,7 +12,7 @@ resume
When enabled, this plugin will display a colored 'X' over suspended buildings. When enabled, this plugin will display a colored 'X' over suspended buildings.
When run as a command, it can resume all suspended building jobs, allowing you When run as a command, it can resume all suspended building jobs, allowing you
to quickly recover if a bunch of jobs were suspended due to the workers getting to quickly recover if a bunch of jobs were suspended due to the workers getting
scared off by wildlife or items temporarily blocking buildling sites. scared off by wildlife or items temporarily blocking building sites.
Usage Usage
----- -----

@ -7,19 +7,19 @@ reveal
:summary: Reveals the map. :summary: Reveals the map.
:tags: adventure fort armok inspection map :tags: adventure fort armok inspection map
.. dfhack-tool:: unreveal .. dfhack-command:: unreveal
:summary: Hides previously hidden tiles again. :summary: Hides previously hidden tiles again.
.. dfhack-tool:: revforget .. dfhack-command:: revforget
:summary: Discard records about what was visible before revealing the map. :summary: Discard records about what was visible before revealing the map.
.. dfhack-tool:: revtoggle .. dfhack-command:: revtoggle
:summary: Switch between reveal and unreveal. :summary: Switch between reveal and unreveal.
.. dfhack-tool:: revflood .. dfhack-command:: revflood
:summary: Hide everything, then reveal tiles with a path to the cursor. :summary: Hide everything, then reveal tiles with a path to the cursor.
.. dfhack-tool:: nopause .. dfhack-command:: nopause
:summary: Disable pausing. :summary: Disable pausing.
This reveals all z-layers in fort mode. It also works in adventure mode, but any This reveals all z-layers in fort mode. It also works in adventure mode, but any

@ -9,7 +9,7 @@ search
:no-command: :no-command:
Search options are added to the Stocks, Animals, Trading, Stockpile, Noble Search options are added to the Stocks, Animals, Trading, Stockpile, Noble
aassignment candidates), Military (position candidates), Burrows (unit list), assignment candidates), Military (position candidates), Burrows (unit list),
Rooms, Announcements, Job List, and Unit List screens all get hotkeys that allow Rooms, Announcements, Job List, and Unit List screens all get hotkeys that allow
you to dynamically filter the displayed lists. you to dynamically filter the displayed lists.

@ -21,7 +21,7 @@ Usage
Start managing seed and plant cooking. By default, no types are watched. Start managing seed and plant cooking. By default, no types are watched.
You have to add them with further ``seedwatch`` commands. You have to add them with further ``seedwatch`` commands.
``seedwatch <type> <target>`` ``seedwatch <type> <target>``
Adds the specifiied type to the watchlist (if it's not already there) and Adds the specified type to the watchlist (if it's not already there) and
sets the target number of seeds to the specified number. You can pass the sets the target number of seeds to the specified number. You can pass the
keyword ``all`` instead of a specific type to set the target for all types. keyword ``all`` instead of a specific type to set the target for all types.
``seedwatch <type>`` ``seedwatch <type>``

@ -21,4 +21,4 @@ Usage
stocks show stocks show
Running ``stocks show`` will bring you to the fortress-wide stock management Running ``stocks show`` will bring you to the fortress-wide stock management
screen from whereever you are. screen from wherever you are.

@ -58,7 +58,7 @@ Examples
``tiletypes-command filter material STONE ; f shape WALL ; paint shape FLOOR`` ``tiletypes-command filter material STONE ; f shape WALL ; paint shape FLOOR``
Turn all stone walls into floors, preserving the material. Turn all stone walls into floors, preserving the material.
``tiletypes-command p any ; p s wall ; p sp normal`` ``tiletypes-command p any ; p s wall ; p sp normal``
Clear the paint specificaiton and set it to unsmoothed walls. Clear the paint specification and set it to unsmoothed walls.
``tiletypes-command f any ; p stone marble ; p sh wall ; p sp normal ; r 10 10`` ``tiletypes-command f any ; p stone marble ; p sh wall ; p sp normal ; r 10 10``
Prepare to paint a 10x10 area of marble walls, ready for harvesting for Prepare to paint a 10x10 area of marble walls, ready for harvesting for
flux. flux.

@ -2,7 +2,7 @@ tubefill
======== ========
.. dfhack-tool:: .. dfhack-tool::
:summary: Replentishes mined-out adamantine. :summary: Replenishes mined-out adamantine.
:tags: fort armok map :tags: fort armok map
Veins that were originally hollow will be left alone. Veins that were originally hollow will be left alone.

@ -66,7 +66,7 @@ Commands that persist until disabled or DF quits:
``civ-view-agreement`` ``civ-view-agreement``
Fixes overlapping text on the "view agreement" screen. Fixes overlapping text on the "view agreement" screen.
``condition-material`` ``condition-material``
Fixes a crash in the work order contition material list (:bug:`9905`). Fixes a crash in the work order condition material list (:bug:`9905`).
``craft-age-wear`` ``craft-age-wear``
Fixes crafted items not wearing out over time (:bug:`6003`). With this Fixes crafted items not wearing out over time (:bug:`6003`). With this
tweak, items made from cloth and leather will gain a level of wear every 20 tweak, items made from cloth and leather will gain a level of wear every 20

@ -122,7 +122,7 @@ The constraint spec consists of 4 parts, separated with ``/`` characters::
ITEM[:SUBTYPE]/[GENERIC_MAT,...]/[SPECIFIC_MAT:...]/[LOCAL,<quality>] ITEM[:SUBTYPE]/[GENERIC_MAT,...]/[SPECIFIC_MAT:...]/[LOCAL,<quality>]
The first part is mandatory and specifies the item type and subtype, using the The first part is mandatory and specifies the item type and subtype, using the
raw tokens for items (the same syntax used custom reaction inputs). For more raw tokens for items (the same syntax used for custom reaction inputs). For more
information, see :wiki:`this wiki page <Material_token>`. information, see :wiki:`this wiki page <Material_token>`.
The subsequent parts are optional: The subsequent parts are optional:

@ -74,8 +74,8 @@ Filters
:all: Process all units. :all: Process all units.
:count <n>: Process only up to n units. :count <n>: Process only up to n units.
:unassigned: Not assigned to zone, chain or built cage. :unassigned: Not assigned to zone, chain or built cage.
:minage <years>: Minimum age. Must be followed by number. :minage <years>: Minimum age. Must be followed by a number.
:maxage <years>: Maximum age. Must be followed by number. :maxage <years>: Maximum age. Must be followed by a number.
:not: Negates the next filter keyword. All of the keywords documented :not: Negates the next filter keyword. All of the keywords documented
below are negatable. below are negatable.
:race: Must be followed by a race RAW ID (e.g. BIRD_TURKEY, ALPACA, :race: Must be followed by a race RAW ID (e.g. BIRD_TURKEY, ALPACA,

@ -3,15 +3,21 @@
# https://www.sphinx-doc.org/en/master/development/tutorials/recipe.html # https://www.sphinx-doc.org/en/master/development/tutorials/recipe.html
# https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-directives # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-directives
from collections import defaultdict
import logging import logging
import os import os
from typing import List, Optional, Type import re
from typing import Dict, Iterable, List, Optional, Tuple, Type
import docutils.nodes as nodes import docutils.nodes as nodes
from docutils.nodes import Node
import docutils.parsers.rst.directives as rst_directives import docutils.parsers.rst.directives as rst_directives
import sphinx import sphinx
import sphinx.addnodes as addnodes import sphinx.addnodes as addnodes
import sphinx.directives import sphinx.directives
from sphinx.domains import Domain, Index, IndexEntry
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import process_index_entry
import dfhack.util import dfhack.util
@ -46,7 +52,6 @@ def make_summary(builder: sphinx.builders.Builder, summary: str) -> nodes.paragr
para += nodes.inline(text=summary) para += nodes.inline(text=summary)
return para return para
_KEYBINDS = {} _KEYBINDS = {}
_KEYBINDS_RENDERED = set() # commands whose keybindings have been rendered _KEYBINDS_RENDERED = set() # commands whose keybindings have been rendered
@ -105,20 +110,39 @@ def check_missing_keybinds():
logger.warning('Undocumented keybindings for command: %s', missing_command) logger.warning('Undocumented keybindings for command: %s', missing_command)
_anchor_pattern = re.compile(r'^\d+')
def to_anchor(name: str) -> str:
name = name.lower()
name = name.replace('/', '-')
name = re.sub(_anchor_pattern, '', name)
return name
class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription): class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription):
has_content = False has_content = False
required_arguments = 0 required_arguments = 0
optional_arguments = 1 optional_arguments = 1
def get_tool_name_from_docname(self):
parts = self.env.docname.split('/')
if 'tools' in parts:
return '/'.join(parts[parts.index('tools') + 1:])
else:
return parts[-1]
def get_name_or_docname(self): def get_name_or_docname(self):
if self.arguments: if self.arguments:
return self.arguments[0] return self.arguments[0]
else: return self.get_tool_name_from_docname()
parts = self.env.docname.split('/')
if 'tools' in parts: def add_index_entries(self, name) -> None:
return '/'.join(parts[parts.index('tools') + 1:]) docname = self.env.docname
else: anchor = to_anchor(self.get_tool_name_from_docname())
return parts[-1] tags = self.env.domaindata['tag-repo']['doctags'][docname]
indexdata = (name, self.options.get('summary', ''), '', docname, anchor, 0)
self.env.domaindata['all']['objects'].append(indexdata)
for tag in tags:
self.env.domaindata[tag]['objects'].append(indexdata)
@staticmethod @staticmethod
def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition: def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition:
@ -145,13 +169,15 @@ class DFHackToolDirective(DFHackToolDirectiveBase):
def render_content(self) -> List[nodes.Node]: def render_content(self) -> List[nodes.Node]:
tag_paragraph = self.make_labeled_paragraph('Tags') tag_paragraph = self.make_labeled_paragraph('Tags')
for tag in self.options.get('tags', []): tags = self.options.get('tags', [])
self.env.domaindata['tag-repo']['doctags'][self.env.docname] = tags
for tag in tags:
tag_paragraph += [ tag_paragraph += [
addnodes.pending_xref(tag, nodes.inline(text=tag), **{ addnodes.pending_xref(tag, nodes.inline(text=tag), **{
'reftype': 'ref', 'reftype': 'ref',
'refdomain': 'std', 'refdomain': 'std',
'reftarget': 'tag/' + tag, 'reftarget': tag + '-tag-index',
'refexplicit': False, 'refexplicit': True,
'refwarn': True, 'refwarn': True,
}), }),
nodes.inline(text=' | '), nodes.inline(text=' | '),
@ -160,6 +186,7 @@ class DFHackToolDirective(DFHackToolDirectiveBase):
ret_nodes = [tag_paragraph] ret_nodes = [tag_paragraph]
if 'no-command' in self.options: if 'no-command' in self.options:
self.add_index_entries(self.get_name_or_docname() + ' (plugin)')
ret_nodes += [make_summary(self.env.app.builder, self.options.get('summary', ''))] ret_nodes += [make_summary(self.env.app.builder, self.options.get('summary', ''))]
return ret_nodes return ret_nodes
@ -177,6 +204,7 @@ class DFHackCommandDirective(DFHackToolDirectiveBase):
def render_content(self) -> List[nodes.Node]: def render_content(self) -> List[nodes.Node]:
command = self.get_name_or_docname() command = self.get_name_or_docname()
self.add_index_entries(command)
return [ return [
self.make_labeled_paragraph('Command', command, content_class=nodes.literal), self.make_labeled_paragraph('Command', command, content_class=nodes.literal),
make_summary(self.env.app.builder, self.options.get('summary', '')), make_summary(self.env.app.builder, self.options.get('summary', '')),
@ -184,16 +212,114 @@ class DFHackCommandDirective(DFHackToolDirectiveBase):
] ]
class TagRepoDomain(Domain):
name = 'tag-repo'
label = 'Holds tag associations per document'
initial_data = {'doctags': {}}
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
self.data['doctags'].update(otherdata['doctags'])
def get_tags():
groups = {}
group_re = re.compile(r'"([^"]+)"')
tag_re = re.compile(r'- `([^ ]+) <[^>]+>`: (.*)')
with open(os.path.join(dfhack.util.DOCS_ROOT, 'Tags.rst')) as f:
lines = f.readlines()
for line in lines:
line = line.strip()
m = re.match(group_re, line)
if m:
group = m.group(1)
groups[group] = []
continue
m = re.match(tag_re, line)
if m:
tag = m.group(1)
desc = m.group(2)
groups[group].append((tag, desc))
return groups
def tag_domain_get_objects(self):
for obj in self.data['objects']:
yield(obj)
def tag_domain_merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
seen = set()
objs = self.data['objects']
for obj in objs:
seen.add(obj[0])
for obj in otherdata['objects']:
if obj[0] not in seen:
objs.append(obj)
objs.sort()
def tag_index_generate(self, docnames: Optional[Iterable[str]] = None) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
content = defaultdict(list)
for name, desc, _, docname, anchor, _ in self.domain.data['objects']:
first_letter = name[0].lower()
extra, descr = desc, ''
if self.domain.env.app.builder.format == 'html':
extra, descr = '', desc
content[first_letter].append(
IndexEntry(name, 0, docname, anchor, extra, '', descr))
return (sorted(content.items()), False)
def register_index(app, tag, title):
domain_class = type(tag+'Domain', (Domain, ), {
'name': tag,
'label': 'Container domain for tag: ' + tag,
'initial_data': {'objects': []},
'merge_domaindata': tag_domain_merge_domaindata,
'get_objects': tag_domain_get_objects,
})
index_class = type(tag+'Index', (Index, ), {
'name': 'tag-index',
'localname': title,
'shortname': tag,
'generate': tag_index_generate,
})
app.add_domain(domain_class)
app.add_index_to_domain(tag, index_class)
def init_tag_indices(app):
os.makedirs(os.path.join(dfhack.util.DOCS_ROOT, 'tags'), mode=0o755, exist_ok=True)
tag_groups = get_tags()
for tag_group in tag_groups:
group_file_path = os.path.join(dfhack.util.DOCS_ROOT, 'tags', 'by{group}.rst'.format(group=tag_group))
with dfhack.util.write_file_if_changed(group_file_path) as topidx:
for tag_tuple in tag_groups[tag_group]:
tag, desc = tag_tuple[0], tag_tuple[1]
topidx.write(('- `{name} <{name}-tag-index>`\n').format(name=tag))
topidx.write((' {desc}\n').format(desc=desc))
register_index(app, tag, desc)
def update_index_titles(app):
for domain in app.env.domains.values():
for index in domain.indices:
if index.shortname == 'all':
continue
if app.builder.format == 'html':
index.localname = '"%s" tag index<h4>%s</h4>' % (index.shortname, index.localname)
else:
index.localname = '"%s" tag index - %s' % (index.shortname, index.localname)
def register(app): def register(app):
app.add_directive('dfhack-tool', DFHackToolDirective) app.add_directive('dfhack-tool', DFHackToolDirective)
app.add_directive('dfhack-command', DFHackCommandDirective) app.add_directive('dfhack-command', DFHackCommandDirective)
update_index_titles(app)
_KEYBINDS.update(scan_all_keybinds(os.path.join(dfhack.util.DFHACK_ROOT, 'data', 'init'))) _KEYBINDS.update(scan_all_keybinds(os.path.join(dfhack.util.DFHACK_ROOT, 'data', 'init')))
def setup(app): def setup(app):
app.connect('builder-inited', register) app.connect('builder-inited', register)
app.add_domain(TagRepoDomain)
register_index(app, 'all', 'Index of DFHack tools')
init_tag_indices(app)
# TODO: re-enable once detection is corrected # TODO: re-enable once detection is corrected
# app.connect('build-finished', lambda *_: check_missing_keybinds()) # app.connect('build-finished', lambda *_: check_missing_keybinds())

@ -622,12 +622,18 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
vector<string> filter; vector<string> filter;
bool skip_tags = false; bool skip_tags = false;
bool show_dev_commands = false; bool show_dev_commands = false;
string exclude_strs = "";
bool in_exclude = false;
for (auto str : params) { for (auto str : params) {
if (str == "--notags") if (in_exclude)
exclude_strs = str;
else if (str == "--notags")
skip_tags = true; skip_tags = true;
else if (str == "--dev") else if (str == "--dev")
show_dev_commands = true; show_dev_commands = true;
else if (str == "--exclude")
in_exclude = true;
else else
filter.push_back(str); filter.push_back(str);
} }
@ -636,7 +642,7 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
auto L = Lua::Core::State; auto L = Lua::Core::State;
Lua::StackUnwinder top(L); Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 4) || if (!lua_checkstack(L, 5) ||
!Lua::PushModulePublic(con, L, "helpdb", "ls")) { !Lua::PushModulePublic(con, L, "helpdb", "ls")) {
con.printerr("Failed to load helpdb Lua code\n"); con.printerr("Failed to load helpdb Lua code\n");
return; return;
@ -645,8 +651,9 @@ void ls_helper(color_ostream &con, const vector<string> &params) {
Lua::PushVector(L, filter); Lua::PushVector(L, filter);
Lua::Push(L, skip_tags); Lua::Push(L, skip_tags);
Lua::Push(L, show_dev_commands); Lua::Push(L, show_dev_commands);
Lua::Push(L, exclude_strs);
if (!Lua::SafeCall(con, L, 3, 0)) { if (!Lua::SafeCall(con, L, 4, 0)) {
con.printerr("Failed Lua call to helpdb.ls.\n"); con.printerr("Failed Lua call to helpdb.ls.\n");
} }
} }

@ -359,6 +359,149 @@ function EditField:onInput(keys)
return self.modal return self.modal
end end
---------------
-- Scrollbar --
---------------
-- these can be overridden by the user, e.g.:
-- require('gui.widgets').SCROLL_DELAY_MS = 100
SCROLL_INITIAL_DELAY_MS = 300
SCROLL_DELAY_MS = 20
Scrollbar = defclass(Scrollbar, Widget)
Scrollbar.ATTRS{
fg = COLOR_LIGHTGREEN,
bg = COLOR_CYAN,
on_scroll = DEFAULT_NIL,
}
function Scrollbar:preinit(init_table)
init_table.frame = init_table.frame or {}
init_table.frame.w = init_table.frame.w or 1
end
function Scrollbar:init()
self.last_scroll_ms = 0
self.is_first_click = false
self.scroll_spec = nil
self.is_dragging = false -- index of the scrollbar tile that we're dragging
self:update(1, 1, 1)
end
-- calculate and cache the number of tiles of empty space above the top of the
-- scrollbar and the number of tiles the scrollbar should occupy to represent
-- the percentage of text that is on the screen.
-- if elems_per_page or num_elems are not specified, the last values passed to
-- Scrollbar:update() are used.
function Scrollbar:update(top_elem, elems_per_page, num_elems)
if not top_elem then error('must specify index of new top element') end
elems_per_page = elems_per_page or self.elems_per_page
num_elems = num_elems or self.num_elems
local frame_height = self.frame_body and self.frame_body.height or 3
local scrollbar_body_height = frame_height - 2
local height = math.max(1, math.floor(
(math.min(elems_per_page, num_elems) * scrollbar_body_height) /
num_elems))
local max_pos = scrollbar_body_height - height
local pos = (num_elems == elems_per_page) and 0 or
math.ceil(((top_elem-1) * max_pos) /
(num_elems - elems_per_page))
self.top_elem = top_elem
self.elems_per_page, self.num_elems = elems_per_page, num_elems
self.bar_offset, self.bar_height = pos, height
end
local function scrollbar_do_drag(scrollbar)
local x,y = dfhack.screen.getMousePos()
x,y = scrollbar.frame_body:localXY(x,y)
local bar_idx = y - scrollbar.bar_offset
local delta = bar_idx - scrollbar.is_dragging
if delta < -scrollbar.bar_height then
scrollbar.on_scroll('up_large')
elseif delta < 0 then
scrollbar.on_scroll('up_small')
elseif delta > scrollbar.bar_height then
scrollbar.on_scroll('down_large')
elseif delta > 0 then
scrollbar.on_scroll('down_small')
end
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 Scrollbar:onRenderBody(dc)
-- don't draw if all elements are visible
if self.elems_per_page >= self.num_elems then return end
-- render up arrow if we're not at the top
dc:seek(0, 0):char(
self.top_elem == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, self.fg, self.bg)
-- render scrollbar body
local starty = self.bar_offset + 1
local endy = self.bar_offset + self.bar_height
for y=1,dc.height-2 do
dc:seek(0, y)
if y >= starty and y <= endy then
dc:char(BAR_CHAR, self.fg)
else
dc:char(BAR_BG_CHAR, self.bg)
end
end
-- render down arrow if we're not at the bottom
local last_visible_el = self.top_elem + self.elems_per_page - 1
dc:seek(0, dc.height-1):char(
last_visible_el >= self.num_elems and NO_ARROW_CHAR or DOWN_ARROW_CHAR,
self.fg, self.bg)
if not self.on_scroll then return end
-- manage state for dragging and continuous scrolling
if self.is_dragging then
scrollbar_do_drag(self)
end
if df.global.enabler.mouse_lbut_down == 0 then
self.last_scroll_ms = 0
self.is_dragging = false
self.scroll_spec = nil
return
end
if self.last_scroll_ms == 0 then return end
local now = dfhack.getTickCount()
local delay = self.is_first_click and
SCROLL_INITIAL_DELAY_MS or SCROLL_DELAY_MS
if now - self.last_scroll_ms >= delay then
self.is_first_click = false
self.on_scroll(self.scroll_spec)
self.last_scroll_ms = now
end
end
function Scrollbar:onInput(keys)
if not keys._MOUSE_L_DOWN or not self.on_scroll then return false end
local _,y = self:getMousePos()
if not y then return false end
local scroll_spec = nil
if y == 0 then scroll_spec = 'up_small'
elseif y == self.frame_body.height - 1 then scroll_spec = 'down_small'
elseif y <= self.bar_offset then scroll_spec = 'up_large'
elseif y > self.bar_offset + self.bar_height then scroll_spec = 'down_large'
else
self.is_dragging = y - self.bar_offset
return true
end
self.scroll_spec = scroll_spec
self.on_scroll(scroll_spec)
-- reset continuous scroll state
self.is_first_click = true
self.last_scroll_ms = dfhack.getTickCount()
return true
end
----------- -----------
-- Label -- -- Label --
----------- -----------
@ -546,12 +689,15 @@ Label.ATTRS{
on_click = DEFAULT_NIL, on_click = DEFAULT_NIL,
on_rclick = DEFAULT_NIL, on_rclick = DEFAULT_NIL,
scroll_keys = STANDARDSCROLL, scroll_keys = STANDARDSCROLL,
show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false
scrollbar_fg = COLOR_LIGHTGREEN,
scrollbar_bg = COLOR_CYAN
} }
function Label:init(args) function Label:init(args)
self.scrollbar = Scrollbar{
frame={r=0},
on_scroll=self:callback('on_scrollbar')}
self:addviews{self.scrollbar}
-- use existing saved text if no explicit text was specified. this avoids -- use existing saved text if no explicit text was specified. this avoids
-- overwriting pre-formatted text that subclasses may have already set -- overwriting pre-formatted text that subclasses may have already set
self:setText(args.text or self.text) self:setText(args.text or self.text)
@ -560,6 +706,12 @@ function Label:init(args)
end end
end end
local function update_label_scrollbar(label)
local body_height = label.frame_body and label.frame_body.height or 1
label.scrollbar:update(label.start_line_num, body_height,
label:getTextHeight())
end
function Label:setText(text) function Label:setText(text)
self.start_line_num = 1 self.start_line_num = 1
self.text = text self.text = text
@ -569,87 +721,8 @@ function Label:setText(text)
self.frame = self.frame or {} self.frame = self.frame or {}
self.frame.h = self:getTextHeight() self.frame.h = self:getTextHeight()
end end
end
function Label:update_scroll_inset() update_label_scrollbar(self)
if self.show_scrollbar == nil then
self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false
else
self._show_scrollbar = self.show_scrollbar
end
if self._show_scrollbar then
-- here self._show_scrollbar can only be either
-- 'left' or any true value which we interpret as right
local l,t,r,b = gui.parse_inset(self.frame_inset)
if self._show_scrollbar == 'left' and l <= 0 then
l = 1
elseif r <= 0 then
r = 1
end
self.frame_inset = {l=l,t=t,r=r,b=b}
end
end
-- the position is the number of tiles of empty space above the top of the
-- scrollbar, and the height is the number of tiles the scrollbar should occupy
-- to represent the percentage of text that is on the screen.
local function get_scrollbar_pos_and_height(label)
local first_visible_line = label.start_line_num
local text_height = label:getTextHeight()
local last_visible_line = first_visible_line + label.frame_body.height - 1
local scrollbar_body_height = label.frame_body.height - 2
local displayed_lines = last_visible_line - first_visible_line
local height = math.floor(((displayed_lines-1) * scrollbar_body_height) /
text_height)
local max_pos = scrollbar_body_height - height
local pos = math.ceil(((first_visible_line-1) * max_pos) /
(text_height - label.frame_body.height))
return pos, height
end
local UP_ARROW_CHAR = string.char(24)
local DOWN_ARROW_CHAR = string.char(25)
local NO_ARROW_CHAR = string.char(32)
local BAR_CHAR = string.char(7)
local BAR_BG_CHAR = string.char(179)
function Label:render_scrollbar(dc, x, y1, y2)
-- render up arrow if we're not at the top
dc:seek(x, y1):char(
self.start_line_num == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR,
self.scrollbar_fg, self.scrollbar_bg)
-- render scrollbar body
local pos, height = get_scrollbar_pos_and_height(self)
local starty = y1 + pos + 1
local endy = y1 + pos + height
for y=y1+1,y2-1 do
if y >= starty and y <= endy then
dc:seek(x, y):char(BAR_CHAR, self.scrollbar_fg)
else
dc:seek(x, y):char(BAR_BG_CHAR, self.scrollbar_bg)
end
end
-- render down arrow if we're not at the bottom
local last_visible_line = self.start_line_num + self.frame_body.height - 1
dc:seek(x, y2):char(
last_visible_line >= self:getTextHeight() and
NO_ARROW_CHAR or DOWN_ARROW_CHAR,
self.scrollbar_fg, self.scrollbar_bg)
end
function Label:computeFrame(parent_rect)
local frame_rect,body_rect = Label.super.computeFrame(self, parent_rect)
self.frame_rect = frame_rect
self.frame_body = parent_rect:viewport(body_rect or frame_rect)
self:update_scroll_inset() -- frame_body is now set
-- recalc with updated frame_inset
return Label.super.computeFrame(self, parent_rect)
end end
function Label:preUpdateLayout() function Label:preUpdateLayout()
@ -659,6 +732,10 @@ function Label:preUpdateLayout()
end end
end end
function Label:postUpdateLayout()
update_label_scrollbar(self)
end
function Label:itemById(id) function Label:itemById(id)
if self.text_ids then if self.text_ids then
return self.text_ids[id] return self.text_ids[id]
@ -682,44 +759,19 @@ function Label:onRenderBody(dc)
render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self))
end end
function Label:onRenderFrame(dc, rect) function Label:on_scrollbar(scroll_spec)
if self._show_scrollbar then local v = 0
local x = self._show_scrollbar == 'left' if scroll_spec == 'down_large' then
and self.frame_body.x1-dc.x1-1 v = '+halfpage'
or self.frame_body.x2-dc.x1+1 elseif scroll_spec == 'up_large' then
self:render_scrollbar(dc, v = '-halfpage'
x, elseif scroll_spec == 'down_small' then
self.frame_body.y1-dc.y1, v = 1
self.frame_body.y2-dc.y1 elseif scroll_spec == 'up_small' then
) v = -1
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 end
if y == rect.y1 then self:scroll(v)
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 end
function Label:scroll(nlines) function Label:scroll(nlines)
@ -740,24 +792,28 @@ function Label:scroll(nlines)
local n = self.start_line_num + nlines local n = self.start_line_num + 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)
nlines = n - self.start_line_num
self.start_line_num = n self.start_line_num = n
update_label_scrollbar(self)
return nlines 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 then if self:inputToSubviews(keys) then
if not self:scroll(self:click_scrollbar()) and return true
self:getMousePos() and self.on_click then end
self:on_click() if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then
end self:on_click()
return true
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()
return true
end end
for k,v in pairs(self.scroll_keys) do for k,v in pairs(self.scroll_keys) do
if keys[k] then if keys[k] and 0 ~= self:scroll(v) then
self:scroll(v) return true
end end
end end
return check_text_keys(self, keys) return check_text_keys(self, keys)
@ -787,7 +843,7 @@ end
-- we can't set the text in init() since we may not yet have a frame that we -- we can't set the text in init() since we may not yet have a frame that we
-- can get wrapping bounds from. -- can get wrapping bounds from.
function WrappedLabel:postComputeFrame() function WrappedLabel:postComputeFrame()
local wrapped_text = self:getWrappedText(self.frame_body.width) local wrapped_text = self:getWrappedText(self.frame_body.width-1)
if not wrapped_text then return end if not wrapped_text then return end
local text = {} local text = {}
for _,line in ipairs(wrapped_text:split(NEWLINE)) do for _,line in ipairs(wrapped_text:split(NEWLINE)) do
@ -953,6 +1009,11 @@ List.ATTRS{
function List:init(info) function List:init(info)
self.page_top = 1 self.page_top = 1
self.page_size = 1 self.page_size = 1
self.scrollbar = Scrollbar{
frame={r=0},
on_scroll=self:callback('on_scrollbar')}
self:addviews{self.scrollbar}
if info.choices then if info.choices then
self:setChoices(info.choices, info.selected) self:setChoices(info.choices, info.selected)
@ -1017,13 +1078,21 @@ function List:postComputeFrame(body)
self:moveCursor(0) self:moveCursor(0)
end end
local function update_list_scrollbar(list)
list.scrollbar:update(list.page_top, list.page_size, #list.choices)
end
function List:postUpdateLayout()
update_list_scrollbar(self)
end
function List:moveCursor(delta, force_cb) function List:moveCursor(delta, force_cb)
local page = math.max(1, self.page_size)
local cnt = #self.choices local cnt = #self.choices
if cnt < 1 then if cnt < 1 then
self.page_top = 1 self.page_top = 1
self.selected = 1 self.selected = 1
update_list_scrollbar(self)
if force_cb and self.on_select then if force_cb and self.on_select then
self.on_select(nil,nil) self.on_select(nil,nil)
end end
@ -1046,14 +1115,40 @@ function List:moveCursor(delta, force_cb)
end end
end end
local buffer = 1 + math.min(4, math.floor(self.page_size/10))
self.selected = 1 + off % cnt self.selected = 1 + off % cnt
self.page_top = 1 + page * math.floor((self.selected-1) / page) if (self.selected - buffer) < self.page_top then
self.page_top = math.max(1, self.selected - buffer)
elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then
local max_page_top = cnt - self.page_size + 1
self.page_top = math.max(1,
math.min(max_page_top, self.selected - self.page_size + buffer + 1))
end
update_list_scrollbar(self)
if (force_cb or delta ~= 0) and self.on_select then if (force_cb or delta ~= 0) and self.on_select then
self.on_select(self:getSelected()) self.on_select(self:getSelected())
end end
end end
function List:on_scrollbar(scroll_spec)
local v = 0
if scroll_spec == 'down_large' then
v = math.ceil(self.page_size / 2)
elseif scroll_spec == 'up_large' then
v = -math.ceil(self.page_size / 2)
elseif scroll_spec == 'down_small' then
v = 1
elseif scroll_spec == 'up_small' then
v = -1
end
local max_page_top = math.max(1, #self.choices - self.page_size + 1)
self.page_top = math.max(1, math.min(max_page_top, self.page_top + v))
update_list_scrollbar(self)
end
function List:onRenderBody(dc) function List:onRenderBody(dc)
local choices = self.choices local choices = self.choices
local top = self.page_top local top = self.page_top
@ -1122,6 +1217,9 @@ function List:submit2()
end end
function List:onInput(keys) function List:onInput(keys)
if self:inputToSubviews(keys) then
return true
end
if self.on_submit and keys.SELECT then if self.on_submit and keys.SELECT then
self:submit() self:submit()
return true return true

@ -14,6 +14,8 @@
local _ENV = mkmodule('helpdb') local _ENV = mkmodule('helpdb')
local argparse = require('argparse')
local MAX_STALE_MS = 60000 local MAX_STALE_MS = 60000
-- paths -- paths
@ -120,7 +122,7 @@ No help available.
]] ]]
local function make_default_entry(entry_name, help_source, kwargs) local function make_default_entry(entry_name, help_source, kwargs)
local default_long_help = DEFAULT_HELP_TEMPLATE:format( local default_long_help = DEFAULT_HELP_TEMPLATE:format(
entry_name, ('*'):rep(#entry_name)) entry_name, ('='):rep(#entry_name))
return { return {
help_source=help_source, help_source=help_source,
short_help='No help available.', short_help='No help available.',
@ -275,7 +277,7 @@ local function make_script_entry(old_entry, entry_name, kwargs)
local is_rb = source_path:endswith('.rb') local is_rb = source_path:endswith('.rb')
update_entry(entry, lines, update_entry(entry, lines,
{begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN), {begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN),
end_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_END), end_marker=(is_rb and SCRIPT_DOC_END_RUBY or SCRIPT_DOC_END),
first_line_is_short_help=(is_rb and '#' or '%-%-')}) first_line_is_short_help=(is_rb and '#' or '%-%-')})
return entry return entry
end end
@ -321,6 +323,8 @@ local function scan_builtins(old_db)
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
{entry_types=entry_types}) {entry_types=entry_types})
end end
-- easter egg: replace underline for 'die' help with tombstones
textdb.die.long_help = textdb.die.long_help:gsub('=', string.char(239))
end end
-- scan for enableable plugins and plugin-provided commands and add their help -- scan for enableable plugins and plugin-provided commands and add their help
@ -394,7 +398,7 @@ local function initialize_tags()
desc = desc .. ' ' .. line desc = desc .. ' ' .. line
tag_index[tag].description = desc tag_index[tag].description = desc
else else
_,_,tag,desc = line:find('^%* (%w+): (.+)') _,_,tag,desc = line:find('^%* (%w+)[^:]*: (.+)')
if not tag then goto continue end if not tag then goto continue end
tag_index[tag] = {description=desc} tag_index[tag] = {description=desc}
in_desc = true in_desc = true
@ -586,6 +590,8 @@ function sort_by_basename(a, b)
return false return false
end end
-- returns true if all filter elements are matched (i.e. any of the tags AND
-- any of the strings AND any of the entry_types)
local function matches(entry_name, filter) local function matches(entry_name, filter)
if filter.tag then if filter.tag then
local matched = false local matched = false
@ -628,9 +634,18 @@ local function matches(entry_name, filter)
return true return true
end end
local function matches_any(entry_name, filters)
for _,filter in ipairs(filters) do
if matches(entry_name, filter) then
return true
end
end
return false
end
-- normalizes the lists in the filter and returns nil if no filter elements are -- normalizes the lists in the filter and returns nil if no filter elements are
-- populated -- populated
local function normalize_filter(f) local function normalize_filter_map(f)
if not f then return nil end if not f then return nil end
local filter = {} local filter = {}
filter.str = normalize_string_list(f.str) filter.str = normalize_string_list(f.str)
@ -642,11 +657,21 @@ local function normalize_filter(f)
return filter return filter
end end
local function normalize_filter_list(fs)
if not fs then return nil end
local filter_list = {}
for _,f in ipairs(#fs > 0 and fs or {fs}) do
table.insert(filter_list, normalize_filter_map(f))
end
if #filter_list == 0 then return nil end
return filter_list
end
-- returns a list of entry names, alphabetized by their last path component, -- returns a list of entry names, alphabetized by their last path component,
-- with populated path components coming before null path components (e.g. -- with populated path components coming before null path components (e.g.
-- autobutcher will immediately follow gui/autobutcher). -- autobutcher will immediately follow gui/autobutcher).
-- the optional include and exclude filter params are maps with the following -- the optional include and exclude filter params are maps (or lists of maps)
-- elements: -- with the following elements:
-- str - if a string, filters by the given substring. if a table of strings, -- str - if a string, filters by the given substring. if a table of strings,
-- includes entry names that match any of the given substrings. -- includes entry names that match any of the given substrings.
-- tag - if a string, filters by the given tag name. if a table of strings, -- tag - if a string, filters by the given tag name. if a table of strings,
@ -656,14 +681,18 @@ end
-- types are: "builtin", "plugin", "command". note that many plugin -- types are: "builtin", "plugin", "command". note that many plugin
-- commands have the same name as the plugin, so those entries will -- commands have the same name as the plugin, so those entries will
-- match both "plugin" and "command" types. -- match both "plugin" and "command" types.
-- filter elements in a map are ANDed together (e.g. if both str and tag are
-- specified, the match is on any of the str elements AND any of the tag
-- elements). If lists of maps are passed, the maps are ORed (that is, the match
-- succeeds if any of the filters match).
function search_entries(include, exclude) function search_entries(include, exclude)
ensure_db() ensure_db()
include = normalize_filter(include) include = normalize_filter_list(include)
exclude = normalize_filter(exclude) exclude = normalize_filter_list(exclude)
local entries = {} local entries = {}
for entry in pairs(entrydb) do for entry in pairs(entrydb) do
if (not include or matches(entry, include)) and if (not include or matches_any(entry, include)) and
(not exclude or not matches(entry, exclude)) then (not exclude or not matches_any(entry, exclude)) then
table.insert(entries, entry) table.insert(entries, entry)
end end
end end
@ -724,13 +753,15 @@ end
local function list_entries(skip_tags, include, exclude) local function list_entries(skip_tags, include, exclude)
local entries = search_entries(include, exclude) local entries = search_entries(include, exclude)
for _,entry in ipairs(entries) do for _,entry in ipairs(entries) do
print_columns(entry, get_entry_short_help(entry)) local short_help = get_entry_short_help(entry)
if not skip_tags then if not skip_tags then
local tags = set_to_sorted_list(get_entry_tags(entry)) local tags = set_to_sorted_list(get_entry_tags(entry))
if #tags > 0 then if #tags > 0 then
print((' tags: %s'):format(table.concat(tags, ', '))) local taglist = table.concat(tags, ', ')
short_help = short_help .. NEWLINE .. 'tags: ' .. taglist
end end
end end
print_columns(entry, short_help)
end end
if #entries == 0 then if #entries == 0 then
print('No matches.') print('No matches.')
@ -739,21 +770,30 @@ end
-- wraps the list_entries() API to provide a more convenient interface for Core -- wraps the list_entries() API to provide a more convenient interface for Core
-- to implement the 'ls' builtin command. -- to implement the 'ls' builtin command.
-- filter_str - if a tag name, will filter by that tag. otherwise, will filter -- filter_str - if a tag name (or a list of tag names), will filter by that
-- as a substring -- tag/those tags. otherwise, will filter as a substring/list of
-- substrings
-- skip_tags - whether to skip printing tag info -- skip_tags - whether to skip printing tag info
-- show_dev_commands - if true, will include scripts in the modtools/ and -- show_dev_commands - if true, will include scripts in the modtools/ and
-- devel/ directories. otherwise those scripts will be -- devel/ directories. otherwise those scripts will be
-- excluded -- excluded
function ls(filter_str, skip_tags, show_dev_commands) -- exclude_strs - comma-separated list of strings. entries are excluded if
-- they match any of the strings.
function ls(filter_str, skip_tags, show_dev_commands, exclude_strs)
local include = {entry_type={ENTRY_TYPES.COMMAND}} local include = {entry_type={ENTRY_TYPES.COMMAND}}
if is_tag(filter_str) then if is_tag(filter_str) then
include.tag = filter_str include.tag = filter_str
else else
include.str = filter_str include.str = filter_str
end end
list_entries(skip_tags, include, local excludes = {}
show_dev_commands and {} or {tag='dev'}) if exclude_strs and #exclude_strs > 0 then
table.insert(excludes, {str=argparse.stringList(exclude_strs)})
end
if not show_dev_commands then
table.insert(excludes, {tag='dev'})
end
list_entries(skip_tags, include, excludes)
end end
local function list_tags() local function list_tags()

@ -308,9 +308,8 @@ df::building *Buildings::findAtTile(df::coord pos)
static unordered_map<int32_t, df::coord> corner1; static unordered_map<int32_t, df::coord> corner1;
static unordered_map<int32_t, df::coord> corner2; static unordered_map<int32_t, df::coord> corner2;
static void cacheBuilding(df::building *building) { static void cacheBuilding(df::building *building, bool is_civzone) {
int32_t id = building->id; int32_t id = building->id;
bool is_civzone = !building->isSettingOccupancy();
df::coord p1(min(building->x1, building->x2), min(building->y1,building->y2), building->z); df::coord p1(min(building->x1, building->x2), min(building->y1,building->y2), building->z);
df::coord p2(max(building->x1, building->x2), max(building->y1,building->y2), building->z); df::coord p2(max(building->x1, building->x2), max(building->y1,building->y2), building->z);
@ -344,7 +343,7 @@ static void cacheNewCivzones() {
auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE];
int32_t idx = df::building::binsearch_index(vec, id); int32_t idx = df::building::binsearch_index(vec, id);
if (idx > -1) if (idx > -1)
cacheBuilding(vec[idx]); cacheBuilding(vec[idx], true);
} }
nextCivzone = nextBuildingId; nextCivzone = nextBuildingId;
} }
@ -1311,8 +1310,9 @@ void Buildings::updateBuildings(color_ostream&, void* ptr)
if (building) if (building)
{ {
if (!corner1.count(id)) bool is_civzone = !building->isSettingOccupancy();
cacheBuilding(building); if (!corner1.count(id) && !is_civzone)
cacheBuilding(building, false);
} }
else if (corner1.count(id)) else if (corner1.count(id))
{ {

@ -174,6 +174,7 @@ bool Units::teleport(df::unit *unit, df::coord target_pos)
// move unit to destination // move unit to destination
unit->pos = target_pos; unit->pos = target_pos;
unit->idle_area = target_pos;
// move unit's riders (including babies) to destination // move unit's riders (including babies) to destination
if (unit->flags1.bits.ridden) if (unit->flags1.bits.ridden)

@ -1 +1 @@
Subproject commit f5fab13fb652dd953e9d59e8deca032b26131208 Subproject commit f2b59b8d5036cca90a88245d0d7c8b2a713f60ee

@ -76,6 +76,8 @@ struct blueprint_options {
// base name to use for generated files // base name to use for generated files
string name; string name;
// whether to capture all smoothed tiles
bool smooth = false;
// whether to capture engravings and smooth the tiles that will be engraved // whether to capture engravings and smooth the tiles that will be engraved
bool engrave = false; bool engrave = false;
@ -103,6 +105,7 @@ static const struct_field_info blueprint_options_fields[] = {
{ struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits<int32_t>::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits<int32_t>::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits<string>::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits<string>::get(), 0, 0 },
{ struct_field_info::PRIMITIVE, "smooth", offsetof(blueprint_options, smooth), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "engrave", offsetof(blueprint_options, engrave), &df::identity_traits<bool>::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "engrave", offsetof(blueprint_options, engrave), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits<bool>::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits<bool>::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits<bool>::identity, 0, 0 },
@ -219,8 +222,8 @@ static const char * get_tile_smooth_minimal(const df::coord &pos,
return NULL; return NULL;
} }
static const char * get_tile_smooth(const df::coord &pos, static const char * get_tile_smooth_with_engravings(const df::coord &pos,
const tile_context &tc) { const tile_context &tc) {
const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); const char * smooth_minimal = get_tile_smooth_minimal(pos, tc);
if (smooth_minimal) if (smooth_minimal)
return smooth_minimal; return smooth_minimal;
@ -244,6 +247,30 @@ static const char * get_tile_smooth(const df::coord &pos,
return NULL; return NULL;
} }
static const char * get_tile_smooth_all(const df::coord &pos,
const tile_context &tc) {
const char * smooth_minimal = get_tile_smooth_minimal(pos, tc);
if (smooth_minimal)
return smooth_minimal;
df::tiletype *tt = Maps::getTileType(pos);
if (!tt)
return NULL;
switch (tileShape(*tt))
{
case tiletype_shape::FLOOR:
case tiletype_shape::WALL:
if (tileSpecial(*tt) == tiletype_special::SMOOTH)
return "s";
break;
default:
break;
}
return NULL;
}
static const char * get_track_str(const char *prefix, df::tiletype tt) { static const char * get_track_str(const char *prefix, df::tiletype tt) {
TileDirection tdir = tileDirection(tt); TileDirection tdir = tileDirection(tt);
@ -1096,9 +1123,13 @@ static bool do_transform(color_ostream &out,
vector<blueprint_processor> processors; vector<blueprint_processor> processors;
get_tile_fn* smooth_get_tile_fn = get_tile_smooth_minimal;
if (opts.engrave) smooth_get_tile_fn = get_tile_smooth_with_engravings;
if (opts.smooth) smooth_get_tile_fn = get_tile_smooth_all;
add_processor(processors, opts, "dig", "dig", opts.dig, get_tile_dig); add_processor(processors, opts, "dig", "dig", opts.dig, get_tile_dig);
add_processor(processors, opts, "dig", "smooth", opts.carve, add_processor(processors, opts, "dig", "smooth", opts.carve,
opts.engrave ? get_tile_smooth : get_tile_smooth_minimal); smooth_get_tile_fn);
add_processor(processors, opts, "dig", "carve", opts.carve, add_processor(processors, opts, "dig", "carve", opts.carve,
opts.engrave ? get_tile_carve : get_tile_carve_minimal); opts.engrave ? get_tile_carve : get_tile_carve_minimal);
add_processor(processors, opts, "build", "build", opts.build, add_processor(processors, opts, "build", "build", opts.build,
@ -1187,23 +1218,11 @@ static bool get_options(color_ostream &out,
return true; return true;
} }
static void print_help(color_ostream &out) {
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 1) ||
!Lua::PushModulePublic(out, L, "plugins.blueprint", "print_help") ||
!Lua::SafeCall(out, L, 0, 0))
{
out.printerr("Failed to load blueprint Lua code\n");
}
}
// returns whether blueprint generation was successful. populates files with the // returns whether blueprint generation was successful. populates files with the
// names of the files that were generated // names of the files that were generated
static bool do_blueprint(color_ostream &out, static command_result do_blueprint(color_ostream &out,
const vector<string> &parameters, const vector<string> &parameters,
vector<string> &files) { vector<string> &files) {
CoreSuspender suspend; CoreSuspender suspend;
if (parameters.size() >= 1 && parameters[0] == "gui") { if (parameters.size() >= 1 && parameters[0] == "gui") {
@ -1221,13 +1240,12 @@ static bool do_blueprint(color_ostream &out,
blueprint_options options; blueprint_options options;
if (!get_options(out, options, parameters) || options.help) { if (!get_options(out, options, parameters) || options.help) {
print_help(out); return CR_WRONG_USAGE;
return options.help;
} }
if (!Maps::IsValid()) { if (!Maps::IsValid()) {
out.printerr("Map is not available!\n"); out.printerr("Map is not available!\n");
return false; return CR_FAILURE;
} }
// start coordinates can come from either the commandline or the map cursor // start coordinates can come from either the commandline or the map cursor
@ -1236,13 +1254,13 @@ static bool do_blueprint(color_ostream &out,
if (!Gui::getCursorCoords(start)) { if (!Gui::getCursorCoords(start)) {
out.printerr("Can't get cursor coords! Make sure you specify the" out.printerr("Can't get cursor coords! Make sure you specify the"
" --cursor parameter or have an active cursor in DF.\n"); " --cursor parameter or have an active cursor in DF.\n");
return false; return CR_FAILURE;
} }
} }
if (!Maps::isValidTilePos(start)) { if (!Maps::isValidTilePos(start)) {
out.printerr("Invalid start position: %d,%d,%d\n", out.printerr("Invalid start position: %d,%d,%d\n",
start.x, start.y, start.z); start.x, start.y, start.z);
return false; return CR_FAILURE;
} }
// end coords are one beyond the last processed coordinate. note that // end coords are one beyond the last processed coordinate. note that
@ -1265,7 +1283,7 @@ static bool do_blueprint(color_ostream &out,
bool ok = do_transform(out, start, end, options, files); bool ok = do_transform(out, start, end, options, files);
cache(NULL); cache(NULL);
return ok; return ok ? CR_OK : CR_FAILURE;
} }
// entrypoint when called from Lua. returns the names of the generated files // entrypoint when called from Lua. returns the names of the generated files
@ -1284,7 +1302,7 @@ static int run(lua_State *L) {
color_ostream *out = Lua::GetOutput(L); color_ostream *out = Lua::GetOutput(L);
if (!out) if (!out)
out = &Core::getInstance().getConsole(); out = &Core::getInstance().getConsole();
if (do_blueprint(*out, argv, files)) { if (CR_OK == do_blueprint(*out, argv, files)) {
Lua::PushVector(L, files); Lua::PushVector(L, files);
return 1; return 1;
} }
@ -1294,13 +1312,13 @@ static int run(lua_State *L) {
command_result blueprint(color_ostream &out, vector<string> &parameters) { command_result blueprint(color_ostream &out, vector<string> &parameters) {
vector<string> files; vector<string> files;
if (do_blueprint(out, parameters, files)) { command_result cr = do_blueprint(out, parameters, files);
if (cr == CR_OK) {
out.print("Generated blueprint file(s):\n"); out.print("Generated blueprint file(s):\n");
for (string &fname : files) for (string &fname : files)
out.print(" %s\n", fname.c_str()); out.print(" %s\n", fname.c_str());
return CR_OK;
} }
return CR_FAILURE; return cr;
} }
DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_PLUGIN_LUA_COMMANDS {

@ -35,6 +35,7 @@ command_result digtype (color_ostream &out, vector <string> & parameters);
DFHACK_PLUGIN("dig"); DFHACK_PLUGIN("dig");
REQUIRE_GLOBAL(ui_sidebar_menus); REQUIRE_GLOBAL(ui_sidebar_menus);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(window_z);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands) DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{ {
@ -1417,16 +1418,18 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
//mostly copy-pasted from digv //mostly copy-pasted from digv
int32_t priority = parse_priority(out, parameters); int32_t priority = parse_priority(out, parameters);
CoreSuspender suspend; CoreSuspender suspend;
if ( parameters.size() > 1 )
if (!Maps::IsValid())
{ {
out.printerr("Too many parameters.\n"); out.printerr("Map is not available!\n");
return CR_FAILURE; return CR_FAILURE;
} }
int32_t targetDigType; uint32_t xMax,yMax,zMax;
if ( parameters.size() == 1 ) Maps::getSize(xMax,yMax,zMax);
{
string parameter = parameters[0]; int32_t targetDigType = -1;
for (string parameter : parameters) {
if ( parameter == "clear" ) if ( parameter == "clear" )
targetDigType = tile_dig_designation::No; targetDigType = tile_dig_designation::No;
else if ( parameter == "dig" ) else if ( parameter == "dig" )
@ -1441,26 +1444,16 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
targetDigType = tile_dig_designation::DownStair; targetDigType = tile_dig_designation::DownStair;
else if ( parameter == "up" ) else if ( parameter == "up" )
targetDigType = tile_dig_designation::UpStair; targetDigType = tile_dig_designation::UpStair;
else if ( parameter == "-z" )
zMax = *window_z + 1;
else else
{ {
out.printerr("Invalid parameter.\n"); out.printerr("Invalid parameter: '%s'.\n", parameter.c_str());
return CR_FAILURE; return CR_FAILURE;
} }
} }
else
{
targetDigType = -1;
}
if (!Maps::IsValid())
{
out.printerr("Map is not available!\n");
return CR_FAILURE;
}
int32_t cx, cy, cz; int32_t cx, cy, cz;
uint32_t xMax,yMax,zMax;
Maps::getSize(xMax,yMax,zMax);
uint32_t tileXMax = xMax * 16; uint32_t tileXMax = xMax * 16;
uint32_t tileYMax = yMax * 16; uint32_t tileYMax = yMax * 16;
Gui::getCursorCoords(cx,cy,cz); Gui::getCursorCoords(cx,cy,cz);

@ -3,37 +3,6 @@ local _ENV = mkmodule('plugins.blueprint')
local argparse = require('argparse') local argparse = require('argparse')
local utils = require('utils') local utils = require('utils')
-- the info here is very basic and minimal, so hopefully we won't need to change
-- it when features are added and the full blueprint docs in Plugins.rst are
-- updated.
local help_text = [=[
blueprint
=========
Records the structure of a portion of your fortress in quickfort blueprints.
Usage:
blueprint <width> <height> [<depth>] [<name> [<phases>]] [<options>]
blueprint gui [<name> [<phases>]] [<options>]
Examples:
blueprint gui
Runs gui/blueprint, the interactive blueprint frontend, where all
configuration can be set visually and interactively.
blueprint 30 40 bedrooms
Generates blueprints for an area 30 tiles wide by 40 tiles tall, starting
from the active cursor on the current z-level. Output files are written to
the "blueprints" directory.
See the online DFHack documentation for more examples and details.
]=]
function print_help() print(help_text) end
local valid_phase_list = { local valid_phase_list = {
'dig', 'dig',
'carve', 'carve',
@ -154,6 +123,7 @@ local function process_args(opts, args)
{'h', 'help', handler=function() opts.help = true end}, {'h', 'help', handler=function() opts.help = true end},
{'s', 'playback-start', hasArg=true, {'s', 'playback-start', hasArg=true,
handler=function(optarg) parse_start(opts, optarg) end}, handler=function(optarg) parse_start(opts, optarg) end},
{nil, 'smooth', handler=function() opts.smooth = true end},
{'t', 'splitby', hasArg=true, {'t', 'splitby', hasArg=true,
handler=function(optarg) parse_split_strategy(opts, optarg) end}, handler=function(optarg) parse_split_strategy(opts, optarg) end},
}) })

@ -70,7 +70,6 @@
#include "df/viewscreen_topicmeetingst.h" #include "df/viewscreen_topicmeetingst.h"
#include "df/viewscreen_topicmeeting_takerequestsst.h" #include "df/viewscreen_topicmeeting_takerequestsst.h"
#include "df/viewscreen_tradeagreementst.h" #include "df/viewscreen_tradeagreementst.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_tradelistst.h" #include "df/viewscreen_tradelistst.h"
#include "df/viewscreen_treasurelistst.h" #include "df/viewscreen_treasurelistst.h"
#include "df/viewscreen_unitlist_page.h" #include "df/viewscreen_unitlist_page.h"
@ -211,7 +210,6 @@ IMPLEMENT_HOOKS(topicmeeting_fill_land_holder_positions)
IMPLEMENT_HOOKS(topicmeeting) IMPLEMENT_HOOKS(topicmeeting)
IMPLEMENT_HOOKS(topicmeeting_takerequests) IMPLEMENT_HOOKS(topicmeeting_takerequests)
IMPLEMENT_HOOKS(tradeagreement) IMPLEMENT_HOOKS(tradeagreement)
IMPLEMENT_HOOKS(tradegoods)
IMPLEMENT_HOOKS(tradelist) IMPLEMENT_HOOKS(tradelist)
IMPLEMENT_HOOKS(treasurelist) IMPLEMENT_HOOKS(treasurelist)
IMPLEMENT_HOOKS(unitlist) IMPLEMENT_HOOKS(unitlist)
@ -310,7 +308,6 @@ DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
INTERPOSE_HOOKS_FAILED(topicmeeting) || INTERPOSE_HOOKS_FAILED(topicmeeting) ||
INTERPOSE_HOOKS_FAILED(topicmeeting_takerequests) || INTERPOSE_HOOKS_FAILED(topicmeeting_takerequests) ||
INTERPOSE_HOOKS_FAILED(tradeagreement) || INTERPOSE_HOOKS_FAILED(tradeagreement) ||
INTERPOSE_HOOKS_FAILED(tradegoods) ||
INTERPOSE_HOOKS_FAILED(tradelist) || INTERPOSE_HOOKS_FAILED(tradelist) ||
INTERPOSE_HOOKS_FAILED(treasurelist) || INTERPOSE_HOOKS_FAILED(treasurelist) ||
INTERPOSE_HOOKS_FAILED(unitlist) || INTERPOSE_HOOKS_FAILED(unitlist) ||

@ -1 +1 @@
Subproject commit 484988bb3d64f7aed9442af3b226faa77d8fd00d Subproject commit 937cf27f9b41301be6df0fe1d75d77b6f089f688

@ -11,70 +11,6 @@ fs.ATTRS = {
focus_path = 'test-framed-screen', focus_path = 'test-framed-screen',
} }
function test.correct_frame_body_with_scroll_icons()
local t = {}
for i = 1, 12 do
t[#t+1] = tostring(i)
t[#t+1] = NEWLINE
end
function fs:init()
self:addviews{
widgets.Label{
view_id = 'text',
frame_inset = 0,
text = t,
},
}
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scrollbar.")
end
function test.correct_frame_body_with_few_text_lines()
local t = {}
for i = 1, 10 do
t[#t+1] = tostring(i)
t[#t+1] = NEWLINE
end
function fs:init()
self:addviews{
widgets.Label{
view_id = 'text',
frame_inset = 0,
text = t,
},
}
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.")
end
function test.correct_frame_body_without_show_scrollbar()
local t = {}
for i = 1, 12 do
t[#t+1] = tostring(i)
t[#t+1] = NEWLINE
end
function fs:init()
self:addviews{
widgets.Label{
view_id = 'text',
frame_inset = 0,
text = t,
show_scrollbar = false,
},
}
end
local o = fs{}
expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.")
end
function test.scroll() function test.scroll()
local t = {} local t = {}
for i = 1, 12 do for i = 1, 12 do

@ -0,0 +1,93 @@
local gui = require('gui')
local widgets = require('gui.widgets')
function test.update()
local s = widgets.Scrollbar{}
s.frame_body = {height=100} -- give us some space to work with
-- initial defaults
expect.eq(1, s.top_elem)
expect.eq(1, s.elems_per_page)
expect.eq(1, s.num_elems)
expect.eq(0, s.bar_offset)
expect.eq(1, s.bar_height)
-- top_elem, elems_per_page, num_elems
s:update(1, 10, 0)
expect.eq(1, s.top_elem)
expect.eq(10, s.elems_per_page)
expect.eq(0, s.num_elems)
expect.eq(0, s.bar_offset)
expect.eq(1, s.bar_height)
-- first 10 of 50 shown
s:update(1, 10, 50)
expect.eq(1, s.top_elem)
expect.eq(10, s.elems_per_page)
expect.eq(50, s.num_elems)
expect.eq(0, s.bar_offset)
expect.eq(19, s.bar_height)
-- bottom 10 of 50 shown
s:update(41, 10, 50)
expect.eq(41, s.top_elem)
expect.eq(10, s.elems_per_page)
expect.eq(50, s.num_elems)
expect.eq(79, s.bar_offset)
expect.eq(19, s.bar_height)
-- ~middle 10 of 50 shown
s:update(23, 10, 50)
expect.eq(23, s.top_elem)
expect.eq(10, s.elems_per_page)
expect.eq(50, s.num_elems)
expect.eq(44, s.bar_offset)
expect.eq(19, s.bar_height)
end
function test.onInput()
local spec = nil
local mock_on_scroll = function(scroll_spec) spec = scroll_spec end
local s = widgets.Scrollbar{on_scroll=mock_on_scroll}
s.frame_body = {height=100} -- give us some space to work with
local y = nil
s.getMousePos = function() return 0, y end
-- put scrollbar somewhere in the middle so we can click above and below it
s:update(23, 10, 50)
expect.false_(s:onInput{}, 'no mouse down')
expect.false_(s:onInput{_MOUSE_L_DOWN=true}, 'no y coord')
spec, y = nil, 0
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('up_small', spec, 'on up arrow')
spec, y = nil, 1
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('up_large', spec, 'on body above bar')
spec, y = nil, 44
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('up_large', spec, 'on body just above bar')
spec, y = nil, 45
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.nil_(spec, 'on top of bar')
spec, y = nil, 63
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.nil_(spec, 'on bottom of bar')
spec, y = nil, 64
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('down_large', spec, 'on body just below bar')
spec, y = nil, 98
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('down_large', spec, 'on body below bar')
spec, y = nil, 99
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.eq('down_small', spec, 'on down arrow')
end

@ -36,89 +36,100 @@ local mock_script_db = {
inscript_docs=true, inscript_docs=true,
inscript_short_only=true, inscript_short_only=true,
nodocs_script=true, nodocs_script=true,
dev_script=true,
} }
local files = { local files = {
['hack/docs/docs/Tags.txt']=[[ ['hack/docs/docs/Tags.txt']=[[
* fort: Tools that are useful while in fort mode. * fort: Tools that are useful while in fort mode.
* armok: Tools that give you complete control over * armok: Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.
an aspect of the game or provide access to
information that the game intentionally keeps
hidden.
* map: Tools that interact with the game map. * map: Tools that interact with the game map.
* units: Tools that interact with units. * units: Tools that interact with units.
* dev: Dev tools.
* nomembers: Nothing is tagged with this. * nomembers: Nothing is tagged with this.
]], ]],
['hack/docs/docs/tools/hascommands.txt']=[[ ['hack/docs/docs/tools/hascommands.txt']=[[
hascommands hascommands
*********** ===========
**Tags:** fort | armok | units Tags: fort | armok | units
Documented a plugin that Documented a plugin that has commands.
has commands.
**Command:** "boxbinders" Command: "boxbinders"
Documented boxbinders. Documented boxbinders.
**Command:** "bindboxers" Command: "bindboxers"
Documented bindboxers. Documented bindboxers.
Documented full help. Documented full help.
]], ]],
['hack/docs/docs/tools/samename.txt']=[[ ['hack/docs/docs/tools/samename.txt']=[[
samename samename
******** ========
**Tags:** fort | armok Tags: fort | armok | units
| units
**Command:** "samename" Command: "samename"
Documented samename. Documented samename.
Documented full help. Documented full help.
]], ]],
['hack/docs/docs/tools/nocommand.txt']=[[ ['hack/docs/docs/tools/nocommand.txt']=[[
nocommand nocommand
********* =========
**Tags:** fort | armok | Tags: fort | armok | units
units
Documented nocommand. Documented nocommand.
Documented full help. Documented full help.
]], ]],
['hack/docs/docs/tools/basic.txt']=[[ ['hack/docs/docs/tools/basic.txt']=[[
basic basic
***** =====
**Tags:** map Tags: map
**Command:** "basic" Command: "basic"
Documented basic. Documented basic.
Documented full help. Documented full help.
]], ]],
['hack/docs/docs/tools/subdir/scriptname.txt']=[[ ['hack/docs/docs/tools/subdir/scriptname.txt']=[[
subdir/scriptname subdir/scriptname
***************** =================
**Tags:** map Tags: map
**Command:** "subdir/scriptname" Command: "subdir/scriptname"
Documented subdir/scriptname. Documented subdir/scriptname.
Documented full help. Documented full help.
]],
['hack/docs/docs/tools/dev_script.txt']=[[
dev_script
==========
Tags: dev
Command: "dev_script"
Short desc.
Full help.
]====]
script contents
]], ]],
['scripts/scriptpath/basic.lua']=[[ ['scripts/scriptpath/basic.lua']=[[
-- in-file short description for basic -- in-file short description for basic
@ -126,11 +137,11 @@ Documented full help.
basic basic
===== =====
**Tags:** map Tags: map
**Command:** "basic" Command: "basic"
in-file basic. in-file basic.
Documented full help. Documented full help.
]====] ]====]
@ -142,11 +153,11 @@ script contents
subdir/scriptname subdir/scriptname
================= =================
**Tags:** map Tags: map
**Command:** "subdir/scriptname" Command: "subdir/scriptname"
in-file scriptname. in-file scriptname.
Documented full help. Documented full help.
]====] ]====]
@ -158,11 +169,11 @@ script contents
inscript_docs inscript_docs
============= =============
**Tags:** map | badtag Tags: map | badtag
**Command:** "inscript_docs" Command: "inscript_docs"
in-file inscript_docs. in-file inscript_docs.
Documented full help. Documented full help.
]====] ]====]
@ -182,11 +193,11 @@ script contents
basic basic
===== =====
**Tags:** map Tags: map
**Command:** "basic" Command: "basic"
in-file basic (other). in-file basic (other).
Documented full help. Documented full help.
]====] ]====]
@ -198,11 +209,11 @@ script contents
subdir/scriptname subdir/scriptname
================= =================
**Tags:** map Tags: map
**Command:** "subdir/scriptname" Command: "subdir/scriptname"
in-file scriptname (other). in-file scriptname (other).
Documented full help. Documented full help.
]====] ]====]
@ -214,14 +225,17 @@ script contents
inscript_docs inscript_docs
============= =============
**Tags:** map Tags: map
**Command:** "inscript_docs" Command: "inscript_docs"
in-file inscript_docs (other). in-file inscript_docs (other).
Documented full help. Documented full help.
]====] ]====]
script contents
]],
['other/scriptpath/dev_script.lua']=[[
script contents script contents
]], ]],
} }
@ -400,15 +414,15 @@ end
function test.get_entry_long_help() function test.get_entry_long_help()
local expected = [[ local expected = [[
basic basic
***** =====
**Tags:** map Tags: map
**Command:** Command:
"basic" "basic"
Documented Documented
basic. basic.
Documented Documented
full help. full help.
@ -432,34 +446,34 @@ full help.
-- plugins/commands that have no doc files get the default template -- plugins/commands that have no doc files get the default template
expect.eq([[ls expect.eq([[ls
** ==
No help available. No help available.
]], h.get_entry_long_help('ls')) ]], h.get_entry_long_help('ls'))
expect.eq([[nodocs_hascommands expect.eq([[nodocs_hascommands
****************** ==================
No help available. No help available.
]], h.get_entry_long_help('nodocs_hascommands')) ]], h.get_entry_long_help('nodocs_hascommands'))
expect.eq([[nodocs_hascommands expect.eq([[nodocs_hascommands
****************** ==================
No help available. No help available.
]], h.get_entry_long_help('nodoc_command')) ]], h.get_entry_long_help('nodoc_command'))
expect.eq([[Nodocs samename. expect.eq([[Nodocs samename.
This command has the same name as its host plugin but no rst docs.]], h.get_entry_long_help('nodocs_samename')) This command has the same name as its host plugin but no rst docs.]], h.get_entry_long_help('nodocs_samename'))
expect.eq([[nodocs_nocommand expect.eq([[nodocs_nocommand
**************** ================
No help available. No help available.
]], h.get_entry_long_help('nodocs_nocommand')) ]], h.get_entry_long_help('nodocs_nocommand'))
expect.eq([[nodocs_script expect.eq([[nodocs_script
************* =============
No help available. No help available.
]], h.get_entry_long_help('nodocs_script')) ]], h.get_entry_long_help('nodocs_script'))
expect.eq([[inscript_short_only expect.eq([[inscript_short_only
******************* ===================
No help available. No help available.
]], h.get_entry_long_help('inscript_short_only')) ]], h.get_entry_long_help('inscript_short_only'))
@ -468,11 +482,11 @@ No help available.
expect.eq([[inscript_docs expect.eq([[inscript_docs
============= =============
**Tags:** map | badtag Tags: map | badtag
**Command:** "inscript_docs" Command: "inscript_docs"
in-file inscript_docs. in-file inscript_docs.
Documented full help.]], h.get_entry_long_help('inscript_docs')) Documented full help.]], h.get_entry_long_help('inscript_docs'))
end end
@ -501,7 +515,7 @@ function test.is_tag()
end end
function test.get_tags() function test.get_tags()
expect.table_eq({'armok', 'fort', 'map', 'nomembers', 'units'}, expect.table_eq({'armok', 'dev', 'fort', 'map', 'nomembers', 'units'},
h.get_tags()) h.get_tags())
end end
@ -540,8 +554,8 @@ end
function test.search_entries() function test.search_entries()
-- all entries, in alphabetical order by last path component -- all entries, in alphabetical order by last path component
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
'fpause', 'hascommands', 'help', 'hide', 'inscript_docs', 'enable', 'fpause', 'hascommands', 'help', 'hide', 'inscript_docs',
'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man',
'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand', 'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
@ -561,19 +575,26 @@ function test.search_entries()
expect.table_eq(expected, h.search_entries({str='script', expect.table_eq(expected, h.search_entries({str='script',
entry_type='builtin'})) entry_type='builtin'}))
expected = {'inscript_docs', 'inscript_short_only','nodocs_script', expected = {'dev_script', 'inscript_docs', 'inscript_short_only',
'subdir/scriptname'} 'nodocs_script', 'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({str='script'}, expect.table_eq(expected, h.search_entries({str='script'},
{entry_type='builtin'})) {entry_type='builtin'}))
expected = {'bindboxers', 'boxbinders'} expected = {'bindboxers', 'boxbinders'}
expect.table_eq(expected, h.search_entries({str='box'})) expect.table_eq(expected, h.search_entries({str='box'}))
expected = {'bindboxers', 'boxbinders', 'inscript_docs',
'inscript_short_only', 'nodocs_script', 'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({{str='script'}, {str='box'}},
{{entry_type='builtin'},
{tag='dev'}}),
'multiple filters for include and exclude')
end end
function test.get_commands() function test.get_commands()
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders', local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable', 'clear', 'cls', 'dev_script', 'die', 'dir', 'disable', 'devel/dump-rpc',
'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only', 'enable', 'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only',
'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command', 'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename', 'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type', 'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
@ -604,21 +625,23 @@ function test.tags()
local mock_print = mock.func() local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function() mock.patch(h, 'print', mock_print, function()
h.tags() h.tags()
expect.eq(7, mock_print.call_count) expect.eq(8, mock_print.call_count)
expect.eq('armok Tools that give you complete control over an aspect of the', expect.eq('armok Tools that give you complete control over an aspect of the',
mock_print.call_args[1][1]) mock_print.call_args[1][1])
expect.eq(' game or provide access to information that the game', expect.eq(' game or provide access to information that the game',
mock_print.call_args[2][1]) mock_print.call_args[2][1])
expect.eq(' intentionally keeps hidden.', expect.eq(' intentionally keeps hidden.',
mock_print.call_args[3][1]) mock_print.call_args[3][1])
expect.eq('fort Tools that are useful while in fort mode.', expect.eq('dev Dev tools.',
mock_print.call_args[4][1]) mock_print.call_args[4][1])
expect.eq('map Tools that interact with the game map.', expect.eq('fort Tools that are useful while in fort mode.',
mock_print.call_args[5][1]) mock_print.call_args[5][1])
expect.eq('nomembers Nothing is tagged with this.', expect.eq('map Tools that interact with the game map.',
mock_print.call_args[6][1]) mock_print.call_args[6][1])
expect.eq('units Tools that interact with units.', expect.eq('nomembers Nothing is tagged with this.',
mock_print.call_args[7][1]) mock_print.call_args[7][1])
expect.eq('units Tools that interact with units.',
mock_print.call_args[8][1])
end) end)
end end
@ -643,7 +666,7 @@ function test.ls()
expect.eq(5, mock_print.call_count) expect.eq(5, mock_print.call_count)
expect.eq('inscript_docs in-file short description for inscript_docs.', expect.eq('inscript_docs in-file short description for inscript_docs.',
mock_print.call_args[1][1]) mock_print.call_args[1][1])
expect.eq(' tags: map', mock_print.call_args[2][1]) expect.eq(' tags: map', mock_print.call_args[2][1])
expect.eq('nodoc_command cpp description.', expect.eq('nodoc_command cpp description.',
mock_print.call_args[3][1]) mock_print.call_args[3][1])
expect.eq('nodocs_samename Nodocs samename.', expect.eq('nodocs_samename Nodocs samename.',
@ -658,15 +681,15 @@ function test.ls()
expect.eq(6, mock_print.call_count) expect.eq(6, mock_print.call_count)
expect.eq('bindboxers Bind your boxers.', expect.eq('bindboxers Bind your boxers.',
mock_print.call_args[1][1]) mock_print.call_args[1][1])
expect.eq(' tags: armok, fort, units', expect.eq(' tags: armok, fort, units',
mock_print.call_args[2][1]) mock_print.call_args[2][1])
expect.eq('boxbinders Box your binders.', expect.eq('boxbinders Box your binders.',
mock_print.call_args[3][1]) mock_print.call_args[3][1])
expect.eq(' tags: armok, fort, units', expect.eq(' tags: armok, fort, units',
mock_print.call_args[4][1]) mock_print.call_args[4][1])
expect.eq('samename Samename.', expect.eq('samename Samename.',
mock_print.call_args[5][1]) mock_print.call_args[5][1])
expect.eq(' tags: armok, fort, units', expect.eq(' tags: armok, fort, units',
mock_print.call_args[6][1]) mock_print.call_args[6][1])
end) end)
@ -676,4 +699,29 @@ function test.ls()
expect.eq(1, mock_print.call_count) expect.eq(1, mock_print.call_count)
expect.eq('No matches.', mock_print.call_args[1][1]) expect.eq('No matches.', mock_print.call_args[1][1])
end) end)
-- test skipping tags and excluding strings
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('armok', true, false, 'boxer,binder')
expect.eq(1, mock_print.call_count)
expect.eq('samename Samename.', mock_print.call_args[1][1])
end)
-- test excluding dev scripts
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('_script', true, false, 'inscript,nodocs')
expect.eq(1, mock_print.call_count)
expect.eq('No matches.', mock_print.call_args[1][1])
end)
-- test including dev scripts
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('_script', true, true, 'inscript,nodocs')
expect.eq(1, mock_print.call_count)
expect.eq('dev_script Short desc.',
mock_print.call_args[1][1])
end)
end end

@ -35,6 +35,12 @@ function test.parse_gui_commandline()
name='blueprint', engrave=true,}, name='blueprint', engrave=true,},
opts) opts)
opts = {}
b.parse_gui_commandline(opts, {'--smooth'})
expect.table_eq({auto_phase=true, format='minimal', split_strategy='none',
name='blueprint', smooth=true,},
opts)
opts = {} opts = {}
b.parse_gui_commandline(opts, {'--engrave'}) b.parse_gui_commandline(opts, {'--engrave'})
expect.table_eq({auto_phase=true, format='minimal', split_strategy='none', expect.table_eq({auto_phase=true, format='minimal', split_strategy='none',