diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b23e9cdd..669a08db9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,6 +39,6 @@ repos: name: Check Authors.rst language: python entry: python3 ci/authors-rst.py - files: docs/Authors\.rst + files: docs/about/Authors\.rst pass_filenames: false exclude: '^(depends/|data/.*\.json$|.*\.diff$)' diff --git a/CMakeLists.txt b/CMakeLists.txt index dcb5b42ff..37f44a727 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,14 +132,8 @@ endif() find_package(Perl REQUIRED) # set up folder structures for IDE solutions -# MSVC Express won't load solutions that use this. It also doesn't include MFC supported -# Check for MFC! -find_package(MFC QUIET) -if(MFC_FOUND OR (NOT MSVC)) - option(CMAKE_USE_FOLDERS "Enable folder grouping of projects in IDEs." ON) -else() - option(CMAKE_USE_FOLDERS "Enable folder grouping of projects in IDEs." OFF) -endif() +# checking for msvc express is meaningless now, all available editions of msvc support folder groupings +option(CMAKE_USE_FOLDERS "Enable folder grouping of projects in IDEs." ON) if(CMAKE_USE_FOLDERS) set_property(GLOBAL PROPERTY USE_FOLDERS ON) @@ -196,7 +190,7 @@ endif() # set up versioning. set(DF_VERSION "50.05") -set(DFHACK_RELEASE "alpha0") +set(DFHACK_RELEASE "alpha1") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/build/win64/build-debug.bat b/build/win64/build-debug.bat index a04cb9984..2db9df402 100644 --- a/build/win64/build-debug.bat +++ b/build/win64/build-debug.bat @@ -1,4 +1 @@ -call "%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 -cd VC2022 -msbuild /m /p:Platform=x64 /p:Configuration=RelWithDebInfo ALL_BUILD.vcxproj -cd .. +cmake --build VC2022 -t ALL_BUILD -- /m /p:Platform=x64 /p:Configuration=RelWithDebInfo diff --git a/build/win64/build-release.bat b/build/win64/build-release.bat index 8068e5074..f719d64bc 100644 --- a/build/win64/build-release.bat +++ b/build/win64/build-release.bat @@ -1,5 +1 @@ -call "%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 -cd VC2022 -msbuild /m /p:Platform=x64 /p:Configuration=Release ALL_BUILD.vcxproj -cd .. -pause +cmake --build VC2022 -t ALL_BUILD -- /m /p:Platform=x64 /p:Configuration=Release diff --git a/build/win64/generate-MSVC-all.bat b/build/win64/generate-MSVC-all.bat index ea8db34c0..f43a03596 100644 --- a/build/win64/generate-MSVC-all.bat +++ b/build/win64/generate-MSVC-all.bat @@ -1,6 +1,4 @@ IF EXIST DF_PATH.txt SET /P _DF_PATH=}/raw/scripts` (only if a save is loaded) -#. :file:`raw/scripts` +#. :file:`save/{world}/scripts` (only if a save is loaded) #. :file:`hack/scripts` For example, if ``teleport`` is run, these folders are searched in order for -``teleport.lua`` or ``teleport.rb``, and the first matching file is run. +``teleport.lua``, and the first matching file is run. Script paths can be added by modifying :file:`dfhack-config/script-paths.txt`. Each line should start with one of these characters: diff --git a/docs/Installing.rst b/docs/Installing.rst index 05db198b8..bc58a39b7 100644 --- a/docs/Installing.rst +++ b/docs/Installing.rst @@ -50,7 +50,7 @@ The GCC 4.8 build is built on Ubuntu 14.04 and targets an older glibc, so it should work on older distributions. In the event that none of the provided binaries work on your distribution, -you may need to `compile DFHack from source `. +you may need to `compile DFHack from source `. macOS ----- @@ -86,7 +86,7 @@ restrictions apply (e.g. a file named ``Windows64`` is for 64-bit Windows DF). or by clicking "Download ZIP" on the repo homepage. This will give you an incomplete copy of the DFHack source code, which will not work as-is. (If you want to compile DFHack instead of using a pre-built release, see - `compile` for instructions.) + `building-dfhack-index` for instructions.) Installing DFHack ================= diff --git a/docs/Quickstart.rst b/docs/Quickstart.rst new file mode 100644 index 000000000..f6f8920c2 --- /dev/null +++ b/docs/Quickstart.rst @@ -0,0 +1,192 @@ +.. _quickstart: + +Quickstart guide +================ + +Welcome to DFHack! This guide will help get you oriented with the DFHack system +and teach you how to find and use the tools productively. If you're reading this +in `quickstart-guide`, hit the right arrow key or click on the hotkey hint in +the lower right corner of the window to go to the next page. + +What is DFHack? +--------------- + +DFHack is a framework for Dwarf Fortress that provides a unified, cross-platform +environment that enables mods and tools to significantly extend the game. The +default DFHack distribution contains a wide variety of tools, including bugfixes, +interface improvements, automation agents, design blueprints, modding building +blocks, and more. Third-party tools (e.g. mods downloaded from Steam Workshop or +the forums) can also seamlessly integrate with the DFHack framework and extend +the game far beyond what can be done by just modding the raws. + +DFHack's mission is to provide tools and interfaces for players and modders to: + +- expand the bounds of what is possible in Dwarf Fortress +- reduce the impact of game bugs +- give the player more agency and control over the game +- provide alternatives to toilsome or frustrating aspects of gameplay +- **make the game more fun** + +What can I do with DFHack tools? +-------------------------------- + +DFHack has been around for a long time -- almost as long as Dwarf Fortress +itself. Many of the game's rough edges have been smoothed with DFHack tools. +Here are some common tasks people use DFHack tools to accomplish: + +- Automatically chop trees when log stocks are low +- Record blueprint files that allow copy and paste of fort designs +- Import and export lists of manager orders +- Clean contaminants from map squares that dwarves can't reach +- Automatically butcher excess livestock so you don't become overrun with + animals +- Promote time-sensitive job types (e.g. food hauling) so they are done + expediently +- Quickly scan the map for visible ores of specific types so you can focus + your mining efforts + +Some tools are one-shot commands. For example, you can run `unforbid all ` +to claim all items on the map after a messy siege. + +Other tools must be `enabled ` and then they will run in the background. +For example, `enable seedwatch ` will start monitoring your stocks of +seeds and prevent your chefs from cooking seeds that you need for planting. +Tools that are enabled in the context of a fort will save their state with that +fort, and the will remember that they are enabled the next time you load your save. + +A third class of tools add information to the screen or provide new integrated +functionality via the DFHack `overlay` framework. For example, the `unsuspend` +tool, in addition to its basic function of unsuspending all building construction +jobs, can also overlay a marker on suspended buildings to indicate that they are +suspended (and will use different markers to tell you whether this is a problem). +These overlays can be enabled and configured with the `gui/overlay` interface. + +How can I figure out which commands to run? +------------------------------------------- + +There are several ways to scan DFHack tools and find the one you need right now. + +The first place to check is the DFHack logo hover hotspot. It's in the upper +left corner of the screen by default, though you can move it anywhere you want +with the `gui/overlay` tool. + +When you hover the mouse over the logo (or hit the Ctrl-Shift-C keyboard shortcut) +a list of DFHack tools relevant to the current context comes up. For example, when +you have a unit selected, the hotspot will show a list of tools that inspect +units, allow you to edit them, or maybe even teleport them. Next to each tool, +you'll see the global hotkey you can hit to invoke the command without even +opening the hover list. + +You can run any DFHack tool from `gui/launcher`, which is always listed first in +the hover list. You can also bring up the launcher by tapping the backtick key +(\`) or hitting Ctrl-Shift-D. In the launcher, you can quickly autocomplete any +command name by selecting it in the list on the right side of the window. +Commands are ordered by how often you run them, so your favorite commands will +always be on top. You can also pull full commandlines out of your history with +Alt-S (or by clicking on the "history search" hotkey hint). + +Once you have typed (or autocompleted, or searched for) a command, other commands +related to the one you have selected will appear in the autocomplete list. +Scanning through that list is a great way to learn about new tools that you might +find useful. You can also see how commands are grouped by running the `tags` command. + +The bottom panel will show the full help text for the command you are running, +allowing you to refer to the usage documentation and examples when you are typing +your command. + +How do DFHack in-game windows work? +----------------------------------- + +Many DFHack tools have graphical interfaces that appear in-game. You can tell +which windows belong to DFHack tools because they will have the word "DFHack" +printed across their bottom frame edge. DFHack provides a custom windowing system +that gives the player a lot of control over where the windows appear and whether +they capture keyboard and mouse input. + +The DFHack windowing system allows you to use DFHack tools without interrupting +the game. That is, if the game is unpaused, it will continue to run while a +DFHack window is open. You can also interact with the map, scrolling it with the +keyboard or mouse and selecting units, buildings, and items. Some tools will +capture all keyboard input, such as tools with editable text fields, and some will +force-pause the game if it makes sense to, like `gui/quickfort`, since you cannot +interact with the map normally while trying to apply a blueprint. + +DFHack windows are draggable from the title bar or from anywhere on the window +that doesn't have a mouse-clickable widget on it. Many are resizable as well +(if the tool window has components that can reasonably be resized). + +DFHack windows close with a right mouse click or keyboard Esc, but if you +want to keep a DFHack tool open while you interact with the game, you can click the +pin in the upper right corner of the DFHack window or hit Alt-L so +that the pin turns green. The DFHack window will then ignore right clicks and +Esc key presses that would otherwise close the window. This is especially +useful for the configuration tool windows for the automation tools. For example, +you can pin the `gui/autochop` window, set it to minimal mode, and let it sit +there monitoring your logging industry as you play, using it as a live status +window. Note that you can still right click *on* the DFHack tool window to close +it, even when it is pinned. + +You can have multiple DFHack tool windows on the screen at the same time. The +one that is receiving keyboard input has a highlighted title bar and will appear +over other windows if dragged across them. Clicking on a DFHack window that is not +currently active will bring it to the foreground and make it the active window. + +Where do I go next? +------------------- + +To recap: + +You can get to popular, relevant tools for the current context by hovering +the mouse over the DFHack logo or by hitting Ctrl-Shift-C. + +You can get to the launcher and its integrated autocomplete, history search, +and help text by hitting backtick (\`) or Ctrl-Shift-D, or, of course, by +running it from the logo hover list. + +You can list and start tools that run in the background with the `enable` +command. + +You can configure screen overlays with the `gui/overlay` tool. + +With those four tools, you have the complete DFHack tool suite at your +fingertips. So what to run first? Here are a few commands to get you started. +You can run them all from the launcher. + +First, let's import some useful manager orders to keep your fort stocked with +basic necessities. Run ``orders import library/basic``. If you go to your +mangager orders screen, you can see all the orders that have been created for you. + +Next, try setting up `autochop` by running the GUI configuration `gui/autochop`. +You can enable it from the GUI, so you don't need to run `enable autochop ` +directly. You can set a target number of logs, and autochop will manage +your logging industry for you. You can control where your woodsdwarves go to +cut down trees by setting up burrows and configuring autochop to only cut in +those burrows. If you have the extra screen space, go ahead and click the pin so +it turns green and set the `gui/autochop` window to minimal mode (click on the +hint near the upper right corner of the window or hit Alt-M). As you play the game, +you can glance at it to check on your stocks of wood. + +Finally, let's do some fort design copy-pasting. Go to some bedrooms that you have +set up in your fort. Run `gui/blueprint`, set a name for your blueprint by +clicking on the name field (or hitting the 'n' hotkey), typing "rooms" (or whatever) +and hitting Enter to set. Then draw a box around the target area by clicking with +the mouse. When you select the second corner, the blueprint will be saved to your +``blueprints`` subfolder. + +Now open up `gui/quickfort`. You can search for the blueprint you just created by +typing its name, but it should be up near the top already. If you copied a dug-out +area with furniture in it, your blueprint will have two labels: "/dig" and "/build". +Click on the "/dig" blueprint or select it with the keyboard arrow keys and hit Enter. +You can rotate or flip the blueprint around if you need to with the transform hotkeys. +You'll see a preview of where the blueprint will be applied as you move the mouse +cursor around the map. Red outlines mean that the blueprint may fail to fully apply +at that location, so be sure to choose a spot where all the preview tiles are shown +with green diamonds. Click the mouse or hit Enter to apply the blueprint and +designate the tiles for digging. Your dwarves will come and dig it out as if you +had designated the tiles yourself. + +Once the area is dug out, run `gui/quickfort` again and select the "/build" blueprint +this time. Apply the blueprint in the dug-out area, and your furniture will be +designated. It's just that easy! + +There are many, many more tools to explore. Have fun! diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 280594674..b0031607a 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -13,6 +13,7 @@ Name Github Other Abel abstern acwatkins acwatkins Alexander Gavrilov angavrilov ag +Amber Brown hawkowl Amostubal Amostubal Andrea Cattaneo acattaneo88 AndreasPK AndreasPK @@ -46,10 +47,12 @@ David Corbett dscorbett David Seguin dseguin David Timm dtimm Deon +dikbut Tjudge1 DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash doomchild doomchild DwarvenM DwarvenM +EarthPulseAcademy EarthPulseAcademy ElMendukol ElMendukol enjia2000 Eric Wald eswald @@ -66,8 +69,10 @@ Guilherme Abraham GuilhermeAbraham Harlan Playford playfordh Hayati Ayguen hayguen Herwig Hochleitner bendlas +Hevlikn Hevlikn Ian S kremlin- IndigoFenix +James 20k James Gilles kazimuth James Logsdon jlogsdon Jared Adams @@ -80,14 +85,18 @@ Joel Meador janxious John Beisley huin John Shade gsvslto Jonas Ask +Jonathan Clark AridTag Josh Cooper cppcooper coope +jowario jowario kane-t kane-t Kelly Kinkade ab9rf +Kib Arekatír arekatir KlonZK KlonZK Kris Parker kaypy Kristjan Moore kristjanmoore Kromtec Kromtec Kurik Amudnil +Kévin Boissonneault KABoissonneault Lethosor lethosor LordGolias LordGolias Mark Nielson pseudodragon @@ -111,6 +120,7 @@ Milo Christiansen milochristiansen MithrilTuxedo MithrilTuxedo mizipzor mizipzor moversti moversti +Murad Beybalaev Erquint Myk Taylor myk002 napagokc napagokc Neil Little nmlittle @@ -119,7 +129,10 @@ Nicolas Ayala nicolasayala Nik Nyby nikolas Nikolay Amiantov abbradar nocico nocico +NotRexButCaesar NotRexButCaesar +Nuno Fernandes UnknowableCoder Omniclasm +oorzkws oorzkws OwnageIsMagic OwnageIsMagic palenerd dlmarquis PassionateAngler PassionateAngler @@ -133,6 +146,7 @@ Pierre-David Bélanger pierredavidbelanger potato Priit Laes plaes Putnam Putnam3145 +quarque2 quarque2 Quietust quietust _Q Rafał Karczmarczyk CarabusX Raidau Raidau @@ -145,6 +159,7 @@ reverb Rich Rauenzahn rrauenza Rinin Rinin rndmvar rndmvar +Rob Bailey actionninja Robert Heinrich rh73 Robert Janetzko robertjanetzko Rocco Moretti roccomoretti @@ -165,8 +180,10 @@ scamtank scamtank Sebastian Wolfertz Enkrod seishuuu seishuuu Seth Woodworth sethwoodworth +Shim Panze Shim-Panze simon Simon Jackson sizeak +Simon Lees simotek stolencatkarma Stoyan Gaydarov sgayda2 Su Moth-Tolias @@ -198,6 +215,7 @@ Vjek vjek Warmist warmist Wes Malone wesQ3 Will Rogers wjrogers +WoosterUK WoosterUK ZechyW ZechyW Zhentar Zhentar zilpin zilpin diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 0fef8cbe9..ad36ed8a8 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -16,6 +16,12 @@ command-prompt ============== Replaced by `gui/launcher --minimal `. +.. _create-items: + +create-items +============ +Replaced by `gui/create-item --multi `. + .. _deteriorateclothes: deteriorateclothes @@ -124,6 +130,12 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _masspit: + +masspit +======= +Replaced with a GUI version: `gui/masspit`. + .. _resume: resume diff --git a/docs/changelog.txt b/docs/changelog.txt index 08aa39951..c119e0c89 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -36,15 +36,35 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins ## Fixes +- `autofarm`: don't duplicate status line entries for crops with no current supply +- `orders`: allow the orders library to be listed and imported properly (if you previously copied the orders library into your ``dfhack-config/orders`` directory to work around this bug, you can remove those files now) + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 50.05-alpha1 + +## Fixes +- ``widgets.WrappedLabel``: no longer resets scroll position when window is moved or resized ## Misc Improvements - Scrollable widgets now react to mouse wheel events when the mouse is over the widget - the ``dfhack-config/scripts/`` folder is now searched for scripts by default - `hotkeys`: overlay hotspot widget now shows the DFHack logo in graphics mode and "DFHack" in text mode +- `script-paths`: removed "raw" directories from default script paths. now the default locations to search for scripts are ``dfhack-config/scripts``, ``save/*/scripts``, and ``hack/scripts`` +- ``init.d``: directories have moved from the ``raw`` subfolder (which no longer exists) to the root of the main DF folder or a savegame folder ## Documentation - `overlay-dev-guide`: added troubleshooting tips and common development workflows - added DFHack architecture diagrams to the dev intro +- added DFHack Quickstart guide ## API - ``Gui::getDwarfmodeDims``: now only returns map viewport dimensions; menu dimensions are obsolete @@ -67,8 +87,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``gui.CLEAR_PEN``: now clears the background and foreground and writes to the background (before it would always write to the foreground) - ``gui.KEEP_LOWER_PEN``: a general use pen that writes associated tiles to the foreground while keeping the existing background -## Internals - ## Removed - ``fix-job-postings`` from the `workflow` plugin is now obsolete since affected savegames can no longer be loaded - Ruby is no longer a supported DFHack scripting language diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 33b48c0a7..ebd82ce19 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1144,6 +1144,10 @@ Job module Prints info about the job item. +* ``dfhack.job.removeJob(job)`` + + Cancels a job, cleans up all references to it, and removes it from the world. + * ``dfhack.job.getGeneralRef(job, type)`` Searches for a general_ref with the given type. @@ -1994,6 +1998,11 @@ Low-level building creation functions: Destroys the building, or queues a deconstruction job. Returns *true* if the building was destroyed and deallocated immediately. +* ``dfhack.buildings.notifyCivzoneModified(building)`` + + Rebuilds the civzone <-> overlapping building association mapping. + Call after changing extents or modifying size in some fashion + * ``dfhack.buildings.markedForRemoval(building)`` Returns *true* if the building is marked for removal (with :kbd:`x`), *false* @@ -4416,6 +4425,13 @@ Subclass of Panel; automatically adjusts its own frame height and width to the minimum required to show its subviews. Pairs nicely with a parent Panel that has ``autoarrange_subviews`` enabled. +It has the following attributes: + +:auto_height: Sets self.frame.h from the positions and height of its subviews + (default is ``true``). +:auto_width: Sets self.frame.w from the positions and width of its subviews + (default is ``false``). + Pages class ----------- @@ -4468,9 +4484,10 @@ calling ``setFocus(true)`` on the field object. If an activation ``key`` is specified, the ``EditField`` will manage its own focus. It will start in the unfocused state, and pressing the activation key will acquire keyboard focus. Pressing the Enter key will release keyboard focus -and then call the ``on_submit`` callback. Pressing the Escape key will also -release keyboard focus, but first it will restore the text that was displayed -before the ``EditField`` gained focus and then call the ``on_change`` callback. +and then call the ``on_submit`` callback. Pressing the Escape key (or r-clicking +with the mouse) will also release keyboard focus, but first it will restore the +text that was displayed before the ``EditField`` gained focus and then call the +``on_change`` callback. The ``EditField`` cursor can be moved to where you want to insert/remove text. You can click where you want the cursor to move or you can use any of the @@ -5800,9 +5817,13 @@ Enabling and disabling scripts ============================== Scripts can choose to recognize the built-in ``enable`` and ``disable`` commands -by including the following line anywhere in their file:: +by including the following line near the top of their file:: + + --@enable = true + --@module = true - --@ enable = true +Note that enableable scripts must also be `modules ` so their +``isEnabled()`` functions can be called from outside the script. When the ``enable`` and ``disable`` commands are invoked, the ``dfhack_flags`` table passed to the script will have the following fields set: @@ -5817,7 +5838,8 @@ command. Example usage:: - --@ enable = true + --@enable = true + --@module = true enabled = enabled or false function isEnabled() @@ -5871,9 +5893,9 @@ all script modules. Save init script ================ -If a save directory contains a file called ``raw/init.lua``, it is +If a save directory contains a file called ``init.lua``, it is automatically loaded and executed every time the save is loaded. -The same applies to any files called ``raw/init.d/*.lua``. Every +The same applies to any files called ``init.d/*.lua``. Every such script can define the following functions to be called by dfhack: * ``function onStateChange(op) ... end`` diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 9fe62bf6a..73d9407d6 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -61,9 +61,8 @@ be: 1. ``own-scripts/`` 2. ``dfhack-config/scripts/`` -3. ``data/save/*/raw/scripts/`` -4. ``raw/scripts/`` -5. ``hack/scripts/`` +3. ``save/*/scripts/`` +4. ``hack/scripts/`` The structure of the game ------------------------- @@ -359,7 +358,7 @@ names for the mod folders within it. In the example below, we'll use a mod ID of ``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 + init.d/example-mod.lua raw/objects/... raw/scripts/example-mod.lua raw/scripts/example-mod/... @@ -367,13 +366,13 @@ directory and has a basic structure that looks like this:: Let's go through that line by line. -* A short (one-line) script in ``raw/init.d/`` to initialise your +* A short (one-line) script in ``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 +* A control script in ``scripts/`` that handles enabling and disabling your mod. -* A subfolder for your mod under ``raw/scripts/`` will contain all the internal +* A subfolder for your mod under ``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 @@ -385,10 +384,10 @@ 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/ + +/path/to/mymods/example-mod/scripts/ Ok, you're all set up! Now, let's take a look at an example -``raw/scripts/example-mod.lua`` file:: +``scripts/example-mod.lua`` file:: -- main setup and teardown for example-mod -- this next line indicates that the script supports the "enable" @@ -475,7 +474,7 @@ Ok, you're all set up! Now, let's take a look at an example 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``:: +``init.d/example-mod.lua``:: dfhack.run_command('enable example-mod') diff --git a/docs/plugins/RemoteFortressReader.rst b/docs/plugins/RemoteFortressReader.rst index 9b45452bc..c70adc608 100644 --- a/docs/plugins/RemoteFortressReader.rst +++ b/docs/plugins/RemoteFortressReader.rst @@ -3,7 +3,7 @@ RemoteFortressReader .. dfhack-tool:: :summary: Backend for Armok Vision. - :tags: untested dev graphics + :tags: dev graphics :no-command: .. dfhack-command:: RemoteFortressReader_version diff --git a/docs/plugins/autodump.rst b/docs/plugins/autodump.rst index ec83b6082..facf70ca9 100644 --- a/docs/plugins/autodump.rst +++ b/docs/plugins/autodump.rst @@ -2,48 +2,32 @@ autodump ======== .. dfhack-tool:: - :summary: Automatically set items in a stockpile to be dumped. - :tags: untested fort armok fps productivity items stockpiles - :no-command: - -.. dfhack-command:: autodump - :summary: Teleports items marked for dumping to the cursor position. + :summary: Instantly gather or destroy items marked for dumping. + :tags: fort armok fps items .. dfhack-command:: autodump-destroy-here - :summary: Destroy items marked for dumping under the cursor. - -.. dfhack-command:: autodump-destroy-item - :summary: Destroys the selected item. + :summary: Destroy items marked for dumping under the keyboard cursor. -When `enabled `, this plugin adds an option to the :kbd:`q` menu for -stockpiles. When the ``autodump`` option is selected for the stockpile, any -items placed in the stockpile will automatically be designated to be dumped. +This tool can instantly move all unforbidden items marked for dumping to the +tile under the keyboard cursor. After moving the items, the dump flag is unset +and the forbid flag is set, just as if it had been dumped normally. Be aware +that dwarves that are en route to pick up the item for dumping may still come +and move the item to your dump zone. -When invoked as a command, it can instantly move all unforbidden items marked -for dumping to the tile under the cursor. After moving the items, the dump flag -is unset and the forbid flag is set, just as if it had been dumped normally. Be -aware that dwarves that are en route to pick up the item for dumping may still -come and move the item to your dump zone. - -The cursor must be placed on a floor tile so the items can be dumped there. +The keyboard cursor must be placed on a floor tile so the items can be dumped +there. Usage ----- :: - enable autodump autodump [] autodump-destroy-here - autodump-destroy-item ``autodump-destroy-here`` is an alias for ``autodump destroy-here`` and is intended for use as a keybinding. -``autodump-destroy-item`` destroys only the selected item. The item may be -selected in the :kbd:`k` list or in the container item list. If called again -before the game is resumed, cancels destruction of the item. - Options ------- @@ -67,5 +51,5 @@ Examples Teleports items marked for dumping to the cursor position. ``autodump destroy`` Destroys all unforbidden items marked for dumping -``autodump-destroy-item`` - Destroys the selected item. +``autodump-destroy-here`` + Destroys items on the selected tile that are marked for dumping. diff --git a/docs/plugins/blueprint.rst b/docs/plugins/blueprint.rst index aa2d0fa03..b10921d30 100644 --- a/docs/plugins/blueprint.rst +++ b/docs/plugins/blueprint.rst @@ -3,7 +3,7 @@ blueprint .. dfhack-tool:: :summary: Record a live game map in a quickfort blueprint. - :tags: untested fort design buildings map stockpiles + :tags: fort design buildings map stockpiles With ``blueprint``, you can export the structure of a portion of your fortress in a blueprint file that you (or anyone else) can later play back with @@ -15,6 +15,13 @@ selected interactively with the ``gui/blueprint`` command or, if the GUI is not used, starts at the active cursor location and extends right and down for the requested width and height. +.. admonition:: Note + + blueprint is still in the process of being updated for the new version of + DF. Stockpiles (the "place" phase), zones (the "zone" phase), building + configuration (the "query" phase), and game configuration (the "config" + phase) are not yet supported. + Usage ----- diff --git a/docs/plugins/cleanconst.rst b/docs/plugins/cleanconst.rst index 420635fe2..12b1db0e5 100644 --- a/docs/plugins/cleanconst.rst +++ b/docs/plugins/cleanconst.rst @@ -3,7 +3,7 @@ cleanconst .. dfhack-tool:: :summary: Cleans up construction materials. - :tags: untested fort fps buildings + :tags: fort fps buildings This tool alters all constructions on the map so that they spawn their building component when they are disassembled, allowing their actual build items to be diff --git a/docs/plugins/cleaners.rst b/docs/plugins/cleaners.rst index dd484f5df..14a25d62f 100644 --- a/docs/plugins/cleaners.rst +++ b/docs/plugins/cleaners.rst @@ -6,7 +6,7 @@ cleaners .. dfhack-tool:: :summary: Provides commands for cleaning spatter from the map. - :tags: untested adventure fort armok fps items map units + :tags: adventure fort armok fps items map units :no-command: .. dfhack-command:: clean diff --git a/docs/plugins/cursecheck.rst b/docs/plugins/cursecheck.rst index fd97ee252..94012919b 100644 --- a/docs/plugins/cursecheck.rst +++ b/docs/plugins/cursecheck.rst @@ -3,7 +3,7 @@ cursecheck .. dfhack-tool:: :summary: Check for cursed creatures. - :tags: untested fort armok inspection units + :tags: fort armok inspection units This command checks a single map tile (or the whole map/world) for cursed creatures (ghosts, vampires, necromancers, werebeasts, zombies, etc.). diff --git a/docs/plugins/fastdwarf.rst b/docs/plugins/fastdwarf.rst index 217c098ef..fb0524c3b 100644 --- a/docs/plugins/fastdwarf.rst +++ b/docs/plugins/fastdwarf.rst @@ -3,7 +3,7 @@ fastdwarf .. dfhack-tool:: :summary: Dwarves teleport and/or finish jobs instantly. - :tags: untested fort armok units + :tags: fort armok units Usage ----- diff --git a/docs/plugins/filltraffic.rst b/docs/plugins/filltraffic.rst index f92549dee..beb57dccd 100644 --- a/docs/plugins/filltraffic.rst +++ b/docs/plugins/filltraffic.rst @@ -6,7 +6,7 @@ filltraffic .. dfhack-tool:: :summary: Set traffic designations using flood-fill starting at the cursor. - :tags: untested fort design productivity map + :tags: fort design productivity map .. dfhack-command:: alltraffic :summary: Set traffic designations for every single tile of the map. diff --git a/docs/plugins/misery.rst b/docs/plugins/misery.rst index 38c2b23b4..f2c4d1952 100644 --- a/docs/plugins/misery.rst +++ b/docs/plugins/misery.rst @@ -3,7 +3,7 @@ misery .. dfhack-tool:: :summary: Increase the intensity of negative dwarven thoughts. - :tags: untested fort armok auto units + :tags: fort armok auto units When enabled, negative thoughts that your dwarves have will multiply by the specified factor. diff --git a/docs/plugins/nestboxes.rst b/docs/plugins/nestboxes.rst index 91d83aa1a..3c07cc30c 100644 --- a/docs/plugins/nestboxes.rst +++ b/docs/plugins/nestboxes.rst @@ -3,7 +3,7 @@ nestboxes .. dfhack-tool:: :summary: Protect fertile eggs incubating in a nestbox. - :tags: untested fort auto animals + :tags: fort auto animals :no-command: This plugin will automatically scan for and forbid fertile eggs incubating in a diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index bc15fd175..8beb246e4 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -23,7 +23,7 @@ Usage one-time orders first, then yearly, seasonally, monthly, and finally, daily. You can keep your orders automatically sorted by adding the following command to -your ``onMapLoad.init`` file:: +your ``dfhack-config/init/onMapLoad.init`` file:: repeat -name orders-sort -time 1 -timeUnits days -command [ orders sort ] diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index d578fd308..af7d253b2 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -3,7 +3,7 @@ tailor .. dfhack-tool:: :summary: Automatically keep your dwarves in fresh clothing. - :tags: untested fort auto workorders + :tags: fort auto workorders Whenever the bookkeeper updates stockpile records, this plugin will scan the fort. If there are fresh cloths available, dwarves who are wearing tattered diff --git a/index.rst b/index.rst index af0f76052..9ebc010eb 100644 --- a/index.rst +++ b/index.rst @@ -15,9 +15,10 @@ Quick Links * `Downloads `_ * `Installation guide ` +* `quickstart` * `Getting help ` * :source:`Source code <>` - (**important:** read `compile` before attempting to build from source.) + (**important:** read `building-dfhack-index` before attempting to build from source.) User Manual =========== @@ -26,6 +27,7 @@ User Manual :maxdepth: 2 /docs/Introduction + /docs/Quickstart /docs/Installing /docs/Core /docs/Tools diff --git a/library/Core.cpp b/library/Core.cpp index 78257a262..e2c983116 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -472,9 +472,8 @@ void Core::getScriptPaths(std::vector *dest) if (df::global::world && isWorldLoaded()) { string save = World::ReadWorldFolder(); if (save.size()) - dest->push_back(df_path + "/data/save/" + save + "/raw/scripts"); + dest->push_back(df_path + "/save/" + save + "/scripts"); } - dest->push_back(df_path + "/raw/scripts"); dest->push_back(df_path + "/hack/scripts"); for (auto it = script_paths[1].begin(); it != script_paths[1].end(); ++it) dest->push_back(*it); @@ -2053,7 +2052,7 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::string rawFolder = "data/save/" + (df::global::world->cur_savegame.save_dir) + "/raw/"; + std::string rawFolder = "save/" + (df::global::world->cur_savegame.save_dir) + "/init"; auto i = table.find(event); if ( i != table.end() ) { @@ -2064,7 +2063,6 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve loadScriptFiles(this, out, set, CONFIG_PATH + "init"); loadScriptFiles(this, out, set, rawFolder); - loadScriptFiles(this, out, set, rawFolder + "objects/"); } for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) @@ -2125,7 +2123,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) case SC_MAP_UNLOADED: if (world && world->cur_savegame.save_dir.size()) { - std::string save_dir = "data/save/" + world->cur_savegame.save_dir; + std::string save_dir = "save/" + world->cur_savegame.save_dir; std::string evtlogpath = save_dir + "/events-dfhack.log"; std::ofstream evtlog; evtlog.open(evtlogpath, std::ios_base::app); // append diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 90815708d..84612f790 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1692,6 +1692,7 @@ static const LuaWrapper::FunctionReg dfhack_textures_module[] = { WRAPM(Textures, getDfhackLogoTexposStart), WRAPM(Textures, getGreenPinTexposStart), WRAPM(Textures, getRedPinTexposStart), + WRAPM(Textures, getIconsTexposStart), { NULL, NULL } }; @@ -2238,6 +2239,7 @@ static const LuaWrapper::FunctionReg dfhack_buildings_module[] = { WRAPM(Buildings, constructWithItems), WRAPM(Buildings, constructWithFilters), WRAPM(Buildings, deconstruct), + WRAPM(Buildings, notifyCivzoneModified), WRAPM(Buildings, markedForRemoval), WRAPM(Buildings, getRoomDescription), WRAPM(Buildings, isActivityZone), diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index d089640ef..124987535 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -23,17 +23,20 @@ distribution. */ #pragma once + #include "Export.h" + #include -#include -#include #include #include -#include -#include -#include #include +#include +#include +#include #include +#include +#include +#include #if defined(_MSC_VER) #define DFHACK_FUNCTION_SIG __FUNCSIG__ @@ -338,6 +341,24 @@ inline typename T::mapped_type map_find( return (it == map.end()) ? defval : it->second; } +template +static void for_each_(std::vector &v, Fn func) +{ + std::for_each(v.begin(), v.end(), func); +} + +template +static void for_each_(std::map &v, Fn func) +{ + std::for_each(v.begin(), v.end(), func); +} + +template +static void transform_(const std::vector &src, std::vector &dst, Fn func) +{ + std::transform(src.begin(), src.end(), std::back_inserter(dst), func); +} + DFHACK_EXPORT bool prefix_matches(const std::string &prefix, const std::string &key, std::string *tail = NULL); template diff --git a/library/include/df/custom/coord.methods.inc b/library/include/df/custom/coord.methods.inc index 6814be3c2..aa6eda484 100644 --- a/library/include/df/custom/coord.methods.inc +++ b/library/include/df/custom/coord.methods.inc @@ -3,7 +3,7 @@ coord(uint16_t _x, uint16_t _y, uint16_t _z) : x(_x), y(_y), z(_z) {} operator coord2d() const { return coord2d(x,y); } -bool isValid() const { return x != -30000; } +bool isValid() const { return x >= 0; } void clear() { x = y = z = -30000; } bool operator==(const coord &other) const diff --git a/library/include/df/custom/coord2d.methods.inc b/library/include/df/custom/coord2d.methods.inc index 202192bb8..512149ce3 100644 --- a/library/include/df/custom/coord2d.methods.inc +++ b/library/include/df/custom/coord2d.methods.inc @@ -1,6 +1,6 @@ coord2d(uint16_t _x, uint16_t _y) : x(_x), y(_y) {} -bool isValid() const { return x != -30000; } +bool isValid() const { return x >= 0; } void clear() { x = y = -30000; } bool operator==(const coord2d &other) const diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index aa539c02a..f4e8f37be 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -202,6 +202,11 @@ DFHACK_EXPORT bool deconstruct(df::building *bld); */ DFHACK_EXPORT bool markedForRemoval(df::building *bld); +/** + * Rebuilds a civzones building associations after it has been modified +*/ +DFHACK_EXPORT void notifyCivzoneModified(df::building* bld); + void updateBuildings(color_ostream& out, void* ptr); void clearBuildings(color_ostream& out); diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index e088ce477..e3e5a8ec0 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -31,9 +31,15 @@ void cleanup(); DFHACK_EXPORT long getDfhackLogoTexposStart(); /** - * Get the texpos for the UI pin tiles. Each are 2x2 grids. + * Get the first texpos for the UI pin tiles. Each are 2x2 grids. */ DFHACK_EXPORT long getGreenPinTexposStart(); DFHACK_EXPORT long getRedPinTexposStart(); + +/** + * Get the first texpos for the DFHack icons. It's a 5x2 grid. + */ +DFHACK_EXPORT long getIconsTexposStart(); + } } diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 26794eedf..b931b2ff4 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -321,7 +321,7 @@ end function pos2xyz(pos) if pos then local x = pos.x - if x and x ~= -30000 then + if x and x >= 0 then return x, pos.y, pos.z end end @@ -346,7 +346,7 @@ end function pos2xy(pos) if pos then local x = pos.x - if x and x ~= -30000 then + if x and x >= 0 then return x, pos.y end end @@ -698,11 +698,14 @@ local valid_script_flags = { local warned_scripts = {} function dfhack.run_script(name,...) - if not warned_scripts[name] and require('helpdb').get_entry_tags(name).untested then - warned_scripts[name] = true - dfhack.printerr(('UNTESTED WARNING: the "%s" script has not been validated to work well with this version of DF.'):format(name)) - dfhack.printerr('It may not work as expected, or it may corrupt your game.') - qerror('Please run the command again to ignore this warning and proceed.') + if not warned_scripts[name] then + local helpdb = require('helpdb') + if helpdb.is_entry(name) and helpdb.get_entry_tags(name).untested then + warned_scripts[name] = true + dfhack.printerr(('UNTESTED WARNING: the "%s" script has not been validated to work well with this version of DF.'):format(name)) + dfhack.printerr('It may not work as expected, or it may corrupt your game.') + qerror('Please run the command again to ignore this warning and proceed.') + end end return dfhack.run_script_with_env(nil, name, nil, ...) @@ -868,7 +871,7 @@ end function dfhack.getSavePath() if dfhack.isWorldLoaded() then - return dfhack.getDFPath() .. '/data/save/' .. df.global.world.cur_savegame.save_dir + return dfhack.getDFPath() .. '/save/' .. df.global.world.cur_savegame.save_dir end end @@ -902,14 +905,14 @@ if dfhack.is_core_context then local path = dfhack.getSavePath() if path and op == SC_WORLD_LOADED then - loadInitFile(path, path..'/raw/init.lua') + loadInitFile(path, path..'/init.lua') - local dirlist = dfhack.internal.getDir(path..'/raw/init.d/') + local dirlist = dfhack.internal.getDir(path..'/init.d/') if dirlist then table.sort(dirlist) for i,name in ipairs(dirlist) do if string.match(name,'%.lua$') then - loadInitFile(path, path..'/raw/init.d/'..name) + loadInitFile(path, path..'/init.d/'..name) end end end diff --git a/library/lua/gui.lua b/library/lua/gui.lua index e7f59a148..530848f61 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -481,7 +481,7 @@ end function View:getMousePos(view_rect) local rect = view_rect or self.frame_body local x,y = dscreen.getMousePos() - if rect and rect:inClipGlobalXY(x,y) then + if rect and x and rect:inClipGlobalXY(x,y) then return rect:localXY(x,y) end end @@ -725,6 +725,7 @@ function ZScreen:onInput(keys) if ZScreen.super.onInput(self, keys) then -- ensure underlying DF screens don't also react to handled clicks if keys._MOUSE_L_DOWN then + -- note we can't clear mouse_lbut here. otherwise we break dragging, df.global.enabler.mouse_lbut_down = 0 end if keys._MOUSE_R_DOWN then diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index bb381be24..675a7228e 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -286,6 +286,19 @@ function get_hotkey_target(key) end end +function getMapKey(keys) + for code in pairs(keys) do + if MOVEMENT_KEYS[code] or HOTKEY_KEYS[code] + or code == '_MOUSE_M_DOWN' or code == '_MOUSE_M' + or code == 'ZOOM_OUT' or code == 'ZOOM_IN' then + if not HOTKEY_KEYS[code] or get_hotkey_target(code) then + return true + end + return code + end + end +end + function Viewport:scrollByKey(key) local dx, dy, dz = get_movement_delta(key, 10, 20) if dx then @@ -339,13 +352,9 @@ function DwarfOverlay:selectBuilding(building,cursor,viewport,gap) end function DwarfOverlay:propagateMoveKeys(keys) - for code,_ in pairs(keys) do - if MOVEMENT_KEYS[code] or HOTKEY_KEYS[code] then - if not HOTKEY_KEYS[code] or get_hotkey_target(code) then - self:sendInputToParent(code) - end - return code - end + local map_key = getMapKey(keys) + if map_key then + self:sendInputToParent(map_key) end end @@ -414,124 +423,45 @@ function DwarfOverlay:onAboutToShow(parent) end end -MenuOverlay = defclass(MenuOverlay, DwarfOverlay) - -MenuOverlay.ATTRS { - frame_inset = 0, - frame_background = gui.CLEAR_PEN, - - -- if sidebar_mode is set, we will enter the specified sidebar mode on show - -- and restore the previous sidebar mode on dismiss. otherwise it is up to - -- the caller to ensure we are in a sidebar mode where the menu is visible. - sidebar_mode = DEFAULT_NIL, -} - -function MenuOverlay:init() - if not dfhack.isMapLoaded() then - -- sidebar menus are only valid when a fort map is loaded - error('A fortress map must be loaded.') - end - - if self.sidebar_mode then - self.saved_sidebar_mode = df.global.plotinfo.main.mode - -- what mode should we restore when this window is dismissed? ideally, we'd - -- restore the mode that the user has set, but we should fall back to - -- restoring the default mode if either of the following conditions are - -- true: - -- 1) enterSidebarMode doesn't support getting back into the current mode - -- 2) a dfhack viewscreen is currently visible. in this case, we can't trust - -- that the current sidebar mode was set by the user. it could just be a - -- MenuOverlay subclass that is currently being shown that has set the - -- sidebar mode for its own purposes. - if not SIDEBAR_MODE_KEYS[self.saved_sidebar_mode] - or dfhack.gui.getCurFocus(true):find('^dfhack/') then - self.saved_sidebar_mode = df.ui_sidebar_mode.Default - end - - enterSidebarMode(self.sidebar_mode) - end -end - -function MenuOverlay:computeFrame(parent_rect) - return self.df_layout.menu, gui.inset_frame(self.df_layout.menu, self.frame_inset) -end - -function MenuOverlay:onAboutToShow(parent) - self:updateLayout() - if not self.df_layout.menu then - error("The menu panel of dwarfmode is not visible") - end -end - -function MenuOverlay:onDismiss() - if self.saved_sidebar_mode then - enterSidebarMode(self.saved_sidebar_mode) - end -end - -function MenuOverlay:render(dc) - self:renderParent() - - local menu = self.df_layout.menu - if menu then - -- Paint signature on the frame. - dscreen.paintString( - {fg=COLOR_BLACK,bg=COLOR_DARKGREY}, - menu.x1+1, menu.y2+1, "DFHack" - ) - - if self.frame_background then - dc:fill(menu, self.frame_background) - end - - MenuOverlay.super.render(self, dc) - end -end - -- Framework for managing rendering over the map area. This function is intended --- to be called from a subclass's onRenderBody() function. +-- to be called from a window's onRenderFrame() function. -- --- get_overlay_char_fn takes a coordinate position and an is_cursor boolean and --- returns the char to render at that position and, optionally, the foreground --- and background colors to use to draw the char. If nothing should be rendered --- at that position, the function should return nil. If no foreground color is --- specified, it defaults to COLOR_GREEN. If no background color is specified, --- it defaults to COLOR_BLACK. +-- get_overlay_pen_fn takes a coordinate position and an is_cursor boolean and +-- returns the pen (and optional char and tile) to render at that position. If +-- nothing should be rendered at that position, the function should return nil. -- -- bounds_rect has elements {x1, x2, y1, y2} in global map coordinates (not -- screen coordinates). The rect is intersected with the visible map viewport to -- get the range over which get_overlay_char_fn is called. If bounds_rect is not -- specified, the entire viewport is scanned. -- --- example call from a subclass: --- function MyMenuOverlaySubclass:onRenderBody() --- local function get_overlay_char(pos) --- return safe_index(self.overlay_chars, pos.z, pos.y, pos.x), COLOR_RED +-- example call: +-- function MyMapOverlay:onRenderFrame(dc, rect) +-- local function get_overlay_pen(pos) +-- if safe_index(self.overlay_map, pos.z, pos.y, pos.x) then +-- return COLOR_GREEN, 'X', dfhack.screen.findGraphicsTile('CURSORS', 4, 3) +-- end -- end --- self:renderMapOverlay(get_overlay_char, self.overlay_bounds) +-- guidm.renderMapOverlay(get_overlay_pen, self.overlay_bounds) -- end -function MenuOverlay:renderMapOverlay(get_overlay_char_fn, bounds_rect) - local vp = self:getViewport() +function renderMapOverlay(get_overlay_pen_fn, bounds_rect) + local vp = Viewport.get() local rect = gui.ViewRect{rect=vp, clip_view=bounds_rect and gui.ViewRect{rect=bounds_rect} or nil} - -- nothing to do if the viewport is completely separate from the bounds_rect + -- nothing to do if the viewport is completely disjoint from the bounds_rect if rect:isDefunct() then return end - local dc = gui.Painter.new(self.df_layout.map) local z = df.global.window_z local cursor = getCursorPos() for y=rect.clip_y1,rect.clip_y2 do for x=rect.clip_x1,rect.clip_x2 do local pos = xyz2pos(x, y, z) - local overlay_char, fg_color, bg_color = get_overlay_char_fn( - pos, same_xy(cursor, pos)) - if not overlay_char then goto continue end - local stile = vp:tileToScreen(pos) - dc:map(true):seek(stile.x, stile.y): - pen(fg_color or COLOR_GREEN, bg_color or COLOR_BLACK): - char(overlay_char):map(false) - ::continue:: + local overlay_pen, char, tile = get_overlay_pen_fn(pos, same_xy(cursor, pos)) + if overlay_pen then + local stile = vp:tileToScreen(pos) + dscreen.paintTile(overlay_pen, stile.x, stile.y, char, tile, true) + end end end end diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 9293e737c..f56eff096 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -456,6 +456,20 @@ end function Panel:computeFrame(parent_rect) local sw, sh = parent_rect.width, parent_rect.height + if self.frame then + if self.frame.t and self.frame.h and self.frame.t + self.frame.h > sh then + self.frame.t = math.max(0, sh - self.frame.h) + end + if self.frame.b and self.frame.h and self.frame.b + self.frame.h > sh then + self.frame.b = math.max(0, sh - self.frame.h) + end + if self.frame.l and self.frame.w and self.frame.l + self.frame.w > sw then + self.frame.l = math.max(0, sw - self.frame.w) + end + if self.frame.r and self.frame.w and self.frame.r + self.frame.w > sw then + self.frame.r = math.max(0, sw - self.frame.w) + end + end return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset, self.frame_style and 1 or 0) end @@ -530,6 +544,11 @@ Window.ATTRS { ResizingPanel = defclass(ResizingPanel, Panel) +ResizingPanel.ATTRS{ + auto_height = true, + auto_width = false, +} + -- adjust our frame dimensions according to positions and sizes of our subviews function ResizingPanel:postUpdateLayout(frame_body) local w, h = 0, 0 @@ -550,6 +569,8 @@ function ResizingPanel:postUpdateLayout(frame_body) end if not self.frame then self.frame = {} end local oldw, oldh = self.frame.w, self.frame.h + if not self.auto_height then h = oldh end + if not self.auto_width then w = oldw end self.frame.w, self.frame.h = w, h if not self._updateLayoutGuard and (oldw ~= w or oldh ~= h) then self._updateLayoutGuard = true -- protect against infinite loops @@ -708,7 +729,7 @@ function EditField:onInput(keys) end end - if self.key and keys.LEAVESCREEN then + if self.key and (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) then local old = self.text self:setText(self.saved_text) if self.on_change and old ~= self.saved_text then @@ -1359,6 +1380,10 @@ function WrappedLabel:getWrappedText(width) return text_to_wrap:wrap(width - self.indent) end +function WrappedLabel:preUpdateLayout() + self.saved_start_line_num = self.start_line_num +end + -- we can't set the text in init() since we may not yet have a frame that we -- can get wrapping bounds from. function WrappedLabel:postComputeFrame() @@ -1371,6 +1396,7 @@ function WrappedLabel:postComputeFrame() table.insert(text, NEWLINE) end self:setText(text) + self:scroll(self.saved_start_line_num - 1) end ------------------ @@ -1694,7 +1720,7 @@ function List:onRenderBody(dc) local function paint_icon(icon, obj) if type(icon) ~= 'string' then - dc:char(nil,icon) + dc:tile(nil,icon) else if current then dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 2e7143533..653f8bccf 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -102,7 +102,6 @@ struct CoordHash { }; static unordered_map locationToBuilding; -static unordered_map, CoordHash> locationToCivzones; static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) { @@ -167,6 +166,163 @@ void buildings_onUpdate(color_ostream &out) } } +static void building_into_zone_unidir(df::building* bld, df::building_civzonest* zone) +{ + for (size_t bid = 0; bid < zone->contained_buildings.size(); bid++) + { + if (zone->contained_buildings[bid] == bld) + return; + } + + zone->contained_buildings.push_back(bld); + + std::sort(zone->contained_buildings.begin(), zone->contained_buildings.end(), [](df::building* b1, df::building* b2) + { + return b1->id < b2->id; + }); +} + +static void zone_into_building_unidir(df::building* bld, df::building_civzonest* zone) +{ + for (size_t bid = 0; bid < bld->relations.size(); bid++) + { + if (bld->relations[bid] == zone) + return; + } + + bld->relations.push_back(zone); + + std::sort(bld->relations.begin(), bld->relations.end(), [](df::building* b1, df::building* b2) + { + return b1->id < b2->id; + }); +} + +static bool is_suitable_building_for_zoning(df::building* bld) +{ + return bld->canMakeRoom(); +} + +static void add_building_to_zone(df::building* bld, df::building_civzonest* zone) +{ + if (!is_suitable_building_for_zoning(bld)) + return; + + building_into_zone_unidir(bld, zone); + zone_into_building_unidir(bld, zone); +} + +static void add_building_to_all_zones(df::building* bld) +{ + if (!is_suitable_building_for_zoning(bld)) + return; + + df::coord coord(bld->centerx, bld->centery, bld->z); + + std::vector cv; + Buildings::findCivzonesAt(&cv, coord); + + for (size_t i=0; i < cv.size(); i++) + { + add_building_to_zone(bld, cv[i]); + } +} + +static void add_zone_to_all_buildings(df::building* zone_as_building) +{ + if (zone_as_building->getType() != building_type::Civzone) + return; + + auto zone = strict_virtual_cast(zone_as_building); + + if (zone == nullptr) + return; + + auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; + + for (size_t i = 0; i < vec.size(); i++) + { + auto against = vec[i]; + + if (zone->z != against->z) + continue; + + if (!is_suitable_building_for_zoning(against)) + continue; + + int32_t cx = against->centerx; + int32_t cy = against->centery; + + df::coord2d coord(cx, cy); + + //can a zone without extents exist? + if (zone->room.extents && zone->isExtentShaped()) + { + auto etile = getExtentTile(zone->room, coord); + if (!etile || !*etile) + continue; + + add_building_to_zone(against, zone); + } + } +} + +static void remove_building_from_zone(df::building* bld, df::building_civzonest* zone) +{ + for (int bid = 0; bid < (int)zone->contained_buildings.size(); bid++) + { + if (zone->contained_buildings[bid] == bld) + { + zone->contained_buildings.erase(zone->contained_buildings.begin() + bid); + bid--; + } + } + + for (int bid = 0; bid < (int)bld->relations.size(); bid++) + { + if (bld->relations[bid] == zone) + { + bld->relations.erase(bld->relations.begin() + bid); + bid--; + } + } +} + +static void remove_building_from_all_zones(df::building* bld) +{ + df::coord coord(bld->centerx, bld->centery, bld->z); + + std::vector cv; + Buildings::findCivzonesAt(&cv, coord); + + for (size_t i=0; i < cv.size(); i++) + { + remove_building_from_zone(bld, cv[i]); + } +} + +static void remove_zone_from_all_buildings(df::building* zone_as_building) +{ + if (zone_as_building->getType() != building_type::Civzone) + return; + + auto zone = strict_virtual_cast(zone_as_building); + + if (zone == nullptr) + return; + + auto& vec = world->buildings.other[buildings_other_id::IN_PLAY]; + + //this is a paranoid check and slower than it could be. Zones contain a list of children + //good for fixing potentially bad game states when deleting a zone + for (size_t i = 0; i < vec.size(); i++) + { + df::building* bld = vec[i]; + + remove_building_from_zone(bld, zone); + } +} + uint32_t Buildings::getNumBuildings() { return world->buildings.all.size(); @@ -325,78 +481,30 @@ static void cacheBuilding(df::building *building, bool is_civzone) { for (int32_t y = p1.y; y <= p2.y; y++) { df::coord pt(x, y, building->z); if (Buildings::containsTile(building, pt, is_civzone)) { - if (is_civzone) - locationToCivzones[pt].push_back(id); - else + if (!is_civzone) locationToBuilding[pt] = id; } } } } -static int32_t nextCivzone = 0; -static void cacheNewCivzones() { - if (!world || !building_next_id) - return; - - int32_t nextBuildingId = *building_next_id; - for (int32_t id = nextCivzone; id < nextBuildingId; ++id) { - auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; - int32_t idx = df::building::binsearch_index(vec, id); - if (idx > -1) - cacheBuilding(vec[idx], true); - } - nextCivzone = nextBuildingId; -} - bool Buildings::findCivzonesAt(std::vector *pvec, df::coord pos) { pvec->clear(); - // Tiles have an occupancy->bits.building flag to quickly determine if it is - // covered by a buildling, but there is no such flag for civzones. - // Therefore, we need to make sure that our cache is authoratative. - // Otherwise, we would have to fall back to linearly scanning the list of - // all civzones on a cache miss. - // - // Since we guarantee our cache contains *at least* all tiles that are - // currently covered by civzones, we can conclude that if a tile is not in - // the cache, there is no civzone there. Civzones *can* be dynamically - // shrunk, so we still need to verify that civzones that once covered this - // tile continue to cover this tile. - cacheNewCivzones(); - - auto civzones_it = locationToCivzones.find(pos); - if (civzones_it == locationToCivzones.end()) - return false; - - set ids_to_remove; - auto &civzones = civzones_it->second; - for (int32_t id : civzones) { - int32_t idx = df::building::binsearch_index( - world->buildings.other[buildings_other_id::ANY_ZONE], id); - df::building_civzonest *civzone = NULL; - if (idx > -1) - civzone = world->buildings.other.ANY_ZONE[idx]; - if (!civzone || civzone->z != pos.z || - !containsTile(civzone, pos, true)) { - ids_to_remove.insert(id); + for (df::building_civzonest* zone : world->buildings.other.ACTIVITY_ZONE) + { + if (pos.z != zone->z) continue; - } - pvec->push_back(civzone); - } - // civzone no longer occupies this tile; update the cache - if (!ids_to_remove.empty()) { - for (auto it = civzones.begin(); it != civzones.end(); ) { - if (ids_to_remove.count(*it)) { - it = civzones.erase(it); + if (zone->room.extents && zone->isExtentShaped()) + { + auto etile = getExtentTile(zone->room, pos); + if (!etile || !*etile) continue; - } - ++it; + + pvec->push_back(zone); } - if (civzones.empty()) - locationToCivzones.erase(pos); } return !pvec->empty(); @@ -1053,7 +1161,6 @@ static int getMaxStockpileId() return max_id; } -/* TODO: understand how this changes for v50 static int getMaxCivzoneId() { auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; @@ -1068,7 +1175,6 @@ static int getMaxCivzoneId() return max_id; } -*/ bool Buildings::constructAbstract(df::building *bld) { @@ -1087,12 +1193,14 @@ bool Buildings::constructAbstract(df::building *bld) stock->stockpile_number = getMaxStockpileId() + 1; break; -/* TODO: understand how this changes for v50 case building_type::Civzone: if (auto zone = strict_virtual_cast(bld)) + { zone->zone_num = getMaxCivzoneId() + 1; + + add_zone_to_all_buildings(zone); + } break; -*/ default: break; @@ -1186,6 +1294,8 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite bld->mat_index = items[i]->getMaterialIndex(); } + add_building_to_all_zones(bld); + createDesign(bld, rough); return true; } @@ -1232,6 +1342,8 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectoruncategorize(); + + remove_building_from_all_zones(bld); + remove_zone_from_all_buildings(bld); + delete bld; if (world->selected_building == bld) @@ -1310,12 +1426,20 @@ bool Buildings::markedForRemoval(df::building *bld) return false; } +void Buildings::notifyCivzoneModified(df::building* bld) +{ + if (bld->getType() != building_type::Civzone) + return; + + //remove zone here needs to be the slow method + remove_zone_from_all_buildings(bld); + add_zone_to_all_buildings(bld); +} + void Buildings::clearBuildings(color_ostream& out) { corner1.clear(); corner2.clear(); locationToBuilding.clear(); - locationToCivzones.clear(); - nextCivzone = 0; } void Buildings::updateBuildings(color_ostream&, void* ptr) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 0fb455fc9..c661cf254 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -46,6 +46,7 @@ using namespace DFHack; #include "modules/Screen.h" #include "modules/Maps.h" #include "modules/Units.h" +#include "modules/World.h" #include "DataDefs.h" @@ -625,10 +626,8 @@ bool Gui::anywhere_hotkey(df::viewscreen *) { return true; } -bool Gui::dwarfmode_hotkey(df::viewscreen *top) -{ - // Require the main dwarf mode screen - return !!strict_virtual_cast(top); +bool Gui::dwarfmode_hotkey(df::viewscreen *top) { + return World::isFortressMode(); } bool Gui::unitjobs_hotkey(df::viewscreen *top) @@ -650,7 +649,7 @@ bool Gui::item_details_hotkey(df::viewscreen *top) static bool has_cursor() { - return df::global::cursor && df::global::cursor->x != -30000; + return Gui::getCursorPos().isValid(); } bool Gui::cursor_hotkey(df::viewscreen *top) @@ -1721,7 +1720,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) // Check if the announcement will actually be announced if (*gamemode == game_mode::ADVENTURE) { - if (r.pos.x != -30000 && + if (r.pos.x >= 0 && r.type != announcement_type::CREATURE_SOUND && r.type != announcement_type::REGULAR_CONVERSATION && r.type != announcement_type::CONFLICT_CONVERSATION && @@ -2152,7 +2151,7 @@ bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) x = selection_rect->start_x; y = selection_rect->start_y; z = selection_rect->start_z; - return (x == -30000) ? false : true; + return (x >= 0) ? false : true; } bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 3ae8658c8..78ed53d97 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -21,6 +21,7 @@ static long g_num_dfhack_textures = 0; static long g_dfhack_logo_texpos_start = -1; static long g_green_pin_texpos_start = -1; static long g_red_pin_texpos_start = -1; +static long g_icons_texpos_start = -1; // Converts an arbitrary Surface to something like the display format // (32-bit RGBA), and converts magenta to transparency if convert_magenta is set @@ -117,6 +118,8 @@ void Textures::init(color_ostream &out) { &g_green_pin_texpos_start); g_num_dfhack_textures += load_textures(out, "hack/data/art/red-pin.png", &g_red_pin_texpos_start); + g_num_dfhack_textures += load_textures(out, "hack/data/art/icons.png", + &g_icons_texpos_start); DEBUG(textures,out).print("loaded %ld textures\n", g_num_dfhack_textures); @@ -160,3 +163,7 @@ long Textures::getGreenPinTexposStart() { long Textures::getRedPinTexposStart() { return g_red_pin_texpos_start; } + +long Textures::getIconsTexposStart() { + return g_icons_texpos_start; +} diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 06cb4e62d..a60803800 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1455,7 +1455,7 @@ int Units::computeMovementSpeed(df::unit *unit) if (isBaby(unit)) speed += 3000; - if (unit->flags3.bits.unk15) + if (unit->flags3.bits.diving) speed /= 20; if (unit->counters2.exhaustion >= 2000) @@ -1965,7 +1965,7 @@ int Units::getStressCategory(df::unit *unit) if (!unit->status.current_soul) return int(stress_cutoffs.size()) / 2; - return getStressCategoryRaw(unit->status.current_soul->personality.stress_level); + return getStressCategoryRaw(unit->status.current_soul->personality.stress); } int Units::getStressCategoryRaw(int32_t stress_level) diff --git a/library/xml b/library/xml index 18446d51e..5d43fd9fd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 18446d51e4133a06271fd2672f5b816b8c56e937 +Subproject commit 5d43fd9fd91007cf674bfde44b2c5a0ac70170db diff --git a/package/linux/dfhack-run b/package/linux/dfhack-run index 55001cfcb..b5e6ccfe2 100755 --- a/package/linux/dfhack-run +++ b/package/linux/dfhack-run @@ -1,6 +1,6 @@ #!/bin/sh -DF_DIR=$(dirname "$0") +DF_DIR=$(dirname "$(readlink -f "$0")") cd "${DF_DIR}" export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./hack/libs":"./hack" diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 06159c51c..3d36b95fc 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -78,7 +78,7 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) dfhack_plugin(autoclothing autoclothing.cpp) -#dfhack_plugin(autodump autodump.cpp) +dfhack_plugin(autodump autodump.cpp) dfhack_plugin(autofarm autofarm.cpp) #dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) #add_subdirectory(autolabor) @@ -86,20 +86,20 @@ dfhack_plugin(autofarm autofarm.cpp) #dfhack_plugin(automelt automelt.cpp) #dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) #dfhack_plugin(autotrade autotrade.cpp) -#dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) +dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -#add_subdirectory(buildingplan) +dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) #add_subdirectory(channel-safely) -#dfhack_plugin(cleanconst cleanconst.cpp) -#dfhack_plugin(cleaners cleaners.cpp) +dfhack_plugin(cleanconst cleanconst.cpp) +dfhack_plugin(cleaners cleaners.cpp) dfhack_plugin(cleanowned cleanowned.cpp) #dfhack_plugin(confirm confirm.cpp LINK_LIBRARIES lua) #dfhack_plugin(createitem createitem.cpp) -#dfhack_plugin(cursecheck cursecheck.cpp) +dfhack_plugin(cursecheck cursecheck.cpp) #dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) #dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) @@ -113,7 +113,7 @@ dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) #dfhack_plugin(embark-tools embark-tools.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) -#dfhack_plugin(filltraffic filltraffic.cpp) +dfhack_plugin(filltraffic filltraffic.cpp) #dfhack_plugin(fix-unit-occupancy fix-unit-occupancy.cpp) #dfhack_plugin(fixveins fixveins.cpp) #dfhack_plugin(flows flows.cpp) @@ -130,10 +130,10 @@ dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) #dfhack_plugin(manipulator manipulator.cpp) #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) -#dfhack_plugin(misery misery.cpp) +dfhack_plugin(misery misery.cpp) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) -#dfhack_plugin(nestboxes nestboxes.cpp) +dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) @@ -149,7 +149,7 @@ add_subdirectory(remotefortressreader) dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) #dfhack_plugin(search search.cpp) dfhack_plugin(seedwatch seedwatch.cpp) -#dfhack_plugin(showmood showmood.cpp) +dfhack_plugin(showmood showmood.cpp) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) #dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(steam-engine steam-engine.cpp) @@ -158,7 +158,7 @@ dfhack_plugin(seedwatch seedwatch.cpp) #add_subdirectory(stockpiles) #dfhack_plugin(stocks stocks.cpp) #dfhack_plugin(strangemood strangemood.cpp) -#dfhack_plugin(tailor tailor.cpp) +dfhack_plugin(tailor tailor.cpp) dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(title-folder title-folder.cpp) #dfhack_plugin(title-version title-version.cpp) diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 0c20c0cb4..34a8979ff 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -522,10 +522,8 @@ static void scan_logs(int32_t *usable_logs, const vector &citizens, if (item->getType() != item_type::WOOD) continue; - if (!is_valid_item(item)) { - INFO(status).print("autochop is_valid_item actually caught something useful!! Please tell the DFHack team.\n"); + if (!is_valid_item(item)) continue; - } if (!is_accessible_item(item->pos, citizens)) { if (inaccessible_logs) diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index fb215c26b..aed6fa0ea 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -1,33 +1,34 @@ // Quick Dumper : Moves items marked as "dump" to cursor // FIXME: local item cache in map blocks needs to be fixed after teleporting items -#include -#include -#include -#include -#include -#include -#include -#include -using namespace std; #include "Core.h" #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" + #include "modules/Maps.h" #include "modules/Gui.h" #include "modules/Items.h" #include "modules/Materials.h" #include "modules/MapCache.h" -#include "DataDefs.h" #include "df/item.h" #include "df/world.h" #include "df/general_ref.h" #include "df/viewscreen_dwarfmodest.h" #include "df/building_stockpilest.h" -#include "uicommon.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; using namespace DFHack; using namespace df::enums; @@ -40,6 +41,7 @@ DFHACK_PLUGIN("autodump"); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); +/* TODO: merge with stockpiles plugin // Stockpile interface START static const string PERSISTENCE_KEY = "autodump/stockpiles"; @@ -267,6 +269,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) } // Stockpile interface END +*/ command_result df_autodump(color_ostream &out, vector & parameters); command_result df_autodump_destroy_here(color_ostream &out, vector & parameters); @@ -276,18 +279,21 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector & parame MapCache MC; int dumped_total = 0; - int cx, cy, cz; - DFCoord pos_cursor; + df::coord pos_cursor; if(!destroy || here) { - if (!Gui::getCursorCoords(cx,cy,cz)) - { - out.printerr("Cursor position not found. Please enable the cursor.\n"); + pos_cursor = Gui::getCursorPos(); + if (!pos_cursor.isValid()) { + out.printerr("Keyboard cursor must be over a suitable map tile.\n"); return CR_FAILURE; } - pos_cursor = DFCoord(cx,cy,cz); } if (!destroy) { @@ -441,13 +445,14 @@ command_result df_autodump(color_ostream &out, vector & parameters) command_result df_autodump_destroy_here(color_ostream &out, vector & parameters) { - // HOTKEY COMMAND; CORE ALREADY SUSPENDED if (!parameters.empty()) return CR_WRONG_USAGE; vector args; args.push_back("destroy-here"); + CoreSuspender suspend; + return autodump_main(out, args); } diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 238627c3a..566c49355 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -332,6 +332,7 @@ public: void status(color_ostream& out) { out << "Autofarm is " << (enabled ? "Active." : "Stopped.") << '\n'; + for (auto& lc : lastCounts) { auto plant = world->raws.plants.all[lc.first]; @@ -340,7 +341,7 @@ public: for (auto& th : thresholds) { - if (lastCounts[th.first] > 0) + if (lastCounts.count(th.first) > 0) continue; auto plant = world->raws.plants.all[th.first]; out << plant->id << " limit " << getThreshold(th.first) << " current 0" << '\n'; diff --git a/plugins/autogems.cpp b/plugins/autogems.cpp index 8cf419aba..87eaafd16 100644 --- a/plugins/autogems.cpp +++ b/plugins/autogems.cpp @@ -299,7 +299,7 @@ IMPLEMENT_VMETHOD_INTERPOSE(autogem_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(autogem_hook, render); bool read_config(color_ostream &out) { - std::string path = "data/save/" + World::ReadWorldFolder() + "/autogems.json"; + std::string path = "save/" + World::ReadWorldFolder() + "/autogems.json"; if (!Filesystem::isfile(path)) { // no need to require the config file to exist return true; diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 509b1e6bc..3d34f592e 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -850,6 +850,7 @@ static const char * get_tile_build(const df::coord &pos, return add_expansion_syntax(ctx, keys); } +/* TODO: understand how this changes for v50 static const char * get_place_keys(const tile_context &ctx) { df::building_stockpilest* sp = virtual_cast(ctx.b); @@ -1086,6 +1087,7 @@ static const char * get_tile_rooms(const df::coord &, const tile_context &ctx) { str << "r{+ " << (max_dim - 3) << "}&"; return cache(str); } +*/ static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { @@ -1326,6 +1328,7 @@ static bool do_transform(color_ostream &out, get_tile_construct, ensure_building); add_processor(processors, opts, "build", "build", opts.build, get_tile_build, ensure_building); +/* TODO: understand how this changes for v50 add_processor(processors, opts, "place", "place", opts.place, get_tile_place, ensure_building); add_processor(processors, opts, "zone", "zone", opts.zone, get_tile_zone); @@ -1333,6 +1336,14 @@ static bool do_transform(color_ostream &out, get_tile_query, ensure_building); add_processor(processors, opts, "query", "rooms", opts.rooms, get_tile_rooms, ensure_building); +*/ if (opts.place) + out.printerr("'place' blueprints are not yet supported for the current version of DF\n"); + if (opts.zone) + out.printerr("'zone' blueprints are not yet supported for the current version of DF\n"); + if (opts.query) + out.printerr("'query' blueprints are not yet supported for the current version of DF\n"); + if (opts.rooms) + out.printerr("'rooms' blueprints are not yet supported for the current version of DF\n"); if (processors.empty()) { out.printerr("no phases requested! nothing to do!\n"); diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp new file mode 100644 index 000000000..1407e5c46 --- /dev/null +++ b/plugins/buildingplan.cpp @@ -0,0 +1,688 @@ +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/Materials.h" +#include "modules/Persistence.h" +#include "modules/World.h" + +#include "df/building.h" +#include "df/building_design.h" +#include "df/item.h" +#include "df/job_item.h" +#include "df/world.h" + +#include +#include +#include +#include + +using std::map; +using std::pair; +using std::queue; +using std::string; +using std::unordered_map; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("buildingplan"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +// logging levels can be dynamically controlled with the `debugfilter` command. +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); +} + +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; + +enum ConfigValues { + CONFIG_BLOCKS = 1, + CONFIG_BOULDERS = 2, + CONFIG_LOGS = 3, + CONFIG_BARS = 4, +}; + +enum BuildingConfigValues { + BLD_CONFIG_ID = 0, +}; + +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} + +class PlannedBuilding { +public: + const df::building::key_field_type id; + + PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) { + DEBUG(status,out).print("creating persistent data for building %d\n", id); + bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY); + set_config_val(bld_config, BLD_CONFIG_ID, id); + } + + PlannedBuilding(DFHack::PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } + + void remove(color_ostream &out); + + // Ensure the building still exists and is in a valid state. It can disappear + // for lots of reasons, such as running the game with the buildingplan plugin + // disabled, manually removing the building, modifying it via the API, etc. + df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) { + auto bld = df::building::find(id); + bool valid = bld && bld->getBuildStage() == 0; + if (!valid) { + remove(out); + return NULL; + } + return bld; + } + +private: + DFHack::PersistentDataItem bld_config; +}; + +static PersistentDataItem config; +// building id -> PlannedBuilding +unordered_map planned_buildings; +// vector id -> filter bucket -> queue of (building id, job_item index) +map>>> tasks; + +// note that this just removes the PlannedBuilding. the tasks will get dropped +// as we discover them in the tasks queues and they fail to be found in planned_buildings. +// this "lazy" task cleaning algorithm works because there is no way to +// re-register a building once it has been removed -- if it has been booted out of +// planned_buildings, then it has either been built or desroyed. therefore there is +// no chance of duplicate tasks getting added to the tasks queues. +void PlannedBuilding::remove(color_ostream &out) { + DEBUG(status,out).print("removing persistent data for building %d\n", id); + DFHack::World::DeletePersistentData(config); + if (planned_buildings.count(id) > 0) + planned_buildings.erase(id); +} + +static const int32_t CYCLE_TICKS = 600; // twice per game day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out); +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(status,out).print("initializing %s\n", plugin_name); + + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Plan building placement before you have materials.", + do_command)); + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(status,out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_BLOCKS, true); + set_config_bool(config, CONFIG_BOULDERS, true); + set_config_bool(config, CONFIG_LOGS, true); + set_config_bool(config, CONFIG_BARS, false); + } + + DEBUG(status,out).print("loading persisted state\n"); + planned_buildings.clear(); + tasks.clear(); + vector building_configs; + World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); + const size_t num_building_configs = building_configs.size(); + for (size_t idx = 0; idx < num_building_configs; ++idx) { + PlannedBuilding pb(building_configs[idx]); + registerPlannedBuilding(out, pb); + } + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); + planned_buildings.clear(); + tasks.clear(); + } + return CR_OK; +} + +static bool cycle_requested = false; + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!Core::getInstance().isWorldLoaded()) + return CR_OK; + + if (is_enabled && + (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) + do_cycle(out); + return CR_OK; +} + +static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot configure %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!call_buildingplan_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(in_job); + F(owned); F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +}; + +static bool itemPassesScreen(df::item * item) { + static const BadFlags bad_flags; + return !(item->flags.whole & bad_flags.whole) + && !item->isAssignedToStockpile(); +} + +static bool matchesFilters(df::item * item, df::job_item * job_item) { + // check the properties that are not checked by Job::isSuitableItem() + if (job_item->item_type > -1 && job_item->item_type != item->getType()) + return false; + + if (job_item->item_subtype > -1 && + job_item->item_subtype != item->getSubtype()) + return false; + + if (job_item->flags2.bits.building_material && !item->isBuildMat()) + return false; + + if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + return false; + + if (job_item->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(job_item->has_tool_use)) + return false; + + return DFHack::Job::isSuitableItem( + job_item, item->getType(), item->getSubtype()) + && DFHack::Job::isSuitableMaterial( + job_item, item->getMaterial(), item->getMaterialIndex(), + item->getType()); +} + +static bool isJobReady(color_ostream &out, df::job * job) { + int needed_items = 0; + for (auto job_item : job->job_items) { needed_items += job_item->quantity; } + if (needed_items) { + DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + return false; + } + return true; +} + +static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { + // we want the items in the opposite order of the filters + return a->job_item_idx > b->job_item_idx; +} + +// this function does not remove the job_items since their quantity fields are +// now all at 0, so there is no risk of having extra items attached. we don't +// remove them to keep the "finalize with buildingplan active" path as similar +// as possible to the "finalize with buildingplan disabled" path. +static void finalizeBuilding(color_ostream &out, df::building * bld) { + DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + auto job = bld->jobs[0]; + + // sort the items so they get added to the structure in the correct order + std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); + + // derive the material properties of the building and job from the first + // applicable item. if any boulders are involved, it makes the whole + // structure "rough". + bool rough = false; + for (auto attached_item : job->items) { + df::item *item = attached_item->item; + rough = rough || item->getType() == df::item_type::BOULDER; + if (bld->mat_type == -1) { + bld->mat_type = item->getMaterial(); + job->mat_type = bld->mat_type; + } + if (bld->mat_index == -1) { + bld->mat_index = item->getMaterialIndex(); + job->mat_index = bld->mat_index; + } + } + + if (bld->needsDesign()) { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } + + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} + +static df::building * popInvalidTasks(color_ostream &out, queue> & task_queue) { + while (!task_queue.empty()) { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { + auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); + if (bld && bld->jobs[0]->job_items[task.second]->quantity) + return bld; + } + DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + task_queue.pop(); + } + return NULL; +} + +static void doVector(color_ostream &out, df::job_item_vector_id vector_id, + map>> & buckets) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + auto item_vector = df::global::world->items.other[other_id]; + DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + item_vector.size(), + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + buckets.size()); + for (auto item_it = item_vector.rbegin(); + item_it != item_vector.rend(); + ++item_it) { + auto item = *item_it; + if (!itemPassesScreen(item)) + continue; + for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { + auto & task_queue = bucket_it->second; + auto bld = popInvalidTasks(out, task_queue); + if (!bld) { + DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + bucket_it = buckets.erase(bucket_it); + continue; + } + auto & task = task_queue.front(); + auto id = task.first; + auto job = bld->jobs[0]; + auto filter_idx = task.second; + if (matchesFilters(item, job->job_items[filter_idx]) + && DFHack::Job::attachJobItem(job, item, + df::job_item_ref::Hauled, filter_idx)) + { + MaterialInfo material; + material.decode(item); + ItemTypeInfo item_type; + item_type.decode(item); + DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", + material.toString().c_str(), + item_type.toString().c_str(), + filter_idx, + ENUM_KEY_STR(building_type, bld->getType()).c_str(), + id, + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str()); + // keep quantity aligned with the actual number of remaining + // items so if buildingplan is turned off, the building will + // be completed with the correct number of items. + --job->job_items[filter_idx]->quantity; + task_queue.pop(); + if (isJobReady(out, job)) { + finalizeBuilding(out, bld); + planned_buildings.at(id).remove(out); + } + if (task_queue.empty()) { + DEBUG(cycle,out).print( + "removing empty item bucket: %s/%s; %zu left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + buckets.erase(bucket_it); + } + // we found a home for this item; no need to look further + break; + } + ++bucket_it; + } + if (buckets.empty()) + break; + } +} + +struct VectorsToScanLast { + std::vector vectors; + VectorsToScanLast() { + // order is important here. we want to match boulders before wood and + // everything before bars. blocks are not listed here since we'll have + // already scanned them when we did the first pass through the buckets. + vectors.push_back(df::job_item_vector_id::BOULDER); + vectors.push_back(df::job_item_vector_id::WOOD); + vectors.push_back(df::job_item_vector_id::BAR); + } +}; + +static void do_cycle(color_ostream &out) { + static const VectorsToScanLast vectors_to_scan_last; + + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; + + DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n", + plugin_name, planned_buildings.size()); + + for (auto it = tasks.begin(); it != tasks.end(); ) { + auto vector_id = it->first; + // we could make this a set, but it's only three elements + if (std::find(vectors_to_scan_last.vectors.begin(), + vectors_to_scan_last.vectors.end(), + vector_id) != vectors_to_scan_last.vectors.end()) { + ++it; + continue; + } + + auto & buckets = it->second; + doVector(out, vector_id, buckets); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + it = tasks.erase(it); + } + else + ++it; + } + for (auto vector_id : vectors_to_scan_last.vectors) { + if (tasks.count(vector_id) == 0) + continue; + auto & buckets = tasks[vector_id]; + doVector(out, vector_id, buckets); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + tasks.erase(vector_id); + } + } + DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + planned_buildings.size()); +} + +///////////////////////////////////////////////////// +// Lua API +// core will already be suspended when coming in through here +// + +static string getBucket(const df::job_item & ji) { + std::ostringstream ser; + + // pull out and serialize only known relevant fields. if we miss a few, then + // the filter bucket will be slighly less specific than it could be, but + // that's probably ok. we'll just end up bucketing slightly different items + // together. this is only a problem if the different filter at the front of + // the queue doesn't match any available items and blocks filters behind it + // that could be matched. + ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' + << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole + << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' + << ji.metal_ore << ':' << ji.has_tool_use; + + return ser.str(); +} + +// get a list of item vectors that we should search for matches +static vector getVectorIds(color_ostream &out, df::job_item *job_item) +{ + std::vector ret; + + // if the filter already has the vector_id set to something specific, use it + if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) + { + DEBUG(status,out).print("using vector_id from job_item: %s\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + ret.push_back(job_item->vector_id); + return ret; + } + + // if the filer is for building material, refer to our global settings for + // which vectors to search + if (job_item->flags2.bits.building_material) + { + if (get_config_bool(config, CONFIG_BLOCKS)) + ret.push_back(df::job_item_vector_id::BLOCKS); + if (get_config_bool(config, CONFIG_BOULDERS)) + ret.push_back(df::job_item_vector_id::BOULDER); + if (get_config_bool(config, CONFIG_LOGS)) + ret.push_back(df::job_item_vector_id::WOOD); + if (get_config_bool(config, CONFIG_BARS)) + ret.push_back(df::job_item_vector_id::BAR); + } + + // fall back to IN_PLAY if no other vector was appropriate + if (ret.empty()) + ret.push_back(df::job_item_vector_id::IN_PLAY); + return ret; +} +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { + df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld) + return false; + + if (bld->jobs.size() != 1) { + DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + return false; + } + auto job_items = bld->jobs[0]->job_items; + int num_job_items = job_items.size(); + if (num_job_items < 1) { + DEBUG(status,out).print("unexpected number of job items: want >0, got %d\n", num_job_items); + return false; + } + int32_t id = bld->id; + for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { + auto job_item = job_items[job_item_idx]; + auto bucket = getBucket(*job_item); + auto vector_ids = getVectorIds(out, job_item); + + // if there are multiple vector_ids, schedule duplicate tasks. after + // the correct number of items are matched, the extras will get popped + // as invalid + for (auto vector_id : vector_ids) { + for (int item_num = 0; item_num < job_item->quantity; ++item_num) { + tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); + DEBUG(status,out).print("added task: %s/%s/%d,%d; " + "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket.c_str(), id, job_item_idx, tasks.size(), + tasks[vector_id].size(), tasks[vector_id][bucket].size()); + } + } + } + + // suspend jobs + for (auto job : bld->jobs) + job->flags.bits.suspend = true; + + // add the planned buildings to our register + planned_buildings.emplace(bld->id, pb); + + return true; +} + +static void printStatus(color_ostream &out) { + DEBUG(status,out).print("entering buildingplan_printStatus\n"); + out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print(" finding materials for %zd buildings\n", planned_buildings.size()); + out.print("Current settings:\n"); + out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); + out.print("\n"); +} + +static bool setSetting(color_ostream &out, string name, bool value) { + DEBUG(status,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + if (name == "blocks") + set_config_bool(config, CONFIG_BLOCKS, value); + else if (name == "boulders") + set_config_bool(config, CONFIG_BOULDERS, value); + else if (name == "logs") + set_config_bool(config, CONFIG_LOGS, value); + else if (name == "bars") + set_config_bool(config, CONFIG_BARS, value); + else { + out.printerr("unrecognized setting: '%s'\n", name.c_str()); + return false; + } + return true; +} + +static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { + DEBUG(status,out).print("entering isPlannableBuilding\n"); + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, type); + Lua::Push(L, subtype); + Lua::Push(L, custom); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return false; + } + return num_filters >= 1; +} + +static bool isPlannedBuilding(color_ostream &out, df::building *bld) { + TRACE(status,out).print("entering isPlannedBuilding\n"); + return bld && planned_buildings.count(bld->id) > 0; +} + +static bool addPlannedBuilding(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering addPlannedBuilding\n"); + if (!bld || planned_buildings.count(bld->id) + || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), + bld->getCustomType())) + return false; + PlannedBuilding pb(out, bld); + return registerPlannedBuilding(out, pb); +} + +static void doCycle(color_ostream &out) { + DEBUG(status,out).print("entering doCycle\n"); + do_cycle(out); +} + +static void scheduleCycle(color_ostream &out) { + DEBUG(status,out).print("entering scheduleCycle\n"); + cycle_requested = true; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(printStatus), + DFHACK_LUA_FUNCTION(setSetting), + DFHACK_LUA_FUNCTION(isPlannableBuilding), + DFHACK_LUA_FUNCTION(isPlannedBuilding), + DFHACK_LUA_FUNCTION(addPlannedBuilding), + DFHACK_LUA_FUNCTION(doCycle), + DFHACK_LUA_FUNCTION(scheduleCycle), + DFHACK_LUA_END +}; diff --git a/plugins/lua/autochop.lua b/plugins/lua/autochop.lua index e6f8e7008..6b8c9f943 100644 --- a/plugins/lua/autochop.lua +++ b/plugins/lua/autochop.lua @@ -63,7 +63,7 @@ function parse_commandline(...) elseif command == 'undesignate' then autochop_undesignate() elseif command == 'target' then - setTarget(args[2], args[3]) + setTargets(args[2], args[3]) elseif command == 'chop' then do_set_burrow_config('chop', true, args[2]) elseif command == 'nochop' then diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index a01a1d0b1..9b953dd7c 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -14,11 +14,31 @@ local _ENV = mkmodule('plugins.buildingplan') --]] -local dialogs = require('gui.dialogs') -local guidm = require('gui.dwarfmode') +local argparse = require('argparse') require('dfhack.buildings') --- does not need the core suspended +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + return true +end + function get_num_filters(btype, subtype, custom) local filters = dfhack.buildings.getFiltersByType( {}, btype, subtype, custom) @@ -26,6 +46,9 @@ function get_num_filters(btype, subtype, custom) return 0 end +local dialogs = require('gui.dialogs') +local guidm = require('gui.dwarfmode') + local function to_title_case(str) str = str:gsub('(%a)([%w_]*)', function (first, rest) return first:upper()..rest:lower() end) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 53885d8fa..2ec38fff6 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -32,12 +32,7 @@ function HotspotMenuWidget:overlay_onupdate() end function HotspotMenuWidget:overlay_trigger() - local hotkeys, bindings = getHotkeys() - return MenuScreen{ - hotspot_frame=self.frame, - hotkeys=hotkeys, - bindings=bindings, - mouseover=self.mouseover}:show() + return MenuScreen{hotspot_frame=self.frame}:show() end local dscreen = dfhack.screen @@ -71,21 +66,17 @@ end -- register the menu hotspot with the overlay OVERLAY_WIDGETS = {menu=HotspotMenuWidget} --- ---------- -- --- MenuScreen -- --- ---------- -- +-- ---- -- +-- Menu -- +-- ---- -- local ARROW = string.char(26) local MAX_LIST_WIDTH = 45 local MAX_LIST_HEIGHT = 15 -MenuScreen = defclass(MenuScreen, gui.Screen) -MenuScreen.ATTRS{ - focus_path='hotkeys/menu', +Menu = defclass(MenuScreen, widgets.Panel) +Menu.ATTRS{ hotspot_frame=DEFAULT_NIL, - hotkeys=DEFAULT_NIL, - bindings=DEFAULT_NIL, - mouseover=false, } -- get a map from the binding string to a list of hotkey strings that all @@ -142,23 +133,30 @@ local function get_choices(hotkeys, bindings, is_inverted) return choices, max_width end -function MenuScreen:init() +function Menu:init() + local hotkeys, bindings = getHotkeys() + local is_inverted = not not self.hotspot_frame.b - local choices,list_width = get_choices(self.hotkeys, self.bindings, - is_inverted) + local choices,list_width = get_choices(hotkeys, bindings, is_inverted) local list_frame = copyall(self.hotspot_frame) + local list_widget_frame = {h=math.min(#choices, MAX_LIST_HEIGHT)} + local quickstart_frame = {} list_frame.w = list_width + 2 - list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2 + list_frame.h = list_widget_frame.h + 4 if list_frame.t then list_frame.t = math.max(0, list_frame.t - 1) + list_widget_frame.t = 0 + quickstart_frame.b = 0 else list_frame.b = math.max(0, list_frame.b - 1) + list_widget_frame.b = 0 + quickstart_frame.t = 0 end if list_frame.l then - list_frame.l = math.max(0, list_frame.l - 1) + list_frame.l = math.max(0, list_frame.l + 5) else - list_frame.r = math.max(0, list_frame.r - 1) + list_frame.r = math.max(0, list_frame.r + 5) end local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r} @@ -169,21 +167,30 @@ function MenuScreen:init() end self:addviews{ - widgets.ResizingPanel{ + widgets.Panel{ view_id='list_panel', - autoarrange_subviews=true, frame=list_frame, frame_style=gui.GREY_LINE_FRAME, frame_background=gui.CLEAR_PEN, subviews={ widgets.List{ view_id='list', + frame=list_widget_frame, choices=choices, icon_width=2, on_select=self:callback('onSelect'), on_submit=self:callback('onSubmit'), on_submit2=self:callback('onSubmit2'), }, + widgets.Panel{frame={h=1}}, + widgets.HotkeyLabel{ + frame=quickstart_frame, + label='Quickstart guide', + key='STRING_A063', + on_activate=function() + self:onSubmit(nil, {command='quickstart-guide'}) + end, + }, }, }, widgets.ResizingPanel{ @@ -207,11 +214,7 @@ function MenuScreen:init() end end -function MenuScreen:onDismiss() - cleanupHotkeys() -end - -function MenuScreen:onSelect(_, choice) +function Menu:onSelect(_, choice) if not choice or #self.subviews == 0 then return end local first_word = choice.command:trim():split(' +')[1] if first_word:startswith(':') then first_word = first_word:sub(2) end @@ -220,22 +223,21 @@ function MenuScreen:onSelect(_, choice) self.subviews.help_panel:updateLayout() end -function MenuScreen:onSubmit(_, choice) +function Menu:onSubmit(_, choice) if not choice then return end - dfhack.screen.hideGuard(self, dfhack.run_command, choice.command) - self:dismiss() + dfhack.screen.hideGuard(self.parent_view, dfhack.run_command, choice.command) + self.parent_view:dismiss() end -function MenuScreen:onSubmit2(_, choice) +function Menu:onSubmit2(_, choice) if not choice then return end - self:dismiss() + self.parent_view:dismiss() dfhack.run_script('gui/launcher', choice.command) end -function MenuScreen:onInput(keys) - if keys.LEAVESCREEN then - self:dismiss() - return true +function Menu:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + return false elseif keys.STANDARDSCROLL_RIGHT then self:onSubmit2(self.subviews.list:getSelected()) return true @@ -246,19 +248,29 @@ function MenuScreen:onInput(keys) self:onSubmit2(list:getSelected()) return true end + if not self:getMouseFramePos() then + self.parent_view:dismiss() + return true + end end - return self:inputToSubviews(keys) + self:inputToSubviews(keys) + return true -- we're modal end -function MenuScreen:onRenderFrame(dc, rect) +function Menu:onRenderFrame(dc, rect) if self.initialize then self.initialize() self.initialize = nil end - self:renderParent() + Menu.super.onRenderFrame(dc, rect) +end + +function Menu:getMouseFramePos() + return self.subviews.list_panel:getMouseFramePos() or + self.subviews.help_panel:getMouseFramePos() end -function MenuScreen:onRenderBody(dc) +function Menu:onRenderBody(dc) local panel = self.subviews.list_panel local list = self.subviews.list local idx = list:getIdxUnderMouse() @@ -267,14 +279,35 @@ function MenuScreen:onRenderBody(dc) -- selection, don't override the selection until the mouse moves to -- another item list:setSelected(idx) - self.mouseover = true self.last_mouse_idx = idx - elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect}) - and self.mouseover then + end + if self:getMouseFramePos() then + self.mouseover = true + elseif self.mouseover then -- once the mouse has entered the list area, leaving the frame should -- close the menu screen - self:dismiss() + self.parent_view:dismiss() end end +-- ---------- -- +-- MenuScreen -- +-- ---------- -- + +MenuScreen = defclass(MenuScreen, gui.ZScreen) +MenuScreen.ATTRS { + focus_path='hotkeys/menu', + hotspot_frame=DEFAULT_NIL, +} + +function MenuScreen:init() + self:addviews{ + Menu{hotspot_frame=self.hotspot_frame}, + } +end + +function MenuScreen:onDismiss() + cleanupHotkeys() +end + return _ENV diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 4fc375d72..cf599fd08 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -43,7 +43,12 @@ end local function triggered_screen_has_lock() if not trigger_lock_holder_screen then return false end - if trigger_lock_holder_screen:isActive() then return true end + if trigger_lock_holder_screen:isActive() then + if trigger_lock_holder_screen.raise then + trigger_lock_holder_screen:raise() + end + return true + end return register_trigger_lock_screen(nil, nil) end @@ -343,7 +348,7 @@ end local function do_trigger(args, quiet) if triggered_screen_has_lock() then dfhack.printerr(('cannot trigger widget; widget "%s" is already active') - :format(active_triggered_widget)) + :format(trigger_lock_holder_description)) return end do_by_names_or_numbers(args[1], function(name, db_entry) @@ -429,9 +434,8 @@ local function _update_viewscreen_widgets(vs_name, vs, now_ms) return now_ms end --- not subject to trigger lock since these widgets are already filtered by --- viewscreen function update_viewscreen_widgets(vs_name, vs) + if triggered_screen_has_lock() then return end local now_ms = _update_viewscreen_widgets(vs_name, vs, nil) _update_viewscreen_widgets('all', vs, now_ms) end diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 6118b3997..e3c57d0f1 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -41,7 +41,7 @@ DFHACK_PLUGIN("orders"); REQUIRE_GLOBAL(world); static const std::string ORDERS_DIR = "dfhack-config/orders"; -static const std::string ORDERS_LIBRARY_DIR = "dfhack-config/orders/library"; +static const std::string ORDERS_LIBRARY_DIR = "hack/data/orders"; static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -86,6 +86,11 @@ static command_result orders_command(color_ostream & out, std::vector files; - if (0 < Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false)) - { - out << COLOR_LIGHTRED << "Unable to list files in directory: " << ORDERS_DIR << std::endl; - return CR_FAILURE; - } + Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false); - if (files.empty()) - { - out << COLOR_YELLOW << "No exported orders yet. Create manager orders and export them with 'orders export ', or copy pre-made orders .json files into " << ORDERS_DIR << "." << std::endl << std::endl; - } - - for (auto it : files) - { + for (auto it : files) { if (it.second) continue; // skip directories std::string name = it.first; diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index e1f29e358..7cc715147 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -619,13 +619,13 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re continue; if (zone->type != df::civzone_type::ArcheryRange) continue; - if(zone->dir_x < 0) + if(zone->zone_settings.archery.dir_x < 0) remote_build->set_direction(EAST); - else if(zone->dir_x > 0) + else if(zone->zone_settings.archery.dir_x > 0) remote_build->set_direction(WEST); - else if (zone->dir_y < 0) + else if (zone->zone_settings.archery.dir_y < 0) remote_build->set_direction(SOUTH); - else if (zone->dir_y > 0) + else if (zone->zone_settings.archery.dir_y > 0) remote_build->set_direction(NORTH); break; } diff --git a/plugins/rendermax/renderer_light.cpp b/plugins/rendermax/renderer_light.cpp index 428de3a25..d806d0c3e 100644 --- a/plugins/rendermax/renderer_light.cpp +++ b/plugins/rendermax/renderer_light.cpp @@ -1171,11 +1171,11 @@ void lightingEngineViewscreen::loadSettings() std::string rawFolder; if(df::global::world->cur_savegame.save_dir!="") { - rawFolder= "data/save/" + (df::global::world->cur_savegame.save_dir) + "/raw/"; + rawFolder= "save/" + (df::global::world->cur_savegame.save_dir) + "/"; } else { - rawFolder= "raw/"; + rawFolder= "dfhack-config/"; } const std::string settingsfile=rawFolder+"rendermax.lua"; diff --git a/plugins/stonesense b/plugins/stonesense index 6376bbc63..a045369db 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 6376bbc630feaf8b4f68623cdd2c28c87ac11af4 +Subproject commit a045369db6728979e709625908df3f4f36e868ca diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 0a8a82f5b..2b1d0c63c 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -6,6 +6,7 @@ #include "Core.h" #include "DataDefs.h" +#include "Debug.h" #include "PluginManager.h" #include "df/creature_raw.h" @@ -29,18 +30,23 @@ #include "modules/World.h" using namespace DFHack; -using namespace std; using df::global::world; using df::global::plotinfo; DFHACK_PLUGIN("tailor"); + #define AUTOENABLE false DFHACK_PLUGIN_IS_ENABLED(enabled); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(plotinfo); +namespace DFHack { + DBG_DECLARE(tailor, cycle, DebugCategory::LINFO); + DBG_DECLARE(tailor, config, DebugCategory::LINFO); +} + class Tailor { // ARMOR, SHOES, HELM, GLOVES, PANTS @@ -48,7 +54,7 @@ class Tailor { private: - const map jobTypeMap = { + const std::map jobTypeMap = { { df::job_type::MakeArmor, df::item_type::ARMOR }, { df::job_type::MakePants, df::item_type::PANTS }, { df::job_type::MakeHelm, df::item_type::HELM }, @@ -56,7 +62,7 @@ private: { df::job_type::MakeShoes, df::item_type::SHOES } }; - const map itemTypeMap = { + const std::map itemTypeMap = { { df::item_type::ARMOR, df::job_type::MakeArmor }, { df::item_type::PANTS, df::job_type::MakePants }, { df::item_type::HELM, df::job_type::MakeHelm }, @@ -107,13 +113,13 @@ private: std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; - map, int> available; // key is item type & size - map, int> needed; // same - map, int> queued; // same + std::map, int> available; // key is item type & size + std::map, int> needed; // same + std::map, int> queued; // same - map sizes; // this maps body size to races + std::map sizes; // this maps body size to races - map, int> orders; // key is item type, item subtype, size + std::map, int> orders; // key is item type, item subtype, size std::map supply; @@ -147,7 +153,7 @@ private: df::item_type t = i->getType(); int size = world->raws.creatures.all[i->getMakerRace()]->adultsize; - available[make_pair(t, size)] += 1; + available[std::make_pair(t, size)] += 1; } } @@ -180,7 +186,7 @@ private: supply[M_LEATHER] += i->getStackSize(); } - out->print("tailor: available silk %d yarn %d cloth %d leather %d\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER]); + DEBUG(cycle).print("tailor: available silk %d yarn %d cloth %d leather %d\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER]); } void scan_replacements() @@ -193,10 +199,10 @@ private: Units::isBaby(u)) continue; // skip units we don't control - set wearing; + std::set wearing; wearing.clear(); - deque worn; + std::deque worn; worn.clear(); for (auto inv : u->inventory) @@ -212,10 +218,16 @@ private: int size = world->raws.creatures.all[u->race]->adultsize; sizes[size] = u->race; - for (auto ty : set{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES }) + for (auto ty : std::set{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES }) { if (wearing.count(ty) == 0) - needed[make_pair(ty, size)] += 1; + { + TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", + ENUM_KEY_STR(item_type, ty).c_str(), + size, + Translation::TranslateName(&u->name, false).c_str()); + needed[std::make_pair(ty, size)] += 1; + } } for (auto w : worn) @@ -227,13 +239,13 @@ private: std::string description; w->getItemDescription(&description, 0); - if (available[make_pair(ty, size)] > 0) + if (available[std::make_pair(ty, size)] > 0) { if (w->flags.bits.owned) { bool confiscated = Items::setOwner(w, NULL); - out->print( + INFO(cycle).print( "tailor: %s %s from %s.\n", (confiscated ? "confiscated" : "could not confiscate"), description.c_str(), @@ -242,18 +254,22 @@ private: } if (wearing.count(ty) == 0) - available[make_pair(ty, size)] -= 1; + { + DEBUG(cycle).print("tailor: allocating a %s to %s\n", + ENUM_KEY_STR(item_type, ty).c_str(), + Translation::TranslateName(&u->name, false).c_str()); + available[std::make_pair(ty, size)] -= 1; + } if (w->getWear() > 1) w->flags.bits.dump = true; } else { - // out->print("%s worn by %s needs replacement\n", - // description.c_str(), - // Translation::TranslateName(&u->name, false).c_str() - // ); - orders[make_tuple(o, w->getSubtype(), size)] += 1; + DEBUG(cycle).print ("%s worn by %s needs replacement, but none available\n", + description.c_str(), + Translation::TranslateName(&u->name, false).c_str()); + orders[std::make_tuple(o, w->getSubtype(), size)] += 1; } } } @@ -270,7 +286,7 @@ private: int count = a.second; int sub = 0; - vector v; + std::vector v; switch (ty) { case df::item_type::ARMOR: v = entity->resources.armor_type; break; @@ -299,7 +315,8 @@ private: } const df::job_type j = itemTypeMap.at(ty); - orders[make_tuple(j, sub, size)] += count; + orders[std::make_tuple(j, sub, size)] += count; + DEBUG(cycle).print("tailor: %s times %d of size %d ordered\n", ENUM_KEY_STR(job_type, j).c_str(), count, size); } } @@ -318,7 +335,11 @@ private: int size = world->raws.creatures.all[race]->adultsize; - orders[make_tuple(o->job_type, sub, size)] -= o->amount_left; + orders[std::make_tuple(o->job_type, sub, size)] -= o->amount_left; + TRACE(cycle).print("tailor: existing order for %d %s of size %d detected\n", + o->amount_left, + ENUM_KEY_STR(job_type, o->job_type).c_str(), + size); } } @@ -333,14 +354,14 @@ private: int sub; int size; - tie(ty, sub, size) = o.first; + std::tie(ty, sub, size) = o.first; int count = o.second; if (count > 0) { - vector v; + std::vector v; BitArray* fl; - string name_s, name_p; + std::string name_s, name_p; switch (ty) { @@ -382,7 +403,7 @@ private: if (!can_make) { - out->print("tailor: civilization cannot make %s, skipped\n", name_p.c_str()); + INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", name_p.c_str()); continue; } @@ -416,7 +437,7 @@ private: world->manager_orders.push_back(order); - out->print("tailor: added order #%d for %d %s %s, sized for %s\n", + INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", order->id, c, bitfield_to_string(order->material_category).c_str(), @@ -464,9 +485,9 @@ public: } public: - command_result set_materials(color_ostream& out, vector& parameters) + command_result set_materials(color_ostream& out, std::vector& parameters) { - list newmat; + std::list newmat; newmat.clear(); for (auto m = parameters.begin() + 1; m != parameters.end(); m++) @@ -475,7 +496,7 @@ public: auto mm = std::find_if(all_materials.begin(), all_materials.end(), nameMatch); if (mm == all_materials.end()) { - out.print("tailor: material %s not recognized\n", m->c_str()); + WARN(config,out).print("tailor: material %s not recognized\n", m->c_str()); return CR_WRONG_USAGE; } else { @@ -484,7 +505,7 @@ public: } material_order = newmat; - out.print("tailor: material list set to %s\n", get_material_list().c_str()); + INFO(config,out).print("tailor: material list set to %s\n", get_material_list().c_str()); return CR_OK; } @@ -549,7 +570,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out) return CR_OK; } -static command_result tailor_cmd(color_ostream& out, vector & parameters) { +static command_result tailor_cmd(color_ostream& out, std::vector & parameters) { bool desired = enabled; if (parameters.size() == 1 && (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")) { diff --git a/plugins/tweak/tweaks/title-start-rename.h b/plugins/tweak/tweaks/title-start-rename.h index baa1552ab..289979be4 100644 --- a/plugins/tweak/tweaks/title-start-rename.h +++ b/plugins/tweak/tweaks/title-start-rename.h @@ -17,7 +17,7 @@ struct title_start_rename_hook : df::viewscreen_titlest { inline std::string full_save_dir(const std::string ®ion_name) { - return std::string("data/save/") + region_name; + return std::string("save/") + region_name; } bool do_rename() diff --git a/plugins/uicommon.h b/plugins/uicommon.h index 03914b461..6179324d4 100644 --- a/plugins/uicommon.h +++ b/plugins/uicommon.h @@ -65,24 +65,6 @@ struct coord32_t } }; -template -static void for_each_(vector &v, Fn func) -{ - for_each(v.begin(), v.end(), func); -} - -template -static void for_each_(map &v, Fn func) -{ - for_each(v.begin(), v.end(), func); -} - -template -static void transform_(const vector &src, vector &dst, Fn func) -{ - transform(src.begin(), src.end(), back_inserter(dst), func); -} - typedef int8_t UIColor; static inline void OutputString(UIColor color, int &x, int &y, const std::string &text, diff --git a/scripts b/scripts index a8c5ac9e9..5bdfd379b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a8c5ac9e94e26f0901917efac213ebedc7e0c276 +Subproject commit 5bdfd379b2d13f56bab455f94af56ec0ce3e0cb1