diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 125b1e931..25f043624 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.17.1 + rev: 0.18.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.0 + rev: v1.3.1 hooks: - id: forbid-tabs exclude_types: diff --git a/conf.py b/conf.py index 924da1577..68d11f0f9 100644 --- a/conf.py +++ b/conf.py @@ -347,6 +347,10 @@ latex_toplevel_sectioning = 'part' from sphinx.writers import text -text.MAXWIDTH = 52 +# this value is arbitrary. it just needs to be bigger than the number of +# characters in the longest paragraph in the DFHack docs +text.MAXWIDTH = 1000000000 +# this is the order that section headers will use the characters for underlines +# they are in the order of (subjective) text-mode readability text_sectionchars = '=-~`+"*' diff --git a/data/blueprints/library/dreamfort.csv b/data/blueprints/library/dreamfort.csv index 72e2161fe..e26650943 100644 --- a/data/blueprints/library/dreamfort.csv +++ b/data/blueprints/library/dreamfort.csv @@ -865,8 +865,8 @@ Feel free to assign an unimportant animal to the pasture in the main entranceway ,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,`,~,,,,,,~,`,,,,,,,Cf,Cf,`,,` ,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,~,Cf,Cf,Cf,Cf,Cf,~,~,,,,,,,,Cf,`,,` ,,,`,,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,~,,~,~,~,,~,~,,,,,Cf,Cf,,Cf,`,,` -,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,,Cf,,~,~,~,,Cf,,,,,,,,,Cf,`,,` -,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,`,,,~,~,~,,,`,,,,,,,Cf,Cf,`,,` +,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,,Cf,,~,~,~,,Cf,,,Cf,,,,,,Cf,`,,` +,,,`,,`,,Cf,,Cf,Cf,Cf,Cf,Cf,`,,,~,~,~,,,`,,Cf,,,,,Cf,Cf,`,,` ,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,` ,,,`,Cf,,,,,,,,,Cf,,,,,~,,,,,Cf,,,,,,,,,Cf,` ,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,` @@ -1143,8 +1143,8 @@ t1(37x33) ,,,`,,`,~,~,~,~,~,~,~,~,`,~,~,~,~,~,~,~,`,Cf,Cf,Cf,Cf,Cf,Cf,~,~,`,,` ,,,`,,`,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,~,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,`,,` ,,,`,,`,~,~,~,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,Cf,Cf,Cf,~,~,Cf,~,`,,` -,,,`,,`,Cf,~,Cf,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,Cf,Cf,Cf,Cf,Cf,Cf,~,`,,` -,,,`,,`,Cf,~,Cf,~,~,~,~,~,`,Cf,Cf,~,~,~,Cf,Cf,`,Cf,Cf,Cf,Cf,Cf,Cf,~,~,`,,` +,,,`,,`,Cf,~,Cf,~,~,~,~,~,~,~,Cf,~,~,~,Cf,~,~,Cf,~,Cf,Cf,Cf,Cf,Cf,~,`,,` +,,,`,,`,Cf,~,Cf,~,~,~,~,~,`,Cf,Cf,~,~,~,Cf,Cf,`,Cf,~,Cf,Cf,Cf,Cf,~,~,`,,` ,,,`,,`,`,`,`,`,`,`,`,`,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,`,`,`,`,`,`,`,`,`,`,,` ,,,`,~,,,,,,,,,,,Cf,Cf,Cf,~,Cf,Cf,Cf,,,,,,,,,,,~,` ,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,` diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 85d010c84..2ee706b09 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -72,6 +72,9 @@ keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity # re-check manager orders keybinding add Alt-R@jobmanagement/Main workorder-recheck +# workorder detail configuration +keybinding add D@workquota_details gui/workorder-details + # view combat reports for the selected unit/corpse/spatter keybinding add Ctrl-Shift-R view-unit-reports diff --git a/depends/clsocket b/depends/clsocket index ae19aebd7..6ed8aa464 160000 --- a/depends/clsocket +++ b/depends/clsocket @@ -1 +1 @@ -Subproject commit ae19aebd795d6d91803e60f46de037b604593cb4 +Subproject commit 6ed8aa46462ea01a1122fc49422840a2facc9757 diff --git a/dfhack-config/dwarfmonitor.json b/dfhack-config/dwarfmonitor.json index 3fd365e74..007dad020 100644 --- a/dfhack-config/dwarfmonitor.json +++ b/dfhack-config/dwarfmonitor.json @@ -2,7 +2,7 @@ "widgets": [ { "type": "weather", - "x": 1, + "x": 22, "y": -1 }, { diff --git a/docs/Authors.rst b/docs/Authors.rst index 2369d393d..280594674 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -1,4 +1,4 @@ -List of Authors +List of authors =============== The following is a list of people who have contributed to DFHack, in alphabetical order. diff --git a/docs/Compile.rst b/docs/Compile.rst index 85ec50344..0f3880816 100644 --- a/docs/Compile.rst +++ b/docs/Compile.rst @@ -40,8 +40,8 @@ This will check out the code on the default branch of the GitHub repo, currently ``develop``, which may be unstable. If you want code for the latest stable release, you can check out the ``master`` branch instead:: - git checkout master - git submodule update + git checkout master + git submodule update In general, a single DFHack clone is suitable for development - most Git operations such as switching branches can be done on an existing clone. If you diff --git a/docs/Core.rst b/docs/Core.rst index 09ae2577d..e03a7fcba 100644 --- a/docs/Core.rst +++ b/docs/Core.rst @@ -9,7 +9,7 @@ DFHack Core :depth: 2 -Command Implementation +Command implementation ====================== DFHack commands can be implemented in any of three ways: @@ -28,7 +28,7 @@ DFHack commands can be implemented in any of three ways: All tools distributed with DFHack are documented `here `. -Using DFHack Commands +Using DFHack commands ===================== DFHack commands can be executed in a number of ways: @@ -38,7 +38,7 @@ DFHack commands can be executed in a number of ways: #. From one of several `init-files`, automatically #. Using `script` to run a batch of commands from a file -The DFHack Console +The DFHack console ------------------ The command line has some nice line editing capabilities, including history that's preserved between different runs of DF - use :kbd:`↑` and :kbd:`↓` @@ -115,7 +115,7 @@ second (Windows) example uses `kill-lua` to stop a Lua script. .. _dfhack-config: -Configuration Files +Configuration files =================== Most DFHack settings can be changed by modifying files in the ``dfhack-config`` @@ -125,7 +125,7 @@ necessary. .. _init-files: -Init Files +Init files ---------- .. contents:: @@ -260,7 +260,7 @@ modified programmatically at any time through the `Lua API `. .. _env-vars: -Environment Variables +Environment variables ===================== DFHack's behavior can be adjusted with some environment variables. For example, @@ -306,7 +306,7 @@ Other (non-DFHack-specific) variables that affect DFHack: sensitive), ``DF2CONSOLE()`` will produce UTF-8-encoded text. Note that this should be the case in most UTF-8-capable \*nix terminal emulators already. -Miscellaneous Notes +Miscellaneous notes =================== This section is for odd but important notes that don't fit anywhere else. diff --git a/docs/Documentation.rst b/docs/Documentation.rst index fccf59867..13ae58dca 100644 --- a/docs/Documentation.rst +++ b/docs/Documentation.rst @@ -1,14 +1,14 @@ .. _documentation: ########################### -DFHack Documentation System +DFHack documentation system ########################### DFHack documentation, like the file you are reading now, is created as a set of ``.rst`` files in `reStructuredText (reST) `_ format. This is a documentation format common in the Python community. It is very -similar in concept - and in syntax - to Markdown, as found on GitHub and many other +similar in concept -- and in syntax -- to Markdown, as found on GitHub and many other places. However it is more advanced than Markdown, with more features available when compiled to HTML, such as automatic tables of contents, cross-linking, special external links (forum, wiki, etc) and more. The documentation is compiled by a @@ -16,18 +16,20 @@ Python tool named `Sphinx `_. The DFHack build process will compile and install the documentation so it can be displayed in-game by the `help` and `ls` commands (and any other command or GUI that -displays help text), but this is disabled by default due to the additional Python and -Sphinx requirements. If you already have a version of the docs installed (say from a -downloaded release binary), then you only need to build the docs if you're changing them -and want to see the changes reflected in your game. +displays help text), but documentation compilation is disabled by default due to the +additional Python and Sphinx requirements. If you already have a version of the docs +installed (say from a downloaded release binary), then you only need to build the docs +if you're changing them and want to see the changes reflected in your game. You can also build the docs if you just want a local HTML- or text-rendered copy, though you can always read the `online version `_ too. +The active development version of the documentation is tagged with ``latest`` and +is available `here `_ -(Note that even if you do want a local copy, it is certainly not necessary to +Note that even if you do want a local copy, it is certainly not necessary to compile the documentation in order to read it. Like Markdown, reST documents are designed to be just as readable in a plain-text editor as they are in HTML format. -The main thing you lose in plain text format is hyperlinking.) +The main thing you lose in plain text format is hyperlinking. .. contents:: Contents :local: @@ -39,22 +41,20 @@ The source ``.rst`` files are compiled to HTML for viewing in a browser and to t format for viewing in-game. For in-game help, the help text is read from its installed location in ``hack/docs`` under the DF directory. -When writing documentation, remember that everything should be documented! If it's not +When writing documentation, remember that everything should be documented! If it's not clear *where* a particular thing should be documented, ask on Discord or in the DFHack thread on Bay12 -- you'll not only be getting help, you'll also be providing valuable -feedback that makes it easier for future contributers to find documentation on how to +feedback that makes it easier for future contributors to find documentation on how to write the documentation! -Try to keep lines within 80-100 characters, so it's readable in plain text -in the terminal - Sphinx (our documentation system) will make sure -paragraphs flow. +Try to keep lines within 80-100 characters so it's readable in plain text in the +terminal - Sphinx (our documentation system) will make sure paragraphs flow. Short descriptions ------------------ -Each command that a user can run, as well as every plugin that can be enabled for some -lasting effect, needs to have a short (~54 character) descriptive string associated with -it. This description text is: +Each command that a user can run -- as well as every plugin -- needs to have a +short (~54 character) descriptive string associated with it. This description text is: - used in-game by the `ls` command and DFHack UI screens that list commands - used in the generated index entries in the HTML docs @@ -114,10 +114,9 @@ in a file named ``docs/gui/foobar.rst`` in the scripts repo. Similarly, a plugin ``foobaz`` should be documented in a file named ``docs/plugins/foobaz.rst`` in the dfhack repo. For plugins, all commands provided by that plugin should be documented in that same file. -Short descriptions (the ~54 character short help) are taken from the first "sentence" of -the help text for scripts and plugins that can be enabled. This means that the help should -begin with a sentence fragment that begins with a capital letter and ends in a full stop -(``.``). Please make this brief but descriptive! +Short descriptions (the ~54 character short help) for scripts and plugins are taken from +the ``summary`` attribute of the ``dfhack-tool`` directive that each tool help document must +have (see the `Header format`_ section below). Please make this brief but descriptive! Short descriptions for commands provided by plugins are taken from the ``description`` parameter passed to the ``PluginCommand`` constructor used when the command is registered @@ -127,11 +126,11 @@ Header format ------------- The docs **must** begin with a heading which exactly matches the script or plugin name, underlined -with ``=====`` to the same length. This should be followed by a ``.. dfhack-tool:`` directive with +with ``=====`` to the same length. This must be followed by a ``.. dfhack-tool:`` directive with at least the following parameters: * ``:summary:`` - a short, single-sentence description of the tool -* ``:tags:`` - a space-separated list of tags that apply to the tool +* ``:tags:`` - a space-separated list of `tags ` that apply to the tool By default, ``dfhack-tool`` generates both a description of a tool and a command with the same name. For tools (specifically plugins) that do not provide exactly @@ -183,11 +182,14 @@ And documentation for the ``autodump`` plugin might look like:: Usage help ---------- -The first section after the header and introductory text should be the usage block. You can +The first section after the header and introductory text should be the usage section. You can choose between two formats, based on whatever is cleaner or clearer for your syntax. The first option is to show usage formats together, with an explanation following the block:: - Usage:: + Usage + ----- + + :: build-now [] build-now here [] @@ -206,7 +208,8 @@ option is to show usage formats together, with an explanation following the bloc The second option is to arrange the usage options in a list, with the full command and arguments in monospaced font. Then indent the next line and describe the effect:: - Usage: + Usage + ----- ``build-now []`` Scan the entire map and build all unsuspended constructions @@ -224,6 +227,10 @@ Note that in both options, the entire commandline syntax is written, including t Literal text is written as-is (e.g. the word ``here`` in the above example), and text that describes the kind of parameter that is being passed (e.g. ``pos`` or ``options``) is enclosed in angle brackets (``<`` and ``>``). Optional elements are enclosed in square brackets (``[`` and ``]``). +If the command takes an arbitrary number of elements, use ``...``, for example:: + + prioritize [] [ ...] + quickfort [,...] [,...] [] Examples -------- @@ -233,11 +240,11 @@ Otherwise, please consider adding a section that shows some real, practical usag many users, this will be the **only** section they will read. It is so important that it is a good idea to include the ``Examples`` section **before** you describe any extended options your command might take. Write examples for what you expect the popular use cases will be. Also be sure to write -examples showing specific, practical values being used for any parameter that takes a value. +examples showing specific, practical values being used for any parameter that takes a value or has +tricky formatting. -Examples should go in their own subheading with a single dash underline (``--------``). The examples -themselves should be organized in a list, the same as in option 2 for Usage above. Here is an -example Examples section:: +Examples should go in their own subheading. The examples themselves should be organized as in +option 2 for Usage above. Here is an example ``Examples`` section:: Examples -------- @@ -251,7 +258,7 @@ example Examples section:: Options ------- -The options header should follow the examples, with each option in the same list format as the +The options header should follow the examples, with each option in the same format as the examples:: Options @@ -278,7 +285,8 @@ scripts and plugins can use a different mechanism to at least make their help te in-game. Note that since help text for external scripts and plugins is not rendered by Sphinx, -it should be written in plain text. Any reStructuredText markup will not be processed. +it should be written in plain text. Any reStructuredText markup will not be processed and, +if present, will be shown verbatim to the player (which is probably not what you want). For external scripts, the short description comes from a comment on the first line (the comment marker and extra whitespace is stripped). For Lua, this would look like: @@ -303,27 +311,33 @@ entire script header:: -- [====[ gui/adv-inventory ================= - Tags: adventure, items + + Tags: adventure | items Allows you to quickly move items between containers. This includes yourself and any followers you have. - Usage: + Usage + ----- gui/adv-inventory [] - Examples: + Examples + -------- gui/adv-inventory Opens the GUI with nothing preselected + gui/adv-inventory take-all Opens the GUI with all container items already selected and ready to move into the adventurer's inventory. - Options: + Options + ------- take-all Starts the GUI with container items pre-selected + give-all Starts the GUI with your own items pre-selected ]====] @@ -340,10 +354,10 @@ Required dependencies .. highlight:: shell In order to build the documentation, you must have Python with Sphinx -version |sphinx_min_version| or later. Python 3 is recommended. +version |sphinx_min_version| or later and Python 3. When installing Sphinx from OS package managers, be aware that there is -another program called Sphinx, completely unrelated to documentation management. +another program called "Sphinx", completely unrelated to documentation management. Be sure you are installing the right Sphinx; it may be called ``python-sphinx``, for example. To avoid doubt, ``pip`` can be used instead as detailed below. @@ -357,13 +371,12 @@ For more detailed platform-specific instructions, see the sections below: :local: :backlinks: none - Linux ----- Most Linux distributions will include Python by default. If not, start by -installing Python (preferably Python 3). On Debian-based distros:: +installing Python 3. On Debian-based distros:: - sudo apt install python3 + sudo apt install python3 Check your package manager to see if Sphinx |sphinx_min_version| or later is available. On Debian-based distros, this package is named ``python3-sphinx``. @@ -372,11 +385,11 @@ want to use a newer Sphinx version (which may result in faster builds), you can install Sphinx through the ``pip`` package manager instead. On Debian-based distros, you can install pip with:: - sudo apt install python3-pip + sudo apt install python3-pip Once pip is available, you can then install Sphinx with:: - pip3 install sphinx + pip3 install sphinx If you run this as an unprivileged user, it may install a local copy of Sphinx for your user only. The ``sphinx-build`` executable will typically end up in @@ -391,34 +404,23 @@ macOS has Python 2.7 installed by default, but it does not have the pip package You can install Homebrew's Python 3, which includes pip, and then install the latest Sphinx using pip:: - brew install python3 - pip3 install sphinx - -Alternatively, you can simply install Sphinx directly from Homebrew:: - - brew install sphinx-doc - -This will install Sphinx for macOS's system Python 2.7, without needing pip. - -Either method works; if you plan to use Python for other purposes, it might best -to install Homebrew's Python 3 so that you have the latest Python as well as pip. -If not, just installing sphinx-doc for macOS's system Python 2.7 is fine. - + brew install python3 + pip3 install sphinx Windows ------- Python for Windows can be downloaded `from python.org `_. -The latest version of Python 3 is recommended, as it includes pip already. +The latest version of Python 3 includes pip already. You can also install Python and pip through the Chocolatey package manager. After installing Chocolatey as outlined in the `Windows compilation instructions `, run the following command from an elevated (admin) command prompt (e.g. ``cmd.exe``):: - choco install python pip -y + choco install python pip -y Once you have pip available, you can install Sphinx with the following command:: - pip install sphinx + pip install sphinx Note that this may require opening a new (admin) command prompt if you just installed pip from the same command prompt. @@ -458,8 +460,9 @@ ways to do this: By default, both HTML and text docs are built by CMake. The generated documentation is stored in ``docs/html`` and ``docs/text`` (respectively) in the -root DFHack folder, and will be installed to ``hack/docs`` when you install -DFHack. +root DFHack folder, and they will both be installed to ``hack/docs`` when you +install DFHack. The html and txt files will intermingle, but will not interfere +with one another. Running Sphinx manually ----------------------- @@ -467,8 +470,8 @@ Running Sphinx manually You can also build the documentation without running CMake - this is faster if you only want to rebuild the documentation regardless of any code changes. The ``docs/build.py`` script will build the documentation in any specified formats -(HTML only by default) using essentially the same command that CMake runs when -building the docs. Run the script with ``--help`` to see additional options. +(HTML only by default) using the same command that CMake runs when building the +docs. Run the script with ``--help`` to see additional options. Examples: @@ -494,7 +497,9 @@ or, to build plain-text output:: Sphinx has many options to enable clean builds, parallel builds, logging, and more - run ``sphinx-build --help`` for details. If you specify a different output path, be warned that Sphinx may overwrite existing files in the output -folder. +folder. Also be aware that when running ``sphinx-build`` directly, the +``docs/html`` folder may be polluted with intermediate build files that normally +get written in the cmake ``build`` directory. Building a PDF version ---------------------- @@ -539,7 +544,6 @@ closest stable release after 0.44.05-alpha1). An entry listed under a stable release like "0.44.05-r1" in changelog.txt will be listed under that release in both the stable changelog and the development changelog. - Changelog syntax ---------------- diff --git a/docs/Introduction.rst b/docs/Introduction.rst index ad24f8f43..d90983d8c 100644 --- a/docs/Introduction.rst +++ b/docs/Introduction.rst @@ -1,7 +1,7 @@ .. _introduction: ######################### -Introduction and Overview +Introduction and overview ######################### DFHack is a Dwarf Fortress memory access library, distributed with diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 942ccfa8f..64ccdd595 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -30,6 +30,7 @@ implemented by Lua files located in :file:`hack/lua/*` :local: :depth: 2 +.. _lua-df: ========================= DF data structure wrapper @@ -928,9 +929,9 @@ can be omitted. The following examples are equivalent:: - dfhack.run_command({'ls', '-a'}) - dfhack.run_command('ls', '-a') - dfhack.run_command('ls -a') -- not recommended + dfhack.run_command({'ls', 'quick'}) + dfhack.run_command('ls', 'quick') + dfhack.run_command('ls quick') -- not recommended * ``dfhack.run_command_silent(command[, ...])`` @@ -1087,6 +1088,11 @@ Announcements Uses the type to look up options from announcements.txt, and calls the above operations accordingly. The units are used to call ``addCombatReportAuto``. +* ``dfhack.gui.getMousePos()`` + + Returns the map coordinates of the map tile the mouse is over as a table of + ``{x, y, z}``. If the cursor is not over the map, returns ``nil``. + Other ~~~~~ @@ -1992,6 +1998,12 @@ Functions: Returns: *tile, tile_grayscale*, or *nil* if not found. The values can then be used for the *tile* field of *pen* structures. +* ``dfhack.screen.hideGuard(screen,callback[,args...])`` + + Removes screen from the viewscreen stack, calls the callback (with optional + supplied arguments), and then restores the screen on the top of the viewscreen + stack. + * ``dfhack.screen.clear()`` Fills the screen with blank background. @@ -3108,9 +3120,11 @@ Each entry has several properties associated with it: Returns the short (~54 character) description for the given entry. -* ``helpdb.get_entry_long_help(entry)`` +* ``helpdb.get_entry_long_help(entry[, width])`` - Returns the full help text for the given entry. + Returns the full help text for the given entry. If ``width`` is specified, the + text will be wrapped at that width, preserving block indents. The wrap width + defaults to 80. * ``helpdb.get_entry_tags(entry)`` @@ -4031,13 +4045,17 @@ It has the following attributes: keys to the number of lines to scroll as positive or negative integers or one of the keywords supported by the ``scroll`` method. The default is up/down arrows scrolling by one line and page up/down scrolling by one page. -:show_scroll_icons: Controls scroll icons' behaviour: ``false`` for no icons, ``'right'`` or ``'left'`` for +:show_scrollbar: Controls scrollbar display: ``false`` for no scrollbar, ``'right'`` or ``'left'`` for icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``), ``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary (if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``. -:up_arrow_icon: The symbol for scroll up arrow. Default is ``string.char(24)`` (``↑``). -:down_arrow_icon: The symbol for scroll down arrow. Default is ``string.char(25)`` (``↓``). -:scroll_icon_pen: Specifies the pen for scroll icons. Default is ``COLOR_LIGHTCYAN``. +:scrollbar_fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN`` (the same as the native DF help screens). +:scrollbar_bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN`` (the same as the native DF help screens). + +If the scrollbar is shown, it will react to mouse clicks on the scrollbar itself. +Clicking on the arrows at the top or the bottom will scroll by one line, and +clicking on the unfilled portion of the scrollbar will scroll by a half page in +that direction. The text itself is represented as a complex structure, and passed to the object via the ``text`` argument of the constructor, or via @@ -5084,9 +5102,8 @@ the extension omitted. For example: * :file:`hack/scripts/gui/teleport.lua` is invoked as ``gui/teleport`` .. note:: - Scripts placed in subdirectories can be run as described above, but are not - listed by the `ls` command unless ``-a`` is specified. In general, scripts - should be placed in subfolders in the following situations: + In general, scripts should be placed in subfolders in the following + situations: * ``devel``: scripts that are intended exclusively for DFHack development, including examples, or scripts that are experimental and unstable @@ -5104,16 +5121,12 @@ folders can be added (for example, a copy of the :source-scripts:`scripts repository <>` for local development). See `script-paths` for more information on how to configure this behavior. -If the first line of the script is a one-line comment (starting with ``--``), -the content of the comment is used by the built-in ``ls`` and ``help`` commands. -Such a comment is required for every script in the official DFHack repository. - Scripts are read from disk when run for the first time, or if they have changed since the last time they were run. Each script has an isolated environment where global variables set by the script are stored. Values of globals persist across script runs in the same DF session. -See `devel/lua-example` for an example of this behavior. Note that local +See `devel/lua-example` for an example of this behavior. Note that ``local`` variables do *not* persist. Arguments are passed in to the scripts via the ``...`` built-in quasi-variable; @@ -5135,9 +5148,9 @@ General script API * ``dfhack.run_script(name[,args...])`` - Run a Lua script in hack/scripts/, as if it were started from the DFHack - command-line. The ``name`` argument should be the name of the script without - its extension, as it would be used on the command line. + Run a Lua script in :file:`hack/scripts/`, as if it were started from the + DFHack command-line. The ``name`` argument should be the name of the script + without its extension, as it would be used on the command line. Example: @@ -5157,10 +5170,10 @@ General script API * ``dfhack.script_help([name, [extension]])`` - Returns the contents of the embedded documentation of the specified script. - ``extension`` defaults to "lua", and ``name`` defaults to the name of the - script where this function was called. For example, the following can be used - to print the current script's help text:: + Returns the contents of the rendered (or embedded) `documentation` for the + specified script. ``extension`` defaults to "lua", and ``name`` defaults to + the name of the script where this function was called. For example, the + following can be used to print the current script's help text:: local args = {...} if args[1] == 'help' then @@ -5168,6 +5181,7 @@ General script API return end +.. _reqscript: Importing scripts ================= @@ -5222,12 +5236,12 @@ Importing scripts .. warning:: Avoid caching the table returned by ``reqscript()`` beyond storing it in - a local or global variable as in the example above. ``reqscript()`` is fast - for scripts that have previously been loaded and haven't changed. If you - retain a reference to a table returned by an old ``reqscript()`` call, this - may lead to unintended behavior if the location of the script changes - (e.g. if a save is loaded or unloaded, or if a `script path ` - is added in some other way). + a local variable as in the example above. ``reqscript()`` is fast for + scripts that have previously been loaded and haven't changed. If you retain + a reference to a table returned by an old ``reqscript()`` call, this may + lead to unintended behavior if the location of the script changes (e.g. if a + save is loaded or unloaded, or if a `script path ` is added in + some other way). .. admonition:: Tip diff --git a/docs/NEWS-dev.rst b/docs/NEWS-dev.rst index f77737176..f77b1b4f5 100644 --- a/docs/NEWS-dev.rst +++ b/docs/NEWS-dev.rst @@ -5,7 +5,7 @@ .. _dev-changelog: ##################### -Development Changelog +Development changelog ##################### This file contains changes grouped by the release (stable or development) in diff --git a/docs/Remote.rst b/docs/Remote.rst index de741f66a..6f276cdb6 100644 --- a/docs/Remote.rst +++ b/docs/Remote.rst @@ -1,7 +1,7 @@ .. _remote: ======================= -DFHack Remote Interface +DFHack remote interface ======================= DFHack provides a remote access interface that external tools can connect to and @@ -103,8 +103,6 @@ ID Method Input Output 1 RunCommand dfproto.CoreRunCommandRequest dfproto.EmptyMessage === ============ =============================== ======================= - - Conversation flow ----------------- diff --git a/docs/Removed.rst b/docs/Removed.rst index c39e0c370..f9bf1c62e 100644 --- a/docs/Removed.rst +++ b/docs/Removed.rst @@ -62,6 +62,30 @@ fix/build-location ================== The corresponding DF :bug:`5991` was fixed in DF 0.40.05. +.. _fix/diplomats: + +fix/diplomats +============= +The corresponding DF :bug:`3295` was fixed in DF 0.40.05. + +.. _fix/fat-dwarves: + +fix/fat-dwarves +=============== +The corresponding DF :bug:`5971` was fixed in DF 0.40.05. + +.. _fix/feeding-timers: + +fix/feeding-timers +================== +The corresponding DF :bug:`2606` was fixed in DF 0.40.12. + +.. _fix/merchants: + +fix/merchants +============= +Humans can now make trade agreements. This fix is no longer necessary. + .. _fortplan: fortplan @@ -72,6 +96,14 @@ script instead. You can use your existing .csv files. Just move them to the ``blueprints`` folder in your DF installation, and instead of ``fortplan file.csv`` run ``quickfort run file.csv``. +.. _gui/assign-rack: + +gui/assign-rack +=============== +This script is no longer useful in current DF versions. The script required a +binpatch `, which has not been available since DF +0.34.11. + .. _gui/hack-wish: gui/hack-wish diff --git a/docs/api/index.rst b/docs/api/index.rst index 1fe03e9a8..6afa9d6d4 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,5 +1,5 @@ ==================== -DFHack API Reference +DFHack API reference ==================== .. toctree:: diff --git a/docs/build.py b/docs/build.py index 6a63ed59e..bfb6780b2 100755 --- a/docs/build.py +++ b/docs/build.py @@ -16,6 +16,7 @@ class SphinxOutputFormat: def args(self): output_dir = os.path.join('docs', self.name) artifacts_dir = os.path.join('build', 'docs', self.name) # for artifacts not part of the final documentation + os.makedirs(artifacts_dir, mode=0o755, exist_ok=True) return [ *self.pre_args, '.', # source dir diff --git a/docs/builtins/ls.rst b/docs/builtins/ls.rst index 6b21e4b53..7305a0256 100644 --- a/docs/builtins/ls.rst +++ b/docs/builtins/ls.rst @@ -25,11 +25,13 @@ Usage Examples -------- -- ``ls adventure`` - Lists all commands with the ``adventure`` tag. -- ``ls --dev trigger`` - Lists all commands, including developer and modding commands, that match the - substring "trigger" +``ls quick`` + List all commands that match the substring "quick". +``ls adventure`` + List all commands with the ``adventure`` tag. +``ls --dev trigger`` + List all commands, including developer and modding commands, that match the + substring "trigger". Options ------- diff --git a/docs/builtins/tags.rst b/docs/builtins/tags.rst index 985943647..698323ffe 100644 --- a/docs/builtins/tags.rst +++ b/docs/builtins/tags.rst @@ -2,15 +2,25 @@ tags ==== .. dfhack-tool:: - :summary: List the strings that DFHack tools can be tagged with. + :summary: List the categories of DFHack tools or the tools with those tags. :tags: dfhack -You can find groups of related tools by passing the tag name to the `ls` -command. +DFHack tools are labeled with tags so you can find groups of related commands. +This builtin command lists the tags that you can explore, or, if called with the +name of a tag, lists the tools that have that tag. Usage ----- -:: +``tags`` + List the categories of DFHack tools and a description of those categories. +``tags `` + List the tools that are tagged with the given tag. - tags +Examples +-------- + +``tags`` + List the defined tags. +``tags design`` + List all the tools that have the ``design`` tag. diff --git a/docs/changelog.txt b/docs/changelog.txt index af034e5d3..e1a53fe3c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins - `autonestbox`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autonestbox start`` to ``enable autonestbox``. - `autobutcher`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autobutcher start`` to ``enable autobutcher``. +- `overlay`: display a "DFHack" button in the lower left corner that you can click to start the new GUI command launcher. ## New Tweaks @@ -43,12 +44,17 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `tags`: new built-in command to list the tool category tags and their definitions. tags associated with each tool are visible in the tool help and in the output of `ls`. ## Fixes +- `autochop`: designate largest trees for chopping first, instead of the smallest +- ``dfhack.run_script``: ensure the arguments passed to scripts are always strings. This allows other scripts to call ``run_script`` with numeric args and it won't break parameter parsing. +- `dig-now`: Fix direction of smoothed walls when adjacent to a door or floodgate - ``job.removeJob()``: ensure jobs are removed from the world list when they are canceled +- `quickfort`: `Dreamfort ` blueprint set: declare the hospital zone before building the coffer; otherwise DF fails to stock the hospital with materials ## Misc Improvements - Init scripts: ``dfhack.init`` and other init scripts have moved to ``dfhack-config/init/``. If you have customized your ``dfhack.init`` file and want to keep your changes, please move the part that you have customized to the new location at ``dfhack-config/init/dfhack.init``. If you do not have changes that you want to keep, do not copy anything, and the new defaults will be used automatically. - History files: ``dfhack.history``, ``tiletypes.history``, ``lua.history``, and ``liquids.history`` have moved to the ``dfhack-config`` directory. If you'd like to keep the contents of your current history files, please move them to ``dfhack-config``. - `do-job-now`: new global keybinding for boosting the priority of the jobs associated with the selected building/work order/unit/item etc.: Alt-N +- `gui/workorder-details`: new keybinding on the workorder details screen: ``D`` - `keybinding`: support backquote (\`) as a hotkey (and assign the hotkey to the new `gui/launcher` interface) - `ls`: can now filter tools by substring or tag. note that dev scripts are hidden by default. pass the ``--dev`` option to show them. - `manipulator`: add a library of useful default professions @@ -60,10 +66,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `spectate`: ``spectate auto-unpause`` is a new feature that will auto-dismiss pause events when enabled. This does not affect the player's ability to pause at a whim. - UX: You can now move the cursor around in DFHack text fields in ``gui/`` scripts (e.g. `gui/blueprint`, `gui/quickfort`, or `gui/gm-editor`). You can move the cursor by clicking where you want it to go with the mouse or using the Left/Right arrow keys. Ctrl+Left/Right will move one word at a time, and Alt+Left/Right will move to the beginning/end of the text. - UX: You can now click on the hotkey hint text in many ``gui/`` script windows to activate the hotkey, like a button. Not all scripts have been updated to use the clickable widget yet, but you can try it in `gui/blueprint` or `gui/quickfort`. -- `quickfort`: `Dreamfort ` blueprint set improvements: set traffic designations to encourage dwarves to eat in the grand hall instead of the manager's office and to eat cooked food instead of raw ingredients +- UX: Label widget scroll icons are replaced with scrollbars that represent the percentage of text on the screen and move with the position of the visible text, just like web browser scrollbars. +- `quickfort`: `Dreamfort ` blueprint set improvements: set traffic designations to encourage dwarves to eat cooked food instead of raw ingredients ## Documentation -- Update all DFHack tool documentation with standard syntax formatting, usage examples, and overall clarified text. +- Added `modding-guide` +- Update all DFHack tool documentation (300+ pages) with standard syntax formatting, usage examples, and overall clarified text. - Group DFHack tools by `tag ` so similar tools are grouped and easy to find ## API @@ -84,8 +92,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Lua - History: added ``dfhack.getCommandHistory(history_id, history_filename)`` and ``dfhack.addCommandToHistory(history_id, history_filename, command)`` so gui scripts can access a commandline history without requiring a terminal. +- Added ``dfhack.screen.hideGuard()``: exposes the C++ ``Screen::Hide`` to Lua - ``helpdb``: database and query interface for DFHack tool help text - ``tile-material``: fix the order of declarations. The ``GetTileMat`` function now returns the material as intended (always returned nil before). Also changed the license info, with permission of the original author. +- ``utils.df_expr_to_ref()``: fixed some errors that could occur when navigating tables - ``widgets.EditField``: new ``onsubmit2`` callback attribute is called when the user hits Shift-Enter. - ``widgets.EditField``: new function: ``setCursor(position)`` sets the input cursor. - ``widgets.EditField``: new attribute: ``ignore_keys`` lets you ignore specified characters if you want to use them as hotkeys diff --git a/docs/guides/examples-guide.rst b/docs/guides/examples-guide.rst index f61ada4d7..bcb7c77ec 100644 --- a/docs/guides/examples-guide.rst +++ b/docs/guides/examples-guide.rst @@ -1,8 +1,8 @@ .. _config-examples-guide: .. _dfhack-examples-guide: -DFHack Example Configuration File Index -======================================= +DFHack config file examples +=========================== The :source:`hack/examples ` folder contains ready-to-use examples of various DFHack configuration files. You can use them by copying them @@ -14,8 +14,8 @@ The ``init/`` subfolder ----------------------- The :source:`init/ ` subfolder contains useful DFHack -`init-files` that you can copy into your main Dwarf Fortress folder -- the same -directory as ``dfhack.init``. +`init-files` that you can copy into your :file:`dfhack-config/init` folder -- +the same directory as ``dfhack.init``. .. _onMapLoad-dreamfort-init: @@ -35,7 +35,7 @@ it is useful (and customizable) for any fort. It includes the following config: - Periodically enqueues orders to shear and milk shearable and milkable pets. - Sets up `autofarm` to grow 30 units of every crop, except for pig tails, which is set to 150 units to support the textile industry. -- Sets up `seedwatch` to keep 30 of every type of seed. +- Sets up `seedwatch` to protect 30 of every type of seed. - Configures `prioritize` to automatically boost the priority of important and time-sensitive tasks that could otherwise get ignored in busy forts, like hauling food, tanning hides, storing items in vehicles, pulling levers, and diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 7208b5276..96c5688d2 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -1,11 +1,14 @@ =========== -User Guides +User guides =========== These pages are detailed guides covering DFHack tools. .. toctree:: :maxdepth: 1 - :glob: - * + /docs/guides/examples-guide + /docs/guides/modding-guide + /docs/guides/quickfort-library-guide + /docs/guides/quickfort-user-guide + /docs/guides/quickfort-alias-guide diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst new file mode 100644 index 000000000..fa0510290 --- /dev/null +++ b/docs/guides/modding-guide.rst @@ -0,0 +1,496 @@ +.. _modding-guide: + +DFHack modding guide +==================== + +.. highlight:: lua + +What is the difference between a script and a mod? +-------------------------------------------------- + +A script is a single file that can be run as a command in DFHack, like something +that modifies or displays game data on request. A mod is something you install +to get persistent behavioural changes in the game and/or add new content. Mods +can contain and use scripts in addition to (or instead of) modifications to the +DF game raws. + +DFHack scripts are written in Lua. If you don't already know Lua, there's a +great primer at `lua.org `__. + +Why not just mod the raws? +-------------------------- + +It depends on what you want to do. Some mods *are* better to do in just the +raws. You don't need DFHack to add a new race or modify attributes, for example. +However, DFHack scripts can do many things that you just can't do in the raws, +like make a creature that trails smoke. Some things *could* be done in the raws, +but writing a script is less hacky, easier to maintain, easier to extend, and is +not prone to side-effects. A great example is adding a syndrome when a reaction +is performed. If done in the raws, you have to create an exploding boulder to +apply the syndrome. DFHack scripts can add the syndrome directly and with much +more flexibility. In the end, complex mods will likely require a mix of raw +modding and DFHack scripting. + +A mod-maker's development environment +------------------------------------- + +While you're writing your mod, you need a place to store your in-development +scripts that will: + +- be directly runnable by DFHack +- not get lost when you upgrade DFHack + +The recommended approach is to create a directory somewhere outside of your DF +installation (let's call it "/path/to/own-scripts") and do all your script +development in there. + +Inside your DF installation folder, there is a file named +:file:`dfhack-config/script-paths.txt`. If you add a line like this to that +file:: + + +/path/to/own-scripts + +Then that directory will be searched when you run DFHack commands from inside +the game. The ``+`` at the front of the path means to search that directory +first, before any other script directory (like :file:`hack/scripts` or +:file:`raw/scripts`). That way, your latest changes will always be used instead +of older copies that you may have installed in a DF directory. + +For scripts with the same name, the `order of precedence ` will +be: + +1. ``own-scripts/`` +2. ``data/save/*/raw/scripts/`` +3. ``raw/scripts/`` +4. ``hack/scripts/`` + +The structure of the game +------------------------- + +"The game" is in the global variable `df `. The game's memory can be +found in ``df.global``, containing things like the list of all items, whether to +reindex pathfinding, et cetera. Also relevant to us in ``df`` are the various +types found in the game, e.g. ``df.pronoun_type`` which we will be using in this +guide. We'll explore more of the game structures below. + +Your first script +----------------- + +So! It's time to write your first script. This section will walk you through how +to make a script that will get the pronoun type of the currently selected unit. + +First line, we get the unit:: + + local unit = dfhack.gui.getSelectedUnit() + +If no unit is selected, an error message will be printed (which can be silenced +by passing ``true`` to ``getSelectedUnit``) and ``unit`` will be ``nil``. + +If ``unit`` is ``nil``, we don't want the script to run anymore:: + + if not unit then + return + end + +Now, the field ``sex`` in a unit is an integer, but each integer corresponds to +a string value ("it", "she", or "he"). We get this value by indexing the +bidirectional map ``df.pronoun_type``. Indexing the other way, incidentally, +with one of the strings, will yield its corresponding number. So:: + + local pronounTypeString = df.pronoun_type[unit.sex] + print(pronounTypeString) + +Simple. Save this as a Lua file in your own scripts directory and run it as +shown before when a unit is selected in the Dwarf Fortress UI. + +Exploring DF structures +----------------------- + +So how could you have known about the field and type we just used? Well, there +are two main tools for discovering the various fields in the game's data +structures. The first is the ``df-structures`` +`repository `__ that contains XML files +describing the contents of the game's structures. These are complete, but +difficult to read (for a human). The second option is the `gui/gm-editor` +script, an interactive data explorer. You can run the script while objects like +units are selected to view the data within them. You can also run +``gui/gm-editor scr`` to view the data for the current screen. Press :kbd:`?` +while the script is active to view help. + +Familiarising yourself with the many structs of the game will help with ideas +immensely, and you can always ask for help in the `right places `. + +Detecting triggers +------------------ + +The common method for injecting new behaviour into the game is to define a +callback function and get it called when something interesting happens. DFHack +provides two libraries for this, ``repeat-util`` and `eventful `. +``repeat-util`` is used to run a function once per a configurable number of +frames (paused or unpaused), ticks (unpaused), in-game days, months, or years. +If you need to be aware the instant something happens, you'll need to run a +check once a tick. Be careful not to do this gratuitiously, though, since +running that often can slow down the game! + +``eventful``, on the other hand, is much more performance-friendly since it will +only call your callback when a relevant event happens, like a reaction or job +being completed or a projectile moving. + +To get something to run once per tick, we can call +``repeat-util.scheduleEvery()``. First, we load the module:: + + local repeatUtil = require('repeat-util') + +Both ``repeat-util`` and ``eventful`` require keys for registered callbacks. You +should use something unique, like your mod name:: + + local modId = "callback-example-mod" + +Then, we pass the key, amount of time units between function calls, what the +time units are, and finally the callback function itself:: + + repeatUtil.scheduleEvery(modId, 1, "ticks", function() + -- Do something like iterating over all active units and + -- check for something interesting + for _, unit in ipairs(df.global.world.units.active) do + ... + end + end) + +``eventful`` is slightly more involved. First get the module:: + + local eventful = require('plugins.eventful') + +``eventful`` contains a table for each event which you populate with functions. +Each function in the table is then called with the appropriate arguments when +the event occurs. So, for example, to print the position of a moving (item) +projectile:: + + eventful.onProjItemCheckMovement[modId] = function(projectile) + print(projectile.cur_pos.x, projectile.cur_pos.y, + projectile.cur_pos.z) + end + +Check out the `full list of supported events ` to see what else +you can react to with ``eventful``. + +Now, you may have noticed that you won't be able to register multiple callbacks +with a single key named after your mod. You can, of course, call all the +functions you want from a single registed callback. Alternately, you can create +multiple callbacks using different keys, using your mod ID as a key name prefix. +If you do register multiple callbacks, though, there are no guarantees about the +call order. + +Custom raw tokens +----------------- + +.. highlight:: none + +In this section, we are going to use `custom raw tokens ` +applied to a reaction to transfer the material of a reagent to a product as a +handle improvement (like on artifact buckets), and then we are going to see how +you could make boots that make units go faster when worn. + +First, let's define a custom crossbow with its own custom reaction. The +crossbow:: + + [ITEM_WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE] + [NAME:crossbow:crossbows] + [SIZE:600] + [SKILL:HAMMER] + [RANGED:CROSSBOW:BOLT] + [SHOOT_FORCE:4000] + [SHOOT_MAXVEL:800] + [TWO_HANDED:0] + [MINIMUM_SIZE:17500] + [MATERIAL_SIZE:4] + [ATTACK:BLUNT:10000:4000:bash:bashes:NO_SUB:1250] + [ATTACK_PREPARE_AND_RECOVER:3:3] + [SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER:2] custom token (you'll see) + +The reaction to make it (you would add the reaction and not the weapon to an +entity raw):: + + [REACTION:MAKE_SIEGE_CROSSBOW] + [NAME:make siege crossbow] + [BUILDING:BOWYER:NONE] + [SKILL:BOWYER] + [REAGENT:mechanism 1:2:TRAPPARTS:NONE:NONE:NONE] + [REAGENT:bar:150:BAR:NONE:NONE:NONE] + [METAL_ITEM_MATERIAL] + [REAGENT:handle 1:1:BLOCKS:NONE:NONE:NONE] wooden handles + [ANY_PLANT_MATERIAL] + [REAGENT:handle 2:1:BLOCKS:NONE:NONE:NONE] + [ANY_PLANT_MATERIAL] + [SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT:1] + another custom token + [PRODUCT:100:1:WEAPON:ITEM_WEAPON_CROSSBOW_SIEGE:GET_MATERIAL_FROM_REAGENT:bar:NONE] + +So, we are going to use the ``eventful`` module to make it so that (after the +script is run) when this crossbow is crafted, it will have two handles, each +with the material given by the block reagents. + +.. highlight:: lua + +First, require the modules we are going to use:: + + local eventful = require("plugins.eventful") + local customRawTokens = require("custom-raw-tokens") + +Now, let's make a callback (we'll be defining the body of this function soon):: + + local modId = "siege-crossbow-mod" + eventful.onReactionComplete[modId] = function(reaction, + reactionProduct, unit, inputItems, inputReagents, + outputItems) + +First, we check to see if it the reaction that just happened is relevant to this +callback:: + + if not customRawTokens.getToken(reaction, + "SIEGE_CROSSBOW_MOD_TRANSFER_HANDLE_MATERIAL_TO_PRODUCT_IMPROVEMENT") + then + return + end + +Then, we get the product number listed. Next, for every reagent, if the reagent +name starts with "handle" then we get the corresponding item, and... + +:: + + for i, reagent in ipairs(inputReagents) do + if reagent.code:startswith('handle') then + -- Found handle reagent + local item = inputItems[i] + +...We then add a handle improvement to the listed product within our loop:: + + local new = df.itemimprovement_itemspecificst:new() + new.mat_type, new.mat_index = item.mat_type, item.mat_index + new.type = df.itemimprovement_specific_type.HANDLE + outputItems[productNumber - 1].improvements:insert('#', new) + +This works well as long as you don't have multiple stacks filling up one +reagent. + +Let's also make some code to modify the fire rate of our siege crossbow:: + + eventful.onProjItemCheckMovement[modId] = function(projectile) + if projectile.distance_flown > 0 then + -- don't make this adjustment more than once + return + end + + local firer = projectile.firer + if not firer then + return + end + + local weapon = df.item.find(projectile.bow_id) + if not weapon then + return + end + + local multiplier = tonumber(customRawTokens.getToken( + weapon.subtype, + "SIEGE_CROSSBOW_MOD_FIRE_RATE_MULTIPLIER")) or 1 + firer.counters.think_counter = math.floor( + firer.counters.think_counter * multiplier) + end + +.. highlight:: none + +Now, let's see how we could make some "pegasus boots". First, let's define the +item in the raws:: + + [ITEM_SHOES:ITEM_SHOES_BOOTS_PEGASUS] + [NAME:pegasus boot:pegasus boots] + [ARMORLEVEL:1] + [UPSTEP:1] + [METAL_ARMOR_LEVELS] + [LAYER:OVER] + [COVERAGE:100] + [LAYER_SIZE:25] + [LAYER_PERMIT:15] + [MATERIAL_SIZE:2] + [METAL] + [LEATHER] + [HARD] + [PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK:5] custom raw token + (you don't have to comment the custom token every time, + but it does clarify what it is) + +.. highlight:: lua + +Then, let's make a ``repeat-util`` callback for once a tick:: + + repeatUtil.scheduleEvery(modId, 1, "ticks", function() + +Let's iterate over every active unit, and for every unit, iterate over their +worn items to calculate how much we are going to take from their movement +timer:: + + for _, unit in ipairs(df.global.world.units.active) do + local amount = 0 + for _, entry in ipairs(unit.inventory) do + if entry.mode == df.unit_inventory_item.T_mode.Worn then + local reduction = customRawTokens.getToken( + entry.item, + 'PEGASUS_BOOTS_MOD_MOVEMENT_TIMER_REDUCTION_PER_TICK') + amount = amount + (tonumber(reduction) or 0) + end + end + end + + -- Subtract amount from movement timer if currently moving + dfhack.units.addMoveTimer(-amount) + +The structure of a full mod +--------------------------- + +Create a folder for mod projects somewhere outside your Dwarf Fortress +installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the +names for the mod folders within it. In the example below, we'll use a mod ID of +``example-mod``. I'm sure your mods will have more creative names! The +``example-mod`` mod will be developed in the ``/path/to/mymods/example-mod/`` +directory and has a basic structure that looks like this:: + + raw/init.d/example-mod.lua + raw/objects/... + raw/scripts/example-mod.lua + raw/scripts/example-mod/... + README.md + +Let's go through that line by line. + +* A short (one-line) script in ``raw/init.d/`` to initialise your + mod when a save is loaded. +* Modifications to the game raws (potentially with custom raw tokens) go in + ``raw/objects/``. +* A control script in ``raw/scripts/`` that handles enabling and disabling your + mod. +* A subfolder for your mod under ``raw/scripts/`` will contain all the internal + scripts and/or modules used by your mod. + +It is a good idea to use a version control system to organize changes to your +mod code. You can create a separate Git repository for each of your mods. The +``README.md`` file will be your mod help text when people browse to your online +repository. + +Unless you want to install your ``raw/`` folder into your DF game folder every +time you make a change to your scripts, you should add your development scripts +directory to your script paths in ``dfhack-config/script-paths.txt``:: + + +/path/to/mymods/example-mod/raw/scripts/ + +Ok, you're all set up! Now, let's take a look at an example +``raw/scripts/example-mod.lua`` file:: + + -- main setup and teardown for example-mod + -- this next line indicates that the script supports the "enable" + -- API so you can start it by running "enable example-mod" and stop + -- it by running "disable example-mod" + --@ enable = true + + local usage = [[ + Usage + ----- + + enable example-mod + disable example-mod + ]] + local repeatUtil = require('repeat-util') + local eventful = require('plugins.eventful') + + -- you can reference global values or functions declared in any of + -- your internal scripts + local moduleA = reqscript('example-mod/module-a') + local moduleB = reqscript('example-mod/module-b') + local moduleC = reqscript('example-mod/module-c') + local moduleD = reqscript('example-mod/module-d') + + enabled = enabled or false + local modId = 'example-mod' + + if not dfhack_flags.enable then + print(usage) + print() + print(('Example mod is currently '):format( + enabled and 'enabled' or 'disabled')) + return + end + + if dfhack_flags.enable_state then + -- do any initialization your internal scripts might require + moduleA.onLoad() + moduleB.onLoad() + + -- register your callbacks + repeatUtil.scheduleEvery(modId .. ' every tick', 1, 'ticks', + moduleA.every1Tick) + repeatUtil.scheduleEvery(modId .. ' 100 frames', 1, 'frames', + moduleD.every100Frames) + + -- multiple functions in the same callback + eventful.onReactionComplete[modId] = function(reaction, + reaction_product, unit, input_items, input_reagents, + output_items) + -- pass the event's parameters to the listeners + moduleB.onReactionComplete(reaction, reaction_product, + unit, input_items, input_reagents, output_items) + moduleC.onReactionComplete(reaction, reaction_product, + unit, input_items, input_reagents, output_items) + end + + -- one function per callback (you can put them in the + -- above format if you prefer) + eventful.onProjItemCheckMovement[modId] = moduleD.onProjItemCheckMovement + eventful.onProjUnitCheckMovement[modId] = moduleD.onProjUnitCheckMovement + + print('Example mod enabled') + enabled = true + else + -- call any shutdown functions your internal scripts might require + moduleA.onUnload() + + repeatUtil.cancel(modId .. ' every ticks') + repeatUtil.cancel(modId .. ' 100 frames') + + eventful.onReactionComplete[modId] = nil + eventful.onProjItemCheckMovement[modId] = nil + eventful.onProjUnitCheckMovement[modId] = nil + + print('Example mod disabled') + enabled = false + end + +You can call ``enable example-mod`` and ``disable example-mod`` yourself while +developing, but for end users you can start your mod automatically from +``raw/init.d/example-mod.lua``:: + + dfhack.run_command('enable example-mod') + +Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: + + --@ module = true + -- The above line is required for reqscript to work + + function onLoad() -- global variables are exported + -- do initialization here + end + + -- this is an internal function: local functions/variables + -- are not exported + local function usedByOnTick(unit) + -- ... + end + + function onTick() -- exported + for _,unit in ipairs(df.global.world.units.all) do + usedByOnTick(unit) + end + end + +The `reqscript ` function reloads scripts that have changed, so you can modify +your scripts while DF is running and just disable/enable your mod to load the +changes into your ongoing game! diff --git a/docs/guides/quickfort-alias-guide.rst b/docs/guides/quickfort-alias-guide.rst index 6afad7569..50e5d7650 100644 --- a/docs/guides/quickfort-alias-guide.rst +++ b/docs/guides/quickfort-alias-guide.rst @@ -1,10 +1,11 @@ .. _quickfort-alias-guide: -Quickfort Keystroke Alias Guide -=============================== +Quickfort keystroke alias reference +=================================== Aliases allow you to use simple words to represent complicated key sequences -when configuring buildings and stockpiles in quickfort ``#query`` blueprints. +when configuring buildings and stockpiles in quickfort ``#query`` and +``#config`` blueprints. For example, say you have the following ``#build`` and ``#place`` blueprints:: diff --git a/docs/guides/quickfort-library-guide.rst b/docs/guides/quickfort-library-guide.rst index be76c7afe..86c82ac60 100644 --- a/docs/guides/quickfort-library-guide.rst +++ b/docs/guides/quickfort-library-guide.rst @@ -1,8 +1,8 @@ .. _blueprint-library-guide: .. _quickfort-library-guide: -Blueprint Library Index -======================= +Quickfort blueprint library +=========================== This guide contains a high-level overview of the blueprints available in the :source:`quickfort blueprint library `. @@ -194,7 +194,7 @@ Extra blueprints that are useful in specific situations. - :source:`library/embark.csv ` - :source:`library/pump_stack.csv ` -Light Aquifer Tap +Light aquifer tap ~~~~~~~~~~~~~~~~~ The aquifer tap helps you create a safe, everlasting source of fresh water from @@ -216,7 +216,7 @@ blueprint that builds important starting workshops (mason, carpenter, mechanic, and craftsdwarf) and a ``#place`` blueprint that lays down a pattern of useful starting stockpiles. -Pump Stack +Pump stack ~~~~~~~~~~ The pump stack blueprints help you move water and magma up to more convenient diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 0af0924b0..693ab033c 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1,8 +1,8 @@ .. _quickfort-blueprint-guide: .. _quickfort-user-guide: -Quickfort Blueprint Editing Guide -================================= +Quickfort blueprint creation guide +================================== `Quickfort ` is a DFHack script that helps you build fortresses from "blueprint" .csv and .xlsx files. Many applications exist to edit these files, @@ -29,9 +29,8 @@ For those just looking to apply existing blueprints, check out the `quickfort command's documentation ` for syntax. There are also many ready-to-use blueprints available in the ``blueprints/library`` subfolder in your DFHack installation. Browse them on your computer or -:source:`online `, or run ``quickfort list`` at the -``[DFHack]#`` prompt to list them, and then ``quickfort run`` to apply them to -your fort! +:source:`online `, or run `gui/quickfort` to browse +and apply them to your fort! Before you become an expert at writing blueprints, though, you should know that the easiest way to make a quickfort blueprint is to build your plan "for real" diff --git a/docs/index-dev.rst b/docs/index-dev.rst index 5a03cbedf..38c458bfe 100644 --- a/docs/index-dev.rst +++ b/docs/index-dev.rst @@ -1,5 +1,5 @@ ======================== -DFHack Development Guide +DFHack development guide ======================== These are pages relevant to people developing for DFHack. diff --git a/docs/plugins/infiniteSky.rst b/docs/plugins/infiniteSky.rst index f9a68a05a..c15f87e26 100644 --- a/docs/plugins/infiniteSky.rst +++ b/docs/plugins/infiniteSky.rst @@ -2,7 +2,7 @@ infiniteSky =========== .. dfhack-tool:: - :summary: Automatically allocates new z-levels of sky + :summary: Automatically allocate new z-levels of sky :tags: fort design map If enabled, this plugin will automatically allocate new z-levels of sky at the diff --git a/docs/plugins/overlay.rst b/docs/plugins/overlay.rst new file mode 100644 index 000000000..9416fba31 --- /dev/null +++ b/docs/plugins/overlay.rst @@ -0,0 +1,20 @@ +overlay +======= + +.. dfhack-tool:: + :summary: Provide an on-screen clickable DFHack launcher button. + :tags: dfhack interface + +This tool places a small button in the lower left corner of the screen that you +can click to run DFHack commands with `gui/launcher`. + +If you would rather always run `gui/launcher` with the hotkeys, or just don't +want the DFHack button on-screen, just disable the plugin with +``disable overlay``. + +Usage +----- + +:: + + enable overlay diff --git a/docs/plugins/workflow.rst b/docs/plugins/workflow.rst index 6bf89008b..9013ba189 100644 --- a/docs/plugins/workflow.rst +++ b/docs/plugins/workflow.rst @@ -2,7 +2,7 @@ workflow ======== .. dfhack-tool:: - :summary: Manage repeat jobs according to stock levels. + :summary: Manage automated item production rules. :tags: fort auto jobs .. dfhack-command:: fix-job-postings diff --git a/docs/sphinx_extensions/dfhack/tool_docs.py b/docs/sphinx_extensions/dfhack/tool_docs.py index 178303744..c7883e14c 100644 --- a/docs/sphinx_extensions/dfhack/tool_docs.py +++ b/docs/sphinx_extensions/dfhack/tool_docs.py @@ -114,7 +114,11 @@ class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription): if self.arguments: return self.arguments[0] else: - return self.env.docname.split('/')[-1] + parts = self.env.docname.split('/') + if 'tools' in parts: + return '/'.join(parts[parts.index('tools') + 1:]) + else: + return parts[-1] @staticmethod def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition: diff --git a/library/Core.cpp b/library/Core.cpp index 15cab5f08..cf47e4ec1 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -600,7 +600,7 @@ void help_helper(color_ostream &con, const string &entry_name) { } } -void tags_helper(color_ostream &con) { +void tags_helper(color_ostream &con, const string &tag) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -611,7 +611,9 @@ void tags_helper(color_ostream &con) { return; } - if (!Lua::SafeCall(con, L, 0, 0)) { + Lua::Push(L, tag); + + if (!Lua::SafeCall(con, L, 1, 0)) { con.printerr("Failed Lua call to helpdb.tags.\n"); } } @@ -712,7 +714,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else if (first == "tags") { - tags_helper(con); + tags_helper(con, parts.size() ? parts[0] : ""); } else if (first == "load" || first == "unload" || first == "reload") { diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d04da21ec..3a83f7aef 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1523,6 +1523,16 @@ static int gui_getDwarfmodeViewDims(lua_State *state) return 1; } +static int gui_getMousePos(lua_State *L) +{ + auto pos = Gui::getMousePos(); + if (pos.isValid()) + Lua::Push(L, pos); + else + lua_pushnil(L); + return 1; +} + static const LuaWrapper::FunctionReg dfhack_gui_module[] = { WRAPM(Gui, getCurViewscreen), WRAPM(Gui, getFocusString), @@ -1555,6 +1565,7 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { static const luaL_Reg dfhack_gui_funcs[] = { { "getDwarfmodeViewDims", gui_getDwarfmodeViewDims }, + { "getMousePos", gui_getMousePos }, { NULL, NULL } }; @@ -2282,18 +2293,12 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = { static int screen_getMousePos(lua_State *L) { - auto pos = Screen::getMousePos(); - lua_pushinteger(L, pos.x); - lua_pushinteger(L, pos.y); - return 2; + return Lua::PushPosXY(L, Screen::getMousePos()); } static int screen_getWindowSize(lua_State *L) { - auto pos = Screen::getWindowSize(); - lua_pushinteger(L, pos.x); - lua_pushinteger(L, pos.y); - return 2; + return Lua::PushPosXY(L, Screen::getWindowSize()); } static int screen_paintTile(lua_State *L) @@ -2370,6 +2375,21 @@ static int screen_findGraphicsTile(lua_State *L) } } +static int screen_hideGuard(lua_State *L) { + df::viewscreen *screen = dfhack_lua_viewscreen::get_pointer(L, 1, false); + luaL_checktype(L, 2, LUA_TFUNCTION); + + // remove screen from the stack so it doesn't get returned as an output + lua_remove(L, 1); + + Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); + + int nargs = lua_gettop(L) - 1; + lua_call(L, nargs, LUA_MULTRET); + + return lua_gettop(L); +} + namespace { int screen_show(lua_State *L) @@ -2472,6 +2492,7 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "paintString", screen_paintString }, { "fillRect", screen_fillRect }, { "findGraphicsTile", screen_findGraphicsTile }, + CWRAP(hideGuard, screen_hideGuard), CWRAP(show, screen_show), CWRAP(dismiss, screen_dismiss), CWRAP(isDismissed, screen_isDismissed), diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 455032fea..a0ae27889 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -126,10 +126,11 @@ namespace DFHack DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL); /* - * Cursor and window coords + * Cursor and window map coords */ DFHACK_EXPORT df::coord getViewportPos(); DFHACK_EXPORT df::coord getCursorPos(); + DFHACK_EXPORT df::coord getMousePos(); static const int AREA_MAP_WIDTH = 23; static const int MENU_WIDTH = 30; @@ -162,8 +163,6 @@ namespace DFHack DFHACK_EXPORT bool getDesignationCoords (int32_t &x, int32_t &y, int32_t &z); DFHACK_EXPORT bool setDesignationCoords (const int32_t x, const int32_t y, const int32_t z); - DFHACK_EXPORT bool getMousePos (int32_t & x, int32_t & y); - // The distance from the z-level of the tile at map coordinates (x, y) to the closest ground z-level below // Defaults to 0, unless overriden by plugins DFHACK_EXPORT int getDepthAt (int32_t x, int32_t y); diff --git a/library/include/modules/Renderer.h b/library/include/modules/Renderer.h index 11abd7c1d..65a9694ab 100644 --- a/library/include/modules/Renderer.h +++ b/library/include/modules/Renderer.h @@ -5,6 +5,13 @@ #pragma once namespace DFHack { namespace Renderer { + // If the the 'x' parameter points to this value, then the 'y' parameter will + // be interpreted as a boolean flag for whether to return map coordinates (false) + // or text tile coordinates (true). Only TWBT implements this logic, and this + // sentinel value can be removed once DF provides an API for retrieving the + // two sets of coordinates. + DFHACK_EXPORT extern const int32_t GET_MOUSE_COORDS_SENTINEL; + struct DFHACK_EXPORT renderer_wrap : public df::renderer { void set_to_null(); void copy_from_parent(); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 8af77e2e4..4a46040af 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -789,7 +789,11 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) end scripts[file].env = env scripts[file].run = script_code - return script_code(...), env + local args = {...} + for i,v in ipairs(args) do + args[i] = tostring(v) -- ensure passed parameters are strings + end + return script_code(table.unpack(args)), env end function dfhack.current_script_name() diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index e56211233..db80554cd 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -104,7 +104,7 @@ function getCursorPos() end function setCursorPos(cursor) - df.global.cursor = cursor + df.global.cursor = copyall(cursor) end function clearCursorPos() diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index d05749417..7076570b9 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -263,6 +263,7 @@ function EditField:onRenderBody(dc) end_pos == #txt and '' or string.char(26)) end dc:advance(self.text_offset):string(txt) + dc:string((' '):rep(dc.clip_x2 - dc.x)) end function EditField:onInput(keys) @@ -544,12 +545,10 @@ Label.ATTRS{ auto_width = false, on_click = DEFAULT_NIL, on_rclick = DEFAULT_NIL, - -- scroll_keys = STANDARDSCROLL, - show_scroll_icons = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false - up_arrow_icon = string.char(24), - down_arrow_icon = string.char(25), - scroll_icon_pen = COLOR_LIGHTCYAN, + show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false + scrollbar_fg = COLOR_LIGHTGREEN, + scrollbar_bg = COLOR_CYAN } function Label:init(args) @@ -573,16 +572,16 @@ function Label:setText(text) end function Label:update_scroll_inset() - if self.show_scroll_icons == nil then - self._show_scroll_icons = self:getTextHeight() > self.frame_body.height and 'right' or false + if self.show_scrollbar == nil then + self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false else - self._show_scroll_icons = self.show_scroll_icons + self._show_scrollbar = self.show_scrollbar end - if self._show_scroll_icons then - -- here self._show_scroll_icons can only be either + if self._show_scrollbar then + -- here self._show_scrollbar can only be either -- 'left' or any true value which we interpret as right local l,t,r,b = gui.parse_inset(self.frame_inset) - if self._show_scroll_icons == 'left' and l <= 0 then + if self._show_scrollbar == 'left' and l <= 0 then l = 1 elseif r <= 0 then r = 1 @@ -591,14 +590,54 @@ function Label:update_scroll_inset() end end -function Label:render_scroll_icons(dc, x, y1, y2) - if self.start_line_num ~= 1 then - dc:seek(x, y1):char(self.up_arrow_icon, self.scroll_icon_pen) +-- the position is the number of tiles of empty space above the top of the +-- scrollbar, and the height is the number of tiles the scrollbar should occupy +-- to represent the percentage of text that is on the screen. +local function get_scrollbar_pos_and_height(label) + local first_visible_line = label.start_line_num + local text_height = label:getTextHeight() + local last_visible_line = first_visible_line + label.frame_body.height - 1 + local scrollbar_body_height = label.frame_body.height - 2 + local displayed_lines = last_visible_line - first_visible_line + + local height = math.floor(((displayed_lines-1) * scrollbar_body_height) / + text_height) + + local max_pos = scrollbar_body_height - height + local pos = math.ceil(((first_visible_line-1) * max_pos) / + (text_height - label.frame_body.height)) + + return pos, height +end + +local UP_ARROW_CHAR = string.char(24) +local DOWN_ARROW_CHAR = string.char(25) +local NO_ARROW_CHAR = string.char(32) +local BAR_CHAR = string.char(7) +local BAR_BG_CHAR = string.char(179) + +function Label:render_scrollbar(dc, x, y1, y2) + -- render up arrow if we're not at the top + dc:seek(x, y1):char( + self.start_line_num == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, + self.scrollbar_fg, self.scrollbar_bg) + -- render scrollbar body + local pos, height = get_scrollbar_pos_and_height(self) + local starty = y1 + pos + 1 + local endy = y1 + pos + height + for y=y1+1,y2-1 do + if y >= starty and y <= endy then + dc:seek(x, y):char(BAR_CHAR, self.scrollbar_fg) + else + dc:seek(x, y):char(BAR_BG_CHAR, self.scrollbar_bg) + end end + -- render down arrow if we're not at the bottom local last_visible_line = self.start_line_num + self.frame_body.height - 1 - if last_visible_line < self:getTextHeight() then - dc:seek(x, y2):char(self.down_arrow_icon, self.scroll_icon_pen) - end + dc:seek(x, y2):char( + last_visible_line >= self:getTextHeight() and + NO_ARROW_CHAR or DOWN_ARROW_CHAR, + self.scrollbar_fg, self.scrollbar_bg) end function Label:computeFrame(parent_rect) @@ -644,13 +683,11 @@ function Label:onRenderBody(dc) end function Label:onRenderFrame(dc, rect) - if self._show_scroll_icons - and self:getTextHeight() > self.frame_body.height - then - local x = self._show_scroll_icons == 'left' + if self._show_scrollbar then + local x = self._show_scrollbar == 'left' and self.frame_body.x1-dc.x1-1 or self.frame_body.x2-dc.x1+1 - self:render_scroll_icons(dc, + self:render_scrollbar(dc, x, self.frame_body.y1-dc.y1, self.frame_body.y2-dc.y1 @@ -658,7 +695,35 @@ function Label:onRenderFrame(dc, rect) end end +function Label:click_scrollbar() + if not self._show_scrollbar then return end + local rect = self.frame_body + local x, y = dscreen.getMousePos() + + if self._show_scrollbar == 'left' and x ~= rect.x1-1 or x ~= rect.x2+1 then + return + end + if y < rect.y1 or y > rect.y2 then + return + end + + if y == rect.y1 then + return -1 + elseif y == rect.y2 then + return 1 + else + local pos, height = get_scrollbar_pos_and_height(self) + if y <= rect.y1 + pos then + return '-halfpage' + elseif y > rect.y1 + pos + height then + return '+halfpage' + end + end + return nil +end + function Label:scroll(nlines) + if not nlines then return end if type(nlines) == 'string' then if nlines == '+page' then nlines = self.frame_body.height @@ -676,12 +741,16 @@ function Label:scroll(nlines) n = math.min(n, self:getTextHeight() - self.frame_body.height + 1) n = math.max(n, 1) self.start_line_num = n + return nlines end function Label:onInput(keys) if is_disabled(self) then return false end - if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then - self:on_click() + if keys._MOUSE_L_DOWN then + if not self:scroll(self:click_scrollbar()) and + self:getMousePos() and self.on_click then + self:on_click() + end end if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then self:on_rclick() diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 2f41bbb2c..e5359b323 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -432,6 +432,37 @@ local function ensure_db() index_tags() end +local function parse_blocks(text) + local blocks = {} + for line in text:gmatch('[^\n]*') do + local _,indent = line:find('^ *') + table.insert(blocks, {line=line:trim(), indent=indent}) + end + return blocks +end + +local function format_block(line, indent, width) + local wrapped = line:wrap(width - indent) + if indent == 0 then return wrapped end + local padding = (' '):rep(indent) + local indented_lines = {} + for line in wrapped:gmatch('[^\n]*') do + table.insert(indented_lines, padding .. line) + end + return table.concat(indented_lines, '\n') +end + +-- wraps the unwrapped source help at the specified width, preserving block +-- indents +local function rewrap(text, width) + local formatted_blocks = {} + for _,block in ipairs(parse_blocks(text)) do + table.insert(formatted_blocks, + format_block(block.line, block.indent, width)) + end + return table.concat(formatted_blocks, '\n') +end + --------------------------------------------------------------------------- -- get API --------------------------------------------------------------------------- @@ -482,9 +513,10 @@ function get_entry_short_help(entry) return get_db_property(entry, 'short_help') end --- returns the full help documentation associated with the entry -function get_entry_long_help(entry) - return get_db_property(entry, 'long_help') +-- returns the full help documentation associated with the entry, optionally +-- wrapped to the specified width (80 if not specified). +function get_entry_long_help(entry, width) + return rewrap(get_db_property(entry, 'long_help'), width or 80) end -- returns the set of tags associated with the entry @@ -687,14 +719,6 @@ local function print_columns(col1text, col2text) end end --- implements the 'tags' builtin command -function tags() - local tags = get_tags() - for _,tag in ipairs(tags) do - print_columns(tag, get_tag_data(tag).description) - end -end - -- prints the requested entries to the console. include and exclude filters are -- defined as in search_entries() above. local function list_entries(skip_tags, include, exclude) @@ -732,4 +756,27 @@ function ls(filter_str, skip_tags, show_dev_commands) show_dev_commands and {} or {tag='dev'}) end +local function list_tags() + local tags = get_tags() + for _,tag in ipairs(tags) do + print_columns(tag, get_tag_data(tag).description) + end +end + +-- implements the 'tags' builtin command +function tags(tag) + if tag and #tag > 0 and not is_tag(tag) then + dfhack.printerr(('unrecognized tag: "%s"'):format(tag)) + end + + if not is_tag(tag) then + list_tags() + return + end + + local skip_tags = true + local include = {entry_type={ENTRY_TYPES.COMMAND}, tag=tag} + list_entries(skip_tags, include) +end + return _ENV diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 99ca87c6e..00e31929f 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -608,18 +608,20 @@ df_env = df_shortcut_env() function df_expr_to_ref(expr) expr = expr:gsub('%["(.-)"%]', function(field) return '.' .. field end) :gsub('%[\'(.-)\'%]', function(field) return '.' .. field end) - :gsub('%[(%d+)]', function(field) return '.' .. field end) + :gsub('%[(%-?%d+)%]', function(field) return '.' .. field end) local parts = expr:split('.', true) local obj = df_env[parts[1]] for i = 2, #parts do local key = tonumber(parts[i]) or parts[i] - local cur = obj[key] - if i == #parts and ((type(cur) ~= 'userdata') or - type(cur) == 'userdata' and getmetatable(cur) == nil) then - obj = obj:_field(key) - else - obj = obj[key] + if i == #parts then + local ok, ret = pcall(function() + return obj:_field(key) + end) + if ok then + return ret + end end + obj = obj[key] end return obj end diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index fc78bb57d..56aa09b7f 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1831,17 +1831,22 @@ bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t return true; } -bool Gui::getMousePos (int32_t & x, int32_t & y) -{ - if (gps) { - x = gps->mouse_x; - y = gps->mouse_y; - } - else { - x = -1; - y = -1; +// returns the map coordinates that the mouse cursor is over +df::coord Gui::getMousePos() +{ + df::coord pos; + if (gps && gps->mouse_x > -1) { + // return invalid coords if the cursor is not over the map + DwarfmodeDims dims = getDwarfmodeViewDims(); + if (gps->mouse_x < dims.map_x1 || gps->mouse_x > dims.map_x2 || + gps->mouse_y < dims.map_y1 || gps->mouse_y > dims.map_y2) { + return pos; + } + pos = getViewportPos(); + pos.x += gps->mouse_x - 1; + pos.y += gps->mouse_y - 1; } - return (x == -1) ? false : true; + return pos; } int getDepthAt_default (int32_t x, int32_t y) diff --git a/library/modules/Renderer.cpp b/library/modules/Renderer.cpp index 474dc3656..b746a149c 100644 --- a/library/modules/Renderer.cpp +++ b/library/modules/Renderer.cpp @@ -9,6 +9,8 @@ using DFHack::Renderer::renderer_wrap; static renderer_wrap *original_renderer = NULL; +const int32_t Renderer::GET_MOUSE_COORDS_SENTINEL = 0xcd1aa471; + bool init() { if (!original_renderer) diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 5001a34c0..bff14a380 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -31,6 +31,7 @@ distribution. #include using namespace std; +#include "modules/Renderer.h" #include "modules/Screen.h" #include "modules/GuiHooks.h" #include "MemAccess.h" @@ -75,12 +76,14 @@ using std::string; * Screen painting API. */ +// returns text grid coordinates, even if the game map is scaled differently df::coord2d Screen::getMousePos() { - if (!gps || (enabler && !enabler->tracking_on)) + int32_t x = Renderer::GET_MOUSE_COORDS_SENTINEL, y = (int32_t)true; + if (!enabler || !enabler->renderer->get_mouse_coords(&x, &y)) { return df::coord2d(-1, -1); - - return df::coord2d(gps->mouse_x, gps->mouse_y); + } + return df::coord2d(x, y); } df::coord2d Screen::getWindowSize() @@ -769,7 +772,7 @@ int dfhack_lua_viewscreen::do_input(lua_State *L) } } - if (enabler && enabler->tracking_on) + if (enabler) { if (enabler->mouse_lbut_down) { lua_pushboolean(L, true); diff --git a/library/xml b/library/xml index dc118c5e9..f5fab13fb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit dc118c5e90aea6181a290e4bbf40e7f2974fb053 +Subproject commit f5fab13fb652dd953e9d59e8deca032b26131208 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 49e704b75..dc341b39d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -144,6 +144,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) + dfhack_plugin(overlay overlay.cpp) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(petcapRemover petcapRemover.cpp) dfhack_plugin(plants plants.cpp) diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index d96e7a972..82c40934c 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -313,7 +313,7 @@ static int do_chop_designation(bool chop, bool count_only, int *skipped = nullpt { int count = 0; int estimated_yield = get_log_count(); - multimap trees_by_size; + multimap> trees_by_size; if (skipped) { diff --git a/plugins/devel/kittens.cpp b/plugins/devel/kittens.cpp index fce924aff..156686a1e 100644 --- a/plugins/devel/kittens.cpp +++ b/plugins/devel/kittens.cpp @@ -135,13 +135,12 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) last_designation[2] = desig_z; out.print("Designation: %d %d %d\n",desig_x, desig_y, desig_z); } - int mouse_x, mouse_y; - Gui::getMousePos(mouse_x,mouse_y); - if(mouse_x != last_mouse[0] || mouse_y != last_mouse[1]) + df::coord mousePos = Gui::getMousePos(); + if(mousePos.x != last_mouse[0] || mousePos.y != last_mouse[1]) { - last_mouse[0] = mouse_x; - last_mouse[1] = mouse_y; - out.print("Mouse: %d %d\n",mouse_x, mouse_y); + last_mouse[0] = mousePos.x; + last_mouse[1] = mousePos.y; + out.print("Mouse: %d %d\n",mousePos.x, mousePos.y); } } return CR_OK; diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index bbcabde65..365be3313 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -424,26 +424,30 @@ static bool is_smooth_wall(MapExtras::MapCache &map, const DFCoord &pos) { && tileShape(tt) == df::tiletype_shape::WALL; } -static bool is_smooth_wall_or_door(MapExtras::MapCache &map, - const DFCoord &pos) { - if (is_smooth_wall(map, pos)) - return true; - +static bool is_connector(MapExtras::MapCache &map, const DFCoord &pos) { df::building *bld = Buildings::findAtTile(pos); - return bld && bld->getType() == df::building_type::Door; + + return bld && + (bld->getType() == df::building_type::Door || + bld->getType() == df::building_type::Floodgate); +} + +static bool is_smooth_wall_or_connector(MapExtras::MapCache &map, + const DFCoord &pos) { + return is_smooth_wall(map, pos) || is_connector(map, pos); } // adds adjacent smooth walls and doors to the given tdir static TileDirection get_adjacent_smooth_walls(MapExtras::MapCache &map, const DFCoord &pos, TileDirection tdir) { - if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y-1, pos.z))) + if (is_smooth_wall_or_connector(map, DFCoord(pos.x, pos.y-1, pos.z))) tdir.north = 1; - if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y+1, pos.z))) + if (is_smooth_wall_or_connector(map, DFCoord(pos.x, pos.y+1, pos.z))) tdir.south = 1; - if (is_smooth_wall_or_door(map, DFCoord(pos.x-1, pos.y, pos.z))) + if (is_smooth_wall_or_connector(map, DFCoord(pos.x-1, pos.y, pos.z))) tdir.west = 1; - if (is_smooth_wall_or_door(map, DFCoord(pos.x+1, pos.y, pos.z))) + if (is_smooth_wall_or_connector(map, DFCoord(pos.x+1, pos.y, pos.z))) tdir.east = 1; return tdir; } @@ -469,7 +473,7 @@ static bool adjust_smooth_wall_dir(MapExtras::MapCache &map, const DFCoord &pos, TileDirection tdir = BLANK_TILE_DIRECTION) { if (!is_smooth_wall(map, pos)) - return false; + return is_connector(map, pos); tdir = ensure_valid_tdir(get_adjacent_smooth_walls(map, pos, tdir)); diff --git a/plugins/infiniteSky.cpp b/plugins/infiniteSky.cpp index bc51f06f1..b477700f4 100644 --- a/plugins/infiniteSky.cpp +++ b/plugins/infiniteSky.cpp @@ -32,7 +32,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector tracking_on) - return pos; + df::coord pos = Gui::getMousePos(); + pos.z -= Gui::getDepthAt(pos.x, pos.y); - if (!Gui::getMousePos(mx, my)) - return pos; - - int32_t vx, vy, vz; - if (!Gui::getViewCoords(vx, vy, vz)) - return pos; - - pos.x = vx + mx - 1; - pos.y = vy + my - 1; - pos.z = vz - Gui::getDepthAt(mx, my); + df::coord vpos = Gui::getViewportPos(); + mx = pos.x - vpos.x + 1; + my = pos.y - vpos.y + 1; return pos; } @@ -536,6 +531,7 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest if (mpos.x == x && mpos.y == y && mpos.z == z) return; + DEBUG(log).print("moving cursor to %d, %d, %d\n", x, y, z); Gui::setCursorCoords(mpos.x, mpos.y, mpos.z); Gui::refreshSidebar(); } @@ -575,8 +571,6 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest int32_t mx, my; auto mpos = get_mouse_pos(mx, my); bool mpos_valid = mpos.x != -30000 && mpos.y != -30000 && mpos.z != -30000; - if (mx < 1 || mx > dims.map_x2 || my < 1 || my > dims.map_y2) - mpos_valid = false; // Check if in lever binding mode if (Gui::getFocusString(Core::getTopViewscreen()) == @@ -723,7 +717,7 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest } } - OutputString(color, mx, my, "X", false, 0, 0, true); + Screen::paintTile(Screen::Pen('X', color), mx, my, true); return; } diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp new file mode 100644 index 000000000..30a5546a4 --- /dev/null +++ b/plugins/overlay.cpp @@ -0,0 +1,338 @@ +#include "df/viewscreen_adopt_regionst.h" +#include "df/viewscreen_adventure_logst.h" +#include "df/viewscreen_announcelistst.h" +#include "df/viewscreen_assign_display_itemst.h" +#include "df/viewscreen_barterst.h" +#include "df/viewscreen_buildinglistst.h" +#include "df/viewscreen_buildingst.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/viewscreen_civlistst.h" +#include "df/viewscreen_counterintelligencest.h" +#include "df/viewscreen_createquotast.h" +#include "df/viewscreen_customize_unitst.h" +#include "df/viewscreen_dungeonmodest.h" +#include "df/viewscreen_dungeon_monsterstatusst.h" +#include "df/viewscreen_dungeon_wrestlest.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_entityst.h" +#include "df/viewscreen_export_graphical_mapst.h" +#include "df/viewscreen_export_regionst.h" +#include "df/viewscreen_game_cleanerst.h" +#include "df/viewscreen_image_creator_mode.h" +#include "df/viewscreen_image_creatorst.h" +#include "df/viewscreen_itemst.h" +#include "df/viewscreen_joblistst.h" +#include "df/viewscreen_jobmanagementst.h" +#include "df/viewscreen_jobst.h" +#include "df/viewscreen_justicest.h" +#include "df/viewscreen_kitchenpref_page.h" +#include "df/viewscreen_kitchenprefst.h" +#include "df/viewscreen_layer_arena_creaturest.h" +#include "df/viewscreen_layer_assigntradest.h" +#include "df/viewscreen_layer_choose_language_namest.h" +#include "df/viewscreen_layer_currencyst.h" +#include "df/viewscreen_layer_export_play_mapst.h" +#include "df/viewscreen_layer.h" +#include "df/viewscreen_layer_militaryst.h" +#include "df/viewscreen_layer_musicsoundst.h" +#include "df/viewscreen_layer_noblelistst.h" +#include "df/viewscreen_layer_overall_healthst.h" +#include "df/viewscreen_layer_reactionst.h" +#include "df/viewscreen_layer_squad_schedulest.h" +#include "df/viewscreen_layer_stockpilest.h" +#include "df/viewscreen_layer_stone_restrictionst.h" +#include "df/viewscreen_layer_unit_actionst.h" +#include "df/viewscreen_layer_unit_healthst.h" +#include "df/viewscreen_layer_unit_relationshipst.h" +#include "df/viewscreen_layer_world_gen_param_presetst.h" +#include "df/viewscreen_layer_world_gen_paramst.h" +#include "df/viewscreen_legendsst.h" +#include "df/viewscreen_loadgamest.h" +#include "df/viewscreen_locationsst.h" +#include "df/viewscreen_meetingst.h" +#include "df/viewscreen_movieplayerst.h" +#include "df/viewscreen_noblest.h" +#include "df/viewscreen_optionst.h" +#include "df/viewscreen_overallstatusst.h" +#include "df/viewscreen_petitionsst.h" +#include "df/viewscreen_petst.h" +#include "df/viewscreen_pricest.h" +#include "df/viewscreen_reportlistst.h" +#include "df/viewscreen_requestagreementst.h" +#include "df/viewscreen_savegamest.h" +#include "df/viewscreen_selectitemst.h" +#include "df/viewscreen_setupadventurest.h" +#include "df/viewscreen_setupdwarfgamest.h" +#include "df/viewscreen_storesst.h" +#include "df/viewscreen_textviewerst.h" +#include "df/viewscreen_titlest.h" +#include "df/viewscreen_topicmeeting_fill_land_holder_positionsst.h" +#include "df/viewscreen_topicmeetingst.h" +#include "df/viewscreen_topicmeeting_takerequestsst.h" +#include "df/viewscreen_tradeagreementst.h" +#include "df/viewscreen_tradegoodsst.h" +#include "df/viewscreen_tradelistst.h" +#include "df/viewscreen_treasurelistst.h" +#include "df/viewscreen_unitlist_page.h" +#include "df/viewscreen_unitlistst.h" +#include "df/viewscreen_unitst.h" +#include "df/viewscreen_update_regionst.h" +#include "df/viewscreen_wagesst.h" +#include "df/viewscreen_workquota_conditionst.h" +#include "df/viewscreen_workquota_detailsst.h" +#include "df/viewscreen_workshop_profilest.h" + +#include "Debug.h" +#include "PluginManager.h" +#include "VTableInterpose.h" +#include "uicommon.h" + +#include "modules/Screen.h" + +using namespace DFHack; + +DFHACK_PLUGIN("overlay"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); +REQUIRE_GLOBAL(enabler); +REQUIRE_GLOBAL(gps); + +namespace DFHack { + DBG_DECLARE(overlay, log, DebugCategory::LINFO); +} + +static const std::string button_text = "[ DFHack Launcher ]"; +static bool clicked = false; + +static bool handle_click() { + if (!enabler->mouse_lbut_down || clicked) { + return false; + } + df::coord2d pos = Screen::getMousePos(); + DEBUG(log).print("clicked at screen coordinates (%d, %d)\n", pos.x, pos.y); + if (pos.y == gps->dimy - 1 && pos.x >= 1 && (size_t)pos.x <= button_text.size()) { + clicked = true; + Core::getInstance().setHotkeyCmd("gui/launcher"); + return true; + } + return false; +} + +static void draw_widgets() { + int x = 1; + int y = gps->dimy - 1; + OutputString(COLOR_GREEN, x, y, button_text); +} + +template +struct viewscreen_overlay : T { + typedef T interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { + if (!handle_click()) + INTERPOSE_NEXT(feed)(input); + } + DEFINE_VMETHOD_INTERPOSE(void, render, ()) { + INTERPOSE_NEXT(render)(); + draw_widgets(); + } +}; + +#define IMPLEMENT_HOOKS(screen) \ + typedef viewscreen_overlay screen##_overlay; \ + template<> IMPLEMENT_VMETHOD_INTERPOSE(screen##_overlay, feed); \ + template<> IMPLEMENT_VMETHOD_INTERPOSE(screen##_overlay, render); + +IMPLEMENT_HOOKS(adopt_region) +IMPLEMENT_HOOKS(adventure_log) +IMPLEMENT_HOOKS(announcelist) +IMPLEMENT_HOOKS(assign_display_item) +IMPLEMENT_HOOKS(barter) +IMPLEMENT_HOOKS(buildinglist) +IMPLEMENT_HOOKS(building) +IMPLEMENT_HOOKS(choose_start_site) +IMPLEMENT_HOOKS(civlist) +IMPLEMENT_HOOKS(counterintelligence) +IMPLEMENT_HOOKS(createquota) +IMPLEMENT_HOOKS(customize_unit) +IMPLEMENT_HOOKS(dungeonmode) +IMPLEMENT_HOOKS(dungeon_monsterstatus) +IMPLEMENT_HOOKS(dungeon_wrestle) +IMPLEMENT_HOOKS(dwarfmode) +IMPLEMENT_HOOKS(entity) +IMPLEMENT_HOOKS(export_graphical_map) +IMPLEMENT_HOOKS(export_region) +IMPLEMENT_HOOKS(game_cleaner) +IMPLEMENT_HOOKS(image_creator) +IMPLEMENT_HOOKS(item) +IMPLEMENT_HOOKS(joblist) +IMPLEMENT_HOOKS(jobmanagement) +IMPLEMENT_HOOKS(job) +IMPLEMENT_HOOKS(justice) +IMPLEMENT_HOOKS(kitchenpref) +IMPLEMENT_HOOKS(layer_arena_creature) +IMPLEMENT_HOOKS(layer_assigntrade) +IMPLEMENT_HOOKS(layer_choose_language_name) +IMPLEMENT_HOOKS(layer_currency) +IMPLEMENT_HOOKS(layer_export_play_map) +IMPLEMENT_HOOKS(layer_military) +IMPLEMENT_HOOKS(layer_musicsound) +IMPLEMENT_HOOKS(layer_noblelist) +IMPLEMENT_HOOKS(layer_overall_health) +IMPLEMENT_HOOKS(layer_reaction) +IMPLEMENT_HOOKS(layer_squad_schedule) +IMPLEMENT_HOOKS(layer_stockpile) +IMPLEMENT_HOOKS(layer_stone_restriction) +IMPLEMENT_HOOKS(layer_unit_action) +IMPLEMENT_HOOKS(layer_unit_health) +IMPLEMENT_HOOKS(layer_unit_relationship) +IMPLEMENT_HOOKS(layer_world_gen_param_preset) +IMPLEMENT_HOOKS(layer_world_gen_param) +IMPLEMENT_HOOKS(legends) +IMPLEMENT_HOOKS(loadgame) +IMPLEMENT_HOOKS(locations) +IMPLEMENT_HOOKS(meeting) +IMPLEMENT_HOOKS(movieplayer) +IMPLEMENT_HOOKS(noble) +IMPLEMENT_HOOKS(option) +IMPLEMENT_HOOKS(overallstatus) +IMPLEMENT_HOOKS(petitions) +IMPLEMENT_HOOKS(pet) +IMPLEMENT_HOOKS(price) +IMPLEMENT_HOOKS(reportlist) +IMPLEMENT_HOOKS(requestagreement) +IMPLEMENT_HOOKS(savegame) +IMPLEMENT_HOOKS(selectitem) +IMPLEMENT_HOOKS(setupadventure) +IMPLEMENT_HOOKS(setupdwarfgame) +IMPLEMENT_HOOKS(stores) +IMPLEMENT_HOOKS(textviewer) +IMPLEMENT_HOOKS(title) +IMPLEMENT_HOOKS(topicmeeting_fill_land_holder_positions) +IMPLEMENT_HOOKS(topicmeeting) +IMPLEMENT_HOOKS(topicmeeting_takerequests) +IMPLEMENT_HOOKS(tradeagreement) +IMPLEMENT_HOOKS(tradegoods) +IMPLEMENT_HOOKS(tradelist) +IMPLEMENT_HOOKS(treasurelist) +IMPLEMENT_HOOKS(unitlist) +IMPLEMENT_HOOKS(unit) +IMPLEMENT_HOOKS(update_region) +IMPLEMENT_HOOKS(wages) +IMPLEMENT_HOOKS(workquota_condition) +IMPLEMENT_HOOKS(workquota_details) +IMPLEMENT_HOOKS(workshop_profile) + +#undef IMPLEMENT_HOOKS + +DFhackCExport command_result plugin_onstatechange(color_ostream &, + state_change_event evt) { + if (evt == SC_VIEWSCREEN_CHANGED) { + clicked = false; + } + return CR_OK; +} + +#define INTERPOSE_HOOKS_FAILED(screen) \ + !INTERPOSE_HOOK(screen##_overlay, feed).apply(enable) || \ + !INTERPOSE_HOOK(screen##_overlay, render).apply(enable) + +DFhackCExport command_result plugin_enable(color_ostream &, bool enable) { + if (is_enabled == enable) + return CR_OK; + + if (enable != is_enabled) { + if (INTERPOSE_HOOKS_FAILED(adopt_region) || + INTERPOSE_HOOKS_FAILED(adventure_log) || + INTERPOSE_HOOKS_FAILED(announcelist) || + INTERPOSE_HOOKS_FAILED(assign_display_item) || + INTERPOSE_HOOKS_FAILED(barter) || + INTERPOSE_HOOKS_FAILED(buildinglist) || + INTERPOSE_HOOKS_FAILED(building) || + INTERPOSE_HOOKS_FAILED(choose_start_site) || + INTERPOSE_HOOKS_FAILED(civlist) || + INTERPOSE_HOOKS_FAILED(counterintelligence) || + INTERPOSE_HOOKS_FAILED(createquota) || + INTERPOSE_HOOKS_FAILED(customize_unit) || + INTERPOSE_HOOKS_FAILED(dungeonmode) || + INTERPOSE_HOOKS_FAILED(dungeon_monsterstatus) || + INTERPOSE_HOOKS_FAILED(dungeon_wrestle) || + INTERPOSE_HOOKS_FAILED(dwarfmode) || + INTERPOSE_HOOKS_FAILED(entity) || + INTERPOSE_HOOKS_FAILED(export_graphical_map) || + INTERPOSE_HOOKS_FAILED(export_region) || + INTERPOSE_HOOKS_FAILED(game_cleaner) || + INTERPOSE_HOOKS_FAILED(image_creator) || + INTERPOSE_HOOKS_FAILED(item) || + INTERPOSE_HOOKS_FAILED(joblist) || + INTERPOSE_HOOKS_FAILED(jobmanagement) || + INTERPOSE_HOOKS_FAILED(job) || + INTERPOSE_HOOKS_FAILED(justice) || + INTERPOSE_HOOKS_FAILED(kitchenpref) || + INTERPOSE_HOOKS_FAILED(layer_arena_creature) || + INTERPOSE_HOOKS_FAILED(layer_assigntrade) || + INTERPOSE_HOOKS_FAILED(layer_choose_language_name) || + INTERPOSE_HOOKS_FAILED(layer_currency) || + INTERPOSE_HOOKS_FAILED(layer_export_play_map) || + INTERPOSE_HOOKS_FAILED(layer_military) || + INTERPOSE_HOOKS_FAILED(layer_musicsound) || + INTERPOSE_HOOKS_FAILED(layer_noblelist) || + INTERPOSE_HOOKS_FAILED(layer_overall_health) || + INTERPOSE_HOOKS_FAILED(layer_reaction) || + INTERPOSE_HOOKS_FAILED(layer_squad_schedule) || + INTERPOSE_HOOKS_FAILED(layer_stockpile) || + INTERPOSE_HOOKS_FAILED(layer_stone_restriction) || + INTERPOSE_HOOKS_FAILED(layer_unit_action) || + INTERPOSE_HOOKS_FAILED(layer_unit_health) || + INTERPOSE_HOOKS_FAILED(layer_unit_relationship) || + INTERPOSE_HOOKS_FAILED(layer_world_gen_param_preset) || + INTERPOSE_HOOKS_FAILED(layer_world_gen_param) || + INTERPOSE_HOOKS_FAILED(legends) || + INTERPOSE_HOOKS_FAILED(loadgame) || + INTERPOSE_HOOKS_FAILED(locations) || + INTERPOSE_HOOKS_FAILED(meeting) || + INTERPOSE_HOOKS_FAILED(movieplayer) || + INTERPOSE_HOOKS_FAILED(noble) || + INTERPOSE_HOOKS_FAILED(option) || + INTERPOSE_HOOKS_FAILED(overallstatus) || + INTERPOSE_HOOKS_FAILED(petitions) || + INTERPOSE_HOOKS_FAILED(pet) || + INTERPOSE_HOOKS_FAILED(price) || + INTERPOSE_HOOKS_FAILED(reportlist) || + INTERPOSE_HOOKS_FAILED(requestagreement) || + INTERPOSE_HOOKS_FAILED(savegame) || + INTERPOSE_HOOKS_FAILED(selectitem) || + INTERPOSE_HOOKS_FAILED(setupadventure) || + INTERPOSE_HOOKS_FAILED(setupdwarfgame) || + INTERPOSE_HOOKS_FAILED(stores) || + INTERPOSE_HOOKS_FAILED(textviewer) || + INTERPOSE_HOOKS_FAILED(title) || + INTERPOSE_HOOKS_FAILED(topicmeeting_fill_land_holder_positions) || + INTERPOSE_HOOKS_FAILED(topicmeeting) || + INTERPOSE_HOOKS_FAILED(topicmeeting_takerequests) || + INTERPOSE_HOOKS_FAILED(tradeagreement) || + INTERPOSE_HOOKS_FAILED(tradegoods) || + INTERPOSE_HOOKS_FAILED(tradelist) || + INTERPOSE_HOOKS_FAILED(treasurelist) || + INTERPOSE_HOOKS_FAILED(unitlist) || + INTERPOSE_HOOKS_FAILED(unit) || + INTERPOSE_HOOKS_FAILED(update_region) || + INTERPOSE_HOOKS_FAILED(wages) || + INTERPOSE_HOOKS_FAILED(workquota_condition) || + INTERPOSE_HOOKS_FAILED(workquota_details) || + INTERPOSE_HOOKS_FAILED(workshop_profile)) + return CR_FAILURE; + + is_enabled = enable; + } + return CR_OK; +} + +#undef INTERPOSE_HOOKS_FAILED + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &) { + return plugin_enable(out, true); +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + return plugin_enable(out, false); +} diff --git a/scripts b/scripts index 7bae77756..484988bb3 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 7bae77756f974aad765251096516a89d9d035977 +Subproject commit 484988bb3d64f7aed9442af3b226faa77d8fd00d diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua index 6b0097d1e..c43b5e886 100644 --- a/test/library/gui/widgets.Label.lua +++ b/test/library/gui/widgets.Label.lua @@ -29,7 +29,7 @@ function test.correct_frame_body_with_scroll_icons() end local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scroll_icons.") + expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scrollbar.") end function test.correct_frame_body_with_few_text_lines() @@ -50,10 +50,10 @@ function test.correct_frame_body_with_few_text_lines() end local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.") end -function test.correct_frame_body_without_show_scroll_icons() +function test.correct_frame_body_without_show_scrollbar() local t = {} for i = 1, 12 do t[#t+1] = tostring(i) @@ -66,13 +66,13 @@ function test.correct_frame_body_without_show_scroll_icons() view_id = 'text', frame_inset = 0, text = t, - show_scroll_icons = false, + show_scrollbar = false, }, } end local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.") end function test.scroll() diff --git a/test/library/helpdb.lua b/test/library/helpdb.lua index 6c2ef2e9b..7a223bcaa 100644 --- a/test/library/helpdb.lua +++ b/test/library/helpdb.lua @@ -398,8 +398,25 @@ function test.get_entry_short_help() end function test.get_entry_long_help() + local expected = [[ +basic +***** + +**Tags:** map + +**Command:** +"basic" + +Documented +basic. + +Documented +full help. + ]] + expect.eq(expected, h.get_entry_long_help('basic', 13)) + -- long help for plugins/commands that have doc files should match the - -- contents of those files exactly + -- contents of those files exactly (test data is already wrapped) expect.eq(files['hack/docs/docs/tools/hascommands.txt'], h.get_entry_long_help('hascommands')) expect.eq(files['hack/docs/docs/tools/hascommands.txt'], @@ -605,6 +622,20 @@ function test.tags() end) end +function test.tags_tag() + local mock_print = mock.func() + mock.patch(h, 'print', mock_print, function() + h.tags('armok') + expect.eq(3, mock_print.call_count) + expect.eq('bindboxers Bind your boxers.', + mock_print.call_args[1][1]) + expect.eq('boxbinders Box your binders.', + mock_print.call_args[2][1]) + expect.eq('samename Samename.', + mock_print.call_args[3][1]) + end) +end + function test.ls() local mock_print = mock.func() mock.patch(h, 'print', mock_print, function() diff --git a/test/library/utils.lua b/test/library/utils.lua index 5261ac913..509fab8bc 100644 --- a/test/library/utils.lua +++ b/test/library/utils.lua @@ -54,3 +54,62 @@ function test.invert_overwrite() expect.eq(i.b, 2) expect.eq(i.a, 3) end + +function test.df_expr_to_ref() + -- userdata field + expect.eq(utils.df_expr_to_ref('df.global.world.engravings'), df.global.world.engravings) + expect.eq(utils.df_expr_to_ref('df.global.world.engravings'), df.global.world:_field('engravings')) + -- primitive field + expect.eq(utils.df_expr_to_ref('df.global.world.original_save_version'), df.global.world:_field('original_save_version')) + -- table field + expect.eq(utils.df_expr_to_ref('df.global.world'), df.global.world) + expect.eq(utils.df_expr_to_ref('df.global'), df.global) + -- table + expect.eq(utils.df_expr_to_ref('df'), df) + + -- userdata object + expect.eq(utils.df_expr_to_ref('scr'), dfhack.gui.getCurViewscreen()) + + local fake_unit + mock.patch(dfhack.gui, 'getSelectedUnit', function() return fake_unit end, function() + -- lightuserdata field + fake_unit = { + null_field=df.NULL, + } + expect.eq(utils.df_expr_to_ref('unit'), fake_unit) + expect.eq(utils.df_expr_to_ref('unit.null_field'), fake_unit.null_field) + + dfhack.with_temp_object(df.unit:new(), function(u) + fake_unit = u + + -- userdata field + expect.eq(utils.df_expr_to_ref('unit.name'), fake_unit.name) + expect.eq(utils.df_expr_to_ref('unit.name'), fake_unit:_field('name')) + + -- primitive field + expect.eq(utils.df_expr_to_ref('unit.profession'), fake_unit:_field('profession')) + end) + + -- vector items + dfhack.with_temp_object(df.new('ptr-vector'), function(vec) + fake_unit = vec + vec:insert('#', df.global.world) + vec:insert('#', df.global.ui) + + expect.eq(utils.df_expr_to_ref('unit'), vec) + + expect.eq(utils.df_expr_to_ref('unit[0]'), utils.df_expr_to_ref('unit.0')) + expect.eq(df.reinterpret_cast(df.world, utils.df_expr_to_ref('unit[0]').value), df.global.world) + + expect.eq(utils.df_expr_to_ref('unit[1]'), utils.df_expr_to_ref('unit.1')) + expect.eq(df.reinterpret_cast(df.ui, utils.df_expr_to_ref('unit[1]').value), df.global.ui) + + expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit.2') end) + expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit[2]') end) + expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit.-1') end) + expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit[-1]') end) + + expect.error_match('not found', function() utils.df_expr_to_ref('unit.a') end) + end) + end) +end