diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index abee9a86a..f03d8b3b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: plugins: all steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies @@ -161,7 +161,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies @@ -184,7 +184,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Set up Ruby 2.7 diff --git a/.github/workflows/buildmaster-rebuild.yml b/.github/workflows/buildmaster-rebuild.yml index 5a1fe9504..d4c7a70e6 100644 --- a/.github/workflows/buildmaster-rebuild.yml +++ b/.github/workflows/buildmaster-rebuild.yml @@ -14,7 +14,7 @@ jobs: name: Trigger Buildmaster steps: - name: Set up Python 3 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 - name: Install dependencies diff --git a/.github/workflows/steam.yml b/.github/workflows/steam.yml index aae1621af..33aa16d34 100644 --- a/.github/workflows/steam.yml +++ b/.github/workflows/steam.yml @@ -14,7 +14,7 @@ on: release_channel: description: Release channel type: string - required: true + required: false default: beta jobs: @@ -36,10 +36,22 @@ jobs: restore-keys: | ccache-win64-cross-msvc-develop-${{ github.event.inputs.commit_hash }} ccache-win64-cross-msvc + - name: Restore steam SDK + uses: actions/cache@v3 + with: + path: depends/steam/steamworks_sdk_156.zip + key: steam-sdk-156 + enableCrossOsArchive: true - name: Cross-compile win64 artifacts env: - CMAKE_EXTRA_ARGS: '-DBUILD_STONESENSE:BOOL=1' + CMAKE_EXTRA_ARGS: '-DBUILD_STONESENSE:BOOL=1 -DBUILD_DFLAUNCH:BOOL=1' + steam_username: ${{ secrets.STEAM_SDK_USERNAME }} + steam_password: ${{ secrets.STEAM_SDK_PASSWORD }} run: | + echo "commit: ${{ github.event.inputs.commit_hash }}" + echo "version: ${{ github.event.inputs.version }}" + echo "release_channel: ${{ github.event.inputs.release_channel }}" + echo cd build bash -x build-win64-from-linux.sh - name: Steam deploy diff --git a/.gitignore b/.gitignore index e24f11252..a386b260a 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ tags # external plugins /plugins/CMakeLists.custom.txt + +# steam api +depends/steam diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4068bc44..c305de56c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.21.0 + rev: 0.22.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: forbid-tabs exclude_types: @@ -41,4 +41,4 @@ repos: entry: python3 ci/authors-rst.py files: docs/about/Authors\.rst pass_filenames: false -exclude: '^(depends/|data/.*\.json$|.*\.diff$)' +exclude: '^(depends/|data/.*\.json$|.*\.diff$|.*\.dfstock$)' diff --git a/CMakeLists.txt b/CMakeLists.txt index 790e8f59b..72314ee6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,7 +192,7 @@ endif() # set up versioning. set(DF_VERSION "50.07") -set(DFHACK_RELEASE "beta1") +set(DFHACK_RELEASE "r2rc3") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") @@ -312,6 +312,7 @@ if(WIN32) ${SDLREAL_DOWNLOAD_DIR}/SDLreal.dll "5a09604daca6b2b5ce049d79af935d6a") endif() + endif() if(APPLE) diff --git a/build/build-win64-from-linux.sh b/build/build-win64-from-linux.sh index 11f4dbbc5..c0559a999 100755 --- a/build/build-win64-from-linux.sh +++ b/build/build-win64-from-linux.sh @@ -41,6 +41,8 @@ fi if ! docker run --rm -i -v "$srcdir":/src -v "$srcdir/build/win64-cross/":/src/build \ -e BUILDER_UID=$builder_uid \ -e CCACHE_DIR=/src/build/ccache \ + -e steam_username \ + -e steam_password \ --name dfhack-win \ ghcr.io/dfhack/build-env:msvc \ bash -c "cd /src/build && dfhack-configure windows 64 Release -DCMAKE_INSTALL_PREFIX=/src/build/output cmake .. -DBUILD_DOCS=1 $CMAKE_EXTRA_ARGS && dfhack-make -j$jobs install" \ diff --git a/build/win64/generate-MSVC-steam.bat b/build/win64/generate-MSVC-steam.bat new file mode 100644 index 000000000..007bb1c08 --- /dev/null +++ b/build/win64/generate-MSVC-steam.bat @@ -0,0 +1,4 @@ +IF EXIST DF_PATH.txt SET /P _DF_PATH=z); /* read first character */ - if (c == LUA_SIGNATURE[0]) { - checkmode(L, p->mode, "binary"); - cl = luaU_undump(L, p->z, p->name); - } - else { + // if (c == LUA_SIGNATURE[0]) { + // checkmode(L, p->mode, "binary"); + // cl = luaU_undump(L, p->z, p->name); + // } + // else { checkmode(L, p->mode, "text"); cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c); - } + // } lua_assert(cl->nupvalues == cl->p->sizeupvalues); luaF_initupvals(L, cl); } diff --git a/depends/xlsxio b/depends/xlsxio index 439fdbc25..0a9945266 160000 --- a/depends/xlsxio +++ b/depends/xlsxio @@ -1 +1 @@ -Subproject commit 439fdbc259c13f23a3122e68ba35ad5a13bcd97c +Subproject commit 0a994526622c2201756e386ef98b44b193e25f06 diff --git a/docs/Core.rst b/docs/Core.rst index 86e357b27..5decd668d 100644 --- a/docs/Core.rst +++ b/docs/Core.rst @@ -235,10 +235,57 @@ root DF folder): #. :file:`dfhack-config/scripts` #. :file:`save/{world}/scripts` (only if a save is loaded) #. :file:`hack/scripts` +#. :file:`data/installed_mods/...` (see below) For example, if ``teleport`` is run, these folders are searched in order for ``teleport.lua``, and the first matching file is run. +Scripts in installed mods +......................... + +Scripts in mods are automatically added to the script path. The following +directories are searched for mods:: + + ../../workshop/content/975370/ (the DF Steam workshop directory) + mods/ + data/installed_mods/ + +Each mod can have two directories that contain scripts: + +- ``scripts_modactive/`` is added to the script path if and only if the mod is + active in the loaded world. +- ``scripts_modinstalled/`` is added to the script path as long as the mod is + installed in one of the searched mod directories. + +Multiple versions of a mod may be installed at the same time. If a mod is +active in a loaded world, then the scripts for the version of the mod that is +active will be added to the script path. Otherwise, the latest version of each +mod is added to the script path. + +Scripts for active mods take precedence according to their load order when you +generated the current world. + +Scripts for non-active mods are ordered by their containing mod's ID. + +For example, the search paths for mods might look like this:: + + activemod_last_in_load_order/scripts_modactive + activemod_last_in_load_order/scripts_modinstalled + activemod_second_to_last_in_load_order/scripts_modactive + activemod_second_to_last_in_load_order/scripts_modinstalled + ... + inactivemod1/scripts_modinstalled + inactivemod2/scripts_modinstalled + ... + +Not all mods will have script directories, of course, and those mods will not be +added to the script search path. Mods are re-scanned whenever a world is loaded +or unloaded. For more information on scripts and mods, check out the +`modding-guide`. + +Custom script paths +................... + 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 bc58a39b7..15a6276bf 100644 --- a/docs/Installing.rst +++ b/docs/Installing.rst @@ -7,78 +7,50 @@ Installing DFHack .. contents:: :local: - Requirements ============ -DFHack supports Windows, Linux, and macOS, and both 64-bit and 32-bit builds -of Dwarf Fortress. +DFHack supports all operating systems and platforms that Dwarf Fortress itself +supports, which at the moment is just 64-bit Windows. However, the Windows +build of DFHack works well under ``wine`` (or ``Proton``, Steam's fork of +``wine``) on other operating systems. .. _installing-df-version: DFHack releases generally only support the version of Dwarf Fortress that they -are named after. For example, DFHack 0.40.24-r5 only supported DF 0.40.24. -DFHack releases *never* support newer versions of DF, because DFHack requires -data about DF that is only possible to obtain after DF has been released. -Occasionally, DFHack releases will be able to maintain support for older -versions of DF - for example, DFHack 0.34.11-r5 supported both DF 0.34.11 and -0.34.10. For maximum stability, you should usually use the latest versions of -both DF and DFHack. - -Windows -------- - -* DFHack only supports the SDL version of Dwarf Fortress. The "legacy" version - will *not* work with DFHack (the "small" SDL version is acceptable, however). -* Windows XP and older are *not* supported, due in part to a - `Visual C++ 2015 bug `_ - -The Windows build of DFHack should work under Wine on other operating systems, -although this is not tested very often. It is recommended to use the native -build for your operating system instead. - -.. _installing-reqs-linux: - -Linux ------ - -Generally, DFHack should work on any modern Linux distribution. There are -multiple release binaries provided - as of DFHack 0.47.04-r1, there are built -with GCC 7 and GCC 4.8 (as indicated by the ``gcc`` component of their -filenames). Using the newest build that works on your system is recommended. -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 `. - -macOS ------ - -OS X 10.6.8 or later is required. - +are named after. For example, DFHack 50.05 only supported DF 50.05. DFHack +releases *never* support newer versions of DF -- DFHack requires data about DF +that is only possible to obtain after DF has been released. Occasionally, +DFHack releases will be able to maintain support for older versions of DF - for +example, DFHack 0.34.11-r5 supported both DF 0.34.11 and 0.34.10. For maximum +stability, you should usually use the latest versions of both DF and DFHack. .. _downloading: Downloading DFHack ================== -Stable builds of DFHack are available on `GitHub `_. -GitHub has been known to change their layout periodically, but as of July 2020, -downloads are available at the bottom of the release notes for each release, under a section -named "Assets" (which you may have to expand). The name of the file indicates -which DF version, platform, and architecture the build supports - the platform -and architecture (64-bit or 32-bit) **must** match your build of DF. The DF -version should also match your DF version - see `above ` -for details. For example: +Stable builds of DFHack are available on +`Steam `__ +or from our `GitHub `__. Either +location will give you exactly the same package. + +On Steam, note that DFHack is a separate app, not a DF Steam Workshop mod. You +can run DF with DFHack by launching either the DFHack app or the original Dwarf +Fortress app. + +If you download from GitHub, downloads are available at the bottom of the +release notes for each release, under a section named "Assets" (which you may +have to expand). The name of the file indicates which DF version, platform, and +architecture the build supports - the platform and architecture (64-bit or +32-bit) **must** match your build of DF. The DF version should also match your +DF version - see `above ` for details. For example: -* ``dfhack-0.47.04-r1-Windows-64bit.zip`` supports 64-bit DF on Windows -* ``dfhack-0.47.04-r1-Linux-32bit-gcc-7.tar.bz2`` supports 32-bit DF on Linux - (see `installing-reqs-linux` for details on the GCC version indicator) +* ``dfhack-50.07-r1-Windows-64bit.zip`` supports 64-bit DF on Windows -The `DFHack website `_ also provides links to -unstable builds. These files have a different naming scheme, but the same -restrictions apply (e.g. a file named ``Windows64`` is for 64-bit Windows DF). +In between stable releases, we may create beta releases to test new features. +These are available via the beta release channel on Steam or from our regular +Github page as a pre-release tagged with a "beta" suffix. .. warning:: @@ -91,19 +63,22 @@ restrictions apply (e.g. a file named ``Windows64`` is for 64-bit Windows DF). Installing DFHack ================= +If you are installing from Steam, this is handled for you automatically. The +instructions here are for manual installs. + When you `download DFHack `, you will end up with a release archive (a ``.zip`` file on Windows, or a ``.tar.bz2`` file on other platforms). Your operating system should have built-in utilities capable of extracting files from these archives. -The release archives contain several folders, including a ``hack`` folder where -DFHack binary and system data is stored, a ``dfhack-config`` folder where user -data and configuration is stored, and a ``blueprints`` folder where `quickfort` -blueprints are stored. To install DFHack, copy all of the files from the DFHack -archive into the root DF folder, which should already include a ``data`` folder -and a ``raw`` folder, among other things. Some packs and other redistributions -of Dwarf Fortress may place DF in another folder, so ensure that the ``hack`` -folder ends up next to the ``data`` folder. +The release archives contain a ``hack`` folder where DFHack binary and system +data is stored, a ``stonesense`` folder that contains data specific to the +`stonesense` 3d renderer, and various libraries and executable files. To +install DFHack, copy all of the files from the DFHack archive into the root DF +folder, which should already include a ``data`` folder and a ``save`` folder, +among other things. Some redistributions of Dwarf Fortress may place DF in +another folder, so ensure that the ``hack`` folder ends up next to the ``data`` +folder, and you'll be fine. .. note:: @@ -112,58 +87,34 @@ folder ends up next to the ``data`` folder. overwrite ``SDL.dll`` if prompted. (If you are not prompted, you may be installing DFHack in the wrong place.) - Uninstalling DFHack =================== -Uninstalling DFHack essentially involves reversing what you did to install -DFHack. On Windows, replace ``SDL.dll`` with ``SDLreal.dll`` first. Then, you +Manually uninstalling DFHack essentially involves reversing what you did to +install. On Windows, replace ``SDL.dll`` with ``SDLreal.dll`` first. Then, you can remove any files that were part of the DFHack archive. DFHack does not currently maintain a list of these files, so if you want to completely remove them, you should consult the DFHack archive that you installed for a full list. Generally, any files left behind should not negatively affect DF. +On Steam, uninstalling DFHack will cleanly remove everything that was installed +with DFHack, **including** the ``SDL.dll`` file, which will render Dwarf +Fortress inoperative. In your Steam client, open the properties window for +Dwarf Fortress, select "Local Files", and click on "Verify integrity of game +files...". This will get Dwarf Fortress working properly again. + +Note that Steam will leave behind the ``dfhack-config`` folder, which contains +all your personal DFHack-related settings and data. If you keep this folder, +all your settings will be restored when you reinstall DFHack later. Upgrading DFHack ================ -The recommended approach to upgrade DFHack is to uninstall DFHack first, then -install the new version. This will ensure that any files that are only part -of the older DFHack installation do not affect the new DFHack installation -(although this is unlikely to occur). - -It is also possible to overwrite an existing DFHack installation in-place. -To do this, follow the installation instructions above, but overwrite all files -that exist in the new DFHack archive (on Windows, this includes ``SDL.dll`` again). - -.. note:: - - You may wish to make a backup of your ``dfhack-config`` folder first if you - have made changes to it. Some archive managers (e.g. Archive Utility on macOS) - will overwrite the entire folder, removing any files that you have added. - - -Pre-packaged DFHack installations -================================= - -There are :wiki:`several packs available ` that include -DF, DFHack, and other utilities. If you are new to Dwarf Fortress and DFHack, -these may be easier to set up. Note that these packs are not maintained by the -DFHack team and vary in their release schedules and contents. Some may make -significant configuration changes, and some may not include DFHack at all. - -Linux packages -============== - -Third-party DFHack packages are available for some Linux distributions, -including in: +Again, if you have installed from Steam, your copy of DFHack will automatically be kept up to date. This section is for manual installers. -* `AUR `__, for Arch and related - distributions -* `RPM Fusion `__, - for Fedora and related distributions +First, remove the ``hack`` and ``stonesense`` folders in their entirety. This +ensures that files that don't exist in the latest version are properly removed +and don't affect your new installation. -Note that these may lag behind DFHack releases. If you want to use a newer -version of DFHack, we generally recommended installing it in a clean copy of DF -in your home folder. Attempting to upgrade an installation of DFHack from a -package manager may break it. +Then, extract the DFHack release archive into your Dwarf Fortress folder, +overwriting any remaining top-level files (including SDL.dll). diff --git a/docs/Quickstart.rst b/docs/Quickstart.rst index 065ba6ecc..f4a022a9c 100644 --- a/docs/Quickstart.rst +++ b/docs/Quickstart.rst @@ -162,7 +162,7 @@ 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. +manager orders screen, you can see all the orders that have been created for you. Note that you could have imported the orders directly from this screen as well, using the DFHack `overlay` widget at the bottom of the manager orders panel. diff --git a/docs/Tags.rst b/docs/Tags.rst index 60a6a6813..ded15c4c2 100644 --- a/docs/Tags.rst +++ b/docs/Tags.rst @@ -21,7 +21,9 @@ for the tag assignment spreadsheet. "why" tags ---------- -- `armok `: Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden. +- `armok `: Tools which give the player god-like powers of any variety, such as control over game events, creating items from thin air, or viewing information the game intentionally keeps hidden. Players that do not wish to see these tools listed in DFHack command lists can hide them in the ``Preferences`` tab of `gui/control-panel`. + + - `auto `: Tools that run in the background and automatically manage routine, toilsome aspects of your fortress. - `bugfix `: Tools that fix specific bugs, either permanently or on-demand. - `design `: Tools that help you design your fort. @@ -49,4 +51,4 @@ for the tag assignment spreadsheet. "misc" tags ----------- -- `untested `: Tools that are not yet tested with the current release. +- `unavailable `: Tools that are not yet available for the current release. diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 75b9eb4f7..cf74c6412 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -12,6 +12,7 @@ Name Github Other 8Z 8Z Abel abstern acwatkins acwatkins +Alex Blamey Cubittus Alexander Collins gearsix Alexander Gavrilov angavrilov ag Amber Brown hawkowl @@ -133,6 +134,7 @@ Milo Christiansen milochristiansen MithrilTuxedo MithrilTuxedo mizipzor mizipzor moversti moversti +mrrho mrrho Murad Beybalaev Erquint Myk Taylor myk002 napagokc napagokc @@ -158,6 +160,7 @@ Petr Mrázek peterix Pfhreak Pfhreak Pierre Lulé plule Pierre-David Bélanger pierredavidbelanger +PopnROFL PopnROFL potato ppaawwll ppaawwll ðŸ‡ðŸ‡ðŸ‡ðŸ‡ Priit Laes plaes @@ -201,6 +204,7 @@ SeerSkye SeerSkye seishuuu seishuuu Seth Woodworth sethwoodworth Shim Panze Shim-Panze +Silver silverflyone simon Simon Jackson sizeak Simon Lees simotek @@ -235,6 +239,7 @@ ViTuRaS ViTuRaS Vjek vjek Warmist warmist Wes Malone wesQ3 +Will H TSM-EVO Will Rogers wjrogers WoosterUK WoosterUK XianMaeve XianMaeve diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 01669fac4..174fc56c4 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -26,6 +26,12 @@ Moved frequently used materials to the top of the materials list when building buildings. Also offered extended options when building constructions. All functionality has been merged into `buildingplan`. +.. _autounsuspend: + +autounsuspend +============= +Replaced by `suspendmanager`. + .. _combine-drinks: combine-drinks @@ -193,6 +199,19 @@ show-unit-syndromes =================== Replaced with a GUI version: `gui/unit-syndromes`. +.. _stocksettings: + +stocksettings +============= +Along with ``copystock``, ``loadstock`` and ``savestock``, replaced with the new +`stockpiles` API. + +.. _title-version: + +title-version +============= +Replaced with an `overlay`. + .. _warn-stuck-trees: warn-stuck-trees diff --git a/docs/changelog.txt b/docs/changelog.txt index b565f533f..66904aa6d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -26,7 +26,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ]]] ================================================================================ -======== IMPORTANT: rename this, and add a new "future" section, BEFORE ======== +======== IMPORTANT: rename this, and add a new "Future" section, BEFORE ======== ======== making a new DFHack release, even if the only changes made ======== ======== were in submodules with their own changelogs! ======== ================================================================================ @@ -36,16 +36,109 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins ## Fixes +- `autoclothing`: eliminate game lag when there are many inventory items in the fort +- `buildingplan`: fixed size limit calculations for rollers +- `buildingplan`: fixed items not being checked for accessibility in the filter and item selection dialogs +- `dig-now`: properly detect and complete smoothing designations that have been converted into active jobs ## Misc Improvements +- `buildingplan`: planner panel is minimized by default and now remembers minimized state +- `buildingplan`: can now filter by gems (for gem windows) and yarn (for ropes in wells) +- ``toggle-kbd-cursor``: add hotkey for toggling the keyboard cursor (Alt-K) +- ``version``: add alias to display the DFHack help (including the version number) so something happens when players try to run "version" +- `gui/control-panel`: add preference option for hiding the terminal console on startup +- `gui/control-panel`: add preference option for hiding "armok" tools in command lists +- ``Dwarf Therapist``: add a warning to the Labors screen when Dwarf Therapist is active so players know that changes they make to that screen will have no effect. If you're starting a new embark and nobody seems to be doing anything, check your Labors tab for this warning to see if Dwarf Therapist thinks it is in control (even if it's not running). +- `overlay`: add the DFHack version string to the DF title screen ## Documentation ## API ## Lua +- ``widgets.RangeSlider``: new mouse-controlled two-headed slider widget +- ``gui.ZScreenModal``: ZScreen subclass for modal dialogs +- ``widgets.CycleHotkeyLabel``: exposed "key_sep" and "val_gap" attributes for improved stylistic control. ## Removed +- `title-version`: replaced by an `overlay` widget + +# 50.07-r1 + +## New Plugins +- `faststart`: speeds up the "Loading..." screen so the Main Menu appears faster + +## Fixes +-@ `hotkeys`: hotkey hints on menu popup will no longer get their last character cut off by the scrollbar +-@ ``launchdf``: launch Dwarf Fortress via the Steam client so Steam Workshop is functional +- `blueprint`: interpret saplings, shrubs, and twigs as floors instead of walls +- `combine`: fix error processing stockpiles with boundaries that extend outside of the map +-@ `prospector`: display both "raw" Z levels and "cooked" elevations +- `stockpiles`: fix crash when importing settings for gems from other worlds +-@ `stockpiles`: allow numbers in saved stockpile filenames + +## Misc Improvements +-@ `buildingplan`: items in the item selection dialog should now use the same item quality symbols as the base game +-@ `buildingplan`: hide planner overlay while the DF tutorial is active so that it can detect when you have placed the carpenter's workshop and bed and allow you to finish the tutorial +- `buildingplan`: can now filter by cloth and silk materials (for ropes) +-@ `buildingplan`: rearranged elements of ``planneroverlay`` interface +-@ `buildingplan`: rearranged elements of ``itemselection`` interface +-@ Mods: scripts in mods that are only in the steam workshop directory are now accessible. this means that a script-only mod that you never mark as "active" when generating a world will still receive automatic updates and be usable from in-game +-@ Mods: scripts from only the most recent version of an installed mod are added to the script path +-@ Mods: give active mods a chance to reattach their load hooks when a world is reloaded +- `gui/control-panel`: bugfix services are now enabled by default +- Core: hide DFHack terminal console by default when running on Steam Deck + +## Documentation +- `installing`: updated to include Steam installation instructions + +## Lua +- added two new window borders: ``gui.BOLD_FRAME`` for accented elements and ``gui.INTERIOR_MEDIUM_FRAME`` for a signature-less frame that's thicker than the existing ``gui.INTERIOR_FRAME`` + +# 50.07-beta2 + +## New Plugins +- `getplants`: designate trees for chopping and shrubs for gathering according to type +- `prospector`: get stone, ore, gem, and other tile property counts in fort mode. + +## Fixes +-@ `buildingplan`: filters are now properly applied to planned stairs +-@ `buildingplan`: existing carved up/down stairs are now taken into account when determining which stair shape to construct +- `buildingplan`: upright spike traps are now placed extended rather than retracted +- `buildingplan`: you can no longer designate constructions on tiles with magma or deep water, mirroring the vanilla restrictions +-@ `buildingplan`: fixed material filters getting lost for planning buildings on save/reload +-@ `buildingplan`: respect building size limits (e.g. roads and bridges cannot be more than 31 tiles in any dimension) +- `tailor`: properly discriminate between dyed and undyed cloth +-@ `tailor`: no longer default to using adamantine cloth for producing clothes +- `tailor`: take queued orders into account when calculating available materials +- `tailor`: skip units who can't wear clothes +- `tailor`: identify more available items as available, solving issues with over-production + +## Misc Improvements +- `buildingplan`: filters and global settings are now ignored when manually choosing items for a building, allowing you to make custom choices independently of the filters that would otherwise be used +- `buildingplan`: if `suspendmanager` is running, then planned buildings will be left suspended when their items are all attached. `suspendmanager` will unsuspsend them for construction when it is safe to do so. +- `buildingplan`: add option for autoselecting the last manually chosen item (like `automaterial` used to do) +- `confirm`: adds confirmation for removing burrows via the repaint menu +- `stockpiles`: support applying stockpile configurations with fully enabled categories to stockpiles in worlds other than the one where the configuration was exported from +- `stockpiles`: support partial application of a saved config based on dynamic filtering (e.g. disable all tallow in a food stockpile, even tallow from world-specific generated creatures) +- `stockpiles`: additive and subtractive modes when applying a second stockpile configuration on top of a first +- `stockpiles`: write player-exported stockpile configurations to the ``dfhack-config/stockpiles`` folder. If you have any stockpile configs in other directories, please move them to that folder. +- `stockpiles`: now includes a library of useful stockpile configs (see docs for details) +- `automelt`: now allows metal chests to be melted (workaround for DF bug 2493 is no longer needed) +- `orders`: add minimize button to overlay panel so you can get it out of the way to read long statue descriptions when choosing a subject in the details screen +- `orders`: add option to delete exported files from the import dialog +- `enable`: can now interpret aliases defined with the `alias` command +- Mods: scripts in mods are now automatically added to the DFHack script path. DFHack recognizes two directories in a mod's folder: ``scripts_modinstalled/`` and ``scripts_modactive/``. ``scripts_modinstalled/`` folders will always be added the script path, regardless of whether the mod is active in a world. ``scripts_modactive/`` folders will only be added to the script path when the mod is active in the current loaded world. + +## Documentation +- `modding-guide`: guide updated to include information for 3rd party script developers +- the ``untested`` tag has been renamed to ``unavailable`` to better reflect the status of the remaining unavaialable tools. most of the simply "untested" tools have now been tested and marked as working. the remaining tools are known to need development work before they are available again. + +## Lua +- ``widgets.Label``: tokens can now specify a ``htile`` property to indicate the tile that should be shown when the Label is hovered over with the mouse +- ``widgets.Label``: click handlers no longer get the label itself as the first param to the click handler +- ``widgets.CycleHotkeyLabel``: options that are bare integers will no longer be interpreted as the pen color in addition to being the label and value +- ``widgets.CycleHotkeyLabel``: option labels and pens can now be functions that return a label or pen # 50.07-beta1 @@ -107,14 +200,14 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Fixes -@ `nestboxes`: fixed bug causing nestboxes themselves to be forbidden, which prevented citizens from using them to lay eggs. Now only eggs are forbidden. -- `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. +-@ `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. -@ `seedwatch`: fix saving and loading of seed stock targets - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. -@ `autochop`: generate default names for burrows with no assigned names - ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. - `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment -@ `confirm`: fix fps drop when enabled -- `channel-safely`: fix an out of bounds error regarding the REPORT event listener receiving (presumably) stale id's +-@ `channel-safely`: fix an out of bounds error regarding the REPORT event listener receiving (presumably) stale id's ## Misc Improvements - `autobutcher`: logs activity to the console terminal instead of making disruptive in-game announcements @@ -138,13 +231,13 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Fixes - ``Units::isFortControlled``: Account for agitated wildlife -@ Fix right click sometimes closing both a DFHack window and a vanilla panel -- Fixed issue with scrollable lists having some data off-screen if they were scrolled before being made visible -- `channel-safely`: fixed bug resulting in marker mode never being set for any designation +-@ Fixed issue with scrollable lists having some data off-screen if they were scrolled before being made visible +-@ `channel-safely`: fixed bug resulting in marker mode never being set for any designation -@ `automelt`: fixed bug related to lua stack smashing behavior in returned stockpile configs -@ `autochop`: fixed bug related to lua stack smashing behavior in returned stockpile configs -- `nestboxes`: now cancels any in-progress hauling jobs when it protects a fertile egg +-@ `nestboxes`: now cancels any in-progress hauling jobs when it protects a fertile egg -@ Fix persisted data not being written on manual save -- `nestboxes`: now scans for eggs more frequently and cancels any in-progress hauling jobs when it protects a fertile egg +-@ `nestboxes`: now scans for eggs more frequently and cancels any in-progress hauling jobs when it protects a fertile egg ## Misc Improvements -@ `automelt`: is now more resistent to vanilla savegame corruption @@ -236,7 +329,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # 50.05-alpha2 ## Fixes -- `autofarm`: don't duplicate status line entries for crops with no current supply +-@ `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) - `tailor`: now respects the setting of the "used dyed clothing" standing order toggle diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 9f2386660..37d85f7f6 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4323,6 +4323,13 @@ Here is an example skeleton for a ZScreen tool window:: view = view and view:raise() or MyScreen{}:show() +ZScreenModal class +------------------ + +A ZScreen convenience subclass that sets the attributes to something +appropriate for modal dialogs. The game is force paused, and no input is passed +through to the underlying viewscreens. + FramedScreen class ------------------ @@ -4352,11 +4359,22 @@ There are the following predefined frame style tables: A frame suitable for overlay widget panels. +* ``BOLD_FRAME`` + + A frame suitable for a non-draggable panel meant to capture the user's focus, + like an important notification, confirmation dialog or error message. + * ``INTERIOR_FRAME`` - A frame suitable for light interior accent elements. This frame does *not* have - a visible ``DFHack`` signature on it, so it must not be used as the most external - frame for a DFHack-owned UI. + A frame suitable for light interior accent elements. This frame does *not* + have a visible ``DFHack`` signature on it, so it must not be used as the most + external frame for a DFHack-owned UI. + +* ``INTERIOR_MEDIUM_FRAME`` + + A copy of ``MEDIUM_FRAME`` that lacks the ``DFHack`` signature. Suitable for + panels that are part of a larger widget cluster. Must *not* be used as the + most external frame for a DFHack-owned UI. gui.widgets =========== @@ -4731,10 +4749,12 @@ containing newlines, or a table with the following possible fields: Specifies the number of character positions to advance on the line before rendering the token. -* ``token.tile = pen`` +* ``token.tile``, ``token.htile`` Specifies a pen or texture index (or a function that returns a pen or texture - index) to paint as one tile before the main part of the token. + index) to paint as one tile before the main part of the token. If ``htile`` + is specified, that is used instead of ``tile`` when the Label is hovered over + with the mouse. * ``token.width = ...`` @@ -4762,10 +4782,10 @@ containing newlines, or a table with the following possible fields: Same as the attributes of the label itself, but applies only to the token. -* ``token.pen``, ``token.dpen`` +* ``token.pen``, ``token.dpen``, ``token.hpen`` - Specify the pen and disabled pen to be used for the token's text. - The field may be either the pen itself, or a callback that returns it. + Specify the pen, disabled pen, and hover pen to be used for the token's text. + The fields may be either the pen itself, or a callback that returns it. * ``token.on_activate`` @@ -4892,15 +4912,20 @@ It has the following attributes: :key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``. :key_back: Similar to ``key``, but will cycle backwards (optional) +:key_sep: If specified, will be used to customize how the activation key is + displayed. See ``token.key_sep`` in the ``Label`` documentation. :label: The string (or a function that returns a string) to display after the hotkey. :label_width: The number of spaces to allocate to the ``label`` (for use in aligning a column of ``CycleHotkeyLabel`` labels). :label_below: If ``true``, then the option value will apear below the label instead of to the right of it. Defaults to ``false``. +:val_gap: The size of the gap between the label text and the option value. + Default is ``1``. If set to ``0``, there'll be no gap between the strings. + Note that ``val_gap`` is ignored if ``label_below`` is set to ``true``. :options: A list of strings or tables of - ``{label=string, value=string[, pen=pen]}``. String options use the same - string for the label and value and the default pen. The optional ``pen`` + ``{label=string or fn, value=val[, pen=pen]}``. String options use the same + string for the label and value and use the default pen. The optional ``pen`` element could be a color like ``COLOR_RED``. :initial_option: The value or numeric index of the initial option. :on_change: The callback to call when the selected option changes. It is called @@ -5117,6 +5142,20 @@ widget does not require direct usage of ``Tab``. usage of ``Tab`` in ``TabBar:init()`` for an example. See the default value of ``active_tab_pens`` or ``inactive_tab_pens`` in ``TabBar`` for an example of how to construct pens. +RangeSlider class +----------------- + +This widget implements a mouse-interactable range-slider. The player can move its two handles to set minimum and maximum values +to define a range, or they can drag the bar itself to move both handles at once. +The parent widget owns the range values, and can control them independently (e.g. with ``CycleHotkeyLabels``). If the range values change, the ``RangeSlider`` appearance will adjust automatically. + +:num_stops: Used to specify the number of "notches" in the range slider, the places where handles can stop. + (this should match the parents' number of options) +:get_left_idx_fn: The function used by the RangeSlider to get the notch index on which to display the left handle. +:get_right_idx_fn: The function used by the RangeSlider to get the notch index on which to display the right handle. +:on_left_change: Callback executed when moving the left handle. +:on_right_change: Callback executed when moving the right handle. + .. _lua-plugins: ======= diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 73d9407d6..38117503c 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -8,70 +8,128 @@ DFHack modding guide What is the difference between a script and a mod? -------------------------------------------------- -A script is a single file that can be run as a command in DFHack, like something -that modifies or displays game data on request. A mod is something you install -to get persistent behavioural changes in the game and/or add new content. Mods -can contain and use scripts in addition to (or instead of) modifications to the -DF game raws. - -DFHack scripts are written in Lua. If you don't already know Lua, there's a -great primer at `lua.org `__. +Well, sometimes there is no difference. A mod is anything you add to the game, +which can be graphics overrides, content in the raws, DFHack scripts, any, or +all. There are already resources out there for +`raws modding `__, so this +guide will focus more on scripts, both standalone and as an extension to +raws-based mods. + +A DFHack script is a Lua file that can be run as a command in +DFHack. Scripts can do pretty much anything, from displaying information to +enforcing new game mechanics. If you don't already know Lua, there's a great +primer at `lua.org `__. Why not just mod the raws? -------------------------- It depends on what you want to do. Some mods *are* better to do in just the -raws. You don't need DFHack to add a new race or modify attributes, for example. -However, DFHack scripts can do many things that you just can't do in the raws, -like make a creature that trails smoke. Some things *could* be done in the raws, -but writing a script is less hacky, easier to maintain, easier to extend, and is -not prone to side-effects. A great example is adding a syndrome when a reaction -is performed. If done in the raws, you have to create an exploding boulder to -apply the syndrome. DFHack scripts can add the syndrome directly and with much -more flexibility. In the end, complex mods will likely require a mix of raw -modding and DFHack scripting. +raws. You don't need DFHack to add a new race or modify attributes. However, +DFHack scripts can do many things that you just can't do in the raws, like make +a creature that trails smoke or launch a unit into the air when they are hit +with a certain type of projectile. Some things *could* be done in the raws, but +a script is better (e.g. easier to maintain, easier to extend, and/or not prone +to side-effects). A great example is adding a syndrome when a reaction +is performed. If done in the raws, you have to create an exploding boulder as +an intermediary to apply the syndrome. DFHack scripts can add the syndrome +directly and with much more flexibility. In the end, complex mods will likely +require a mix of raw modding and DFHack scripting. + +The structure of a mod +---------------------- + +In the example below, we'll use a mod name of ``example-mod``. I'm sure your +mods will have more creative names! Mods have a basic structure that looks like +this:: + + info.txt + graphics/... + objects/... + scripts_modactive/example-mod.lua + scripts_modactive/internal/example-mod/... + scripts_modinstalled/... + README.md (optional) + +Let's go through that line by line. + +- The :file:`info.txt` file contains metadata about your mod that DF will + display in-game. You can read more about this file in the + `Official DF Modding Guide `__. +- Modifications to the game raws (potentially with custom raw tokens) go in + the :file:`graphics/` and :file:`objects/` folders. You can read more about + the files that go in these directories on the :wiki:`Modding` wiki page. +- A control script in :file:`scripts_modactive/` directory that handles + system-level event hooks (e.g. reloading state when a world is loaded), + registering `overlays `, and + `enabling/disabling ` your mod. You can put other + scripts in this directory as well if you want them to appear as runnable + DFHack commands when your mod is active for the current world. Lua modules + that your main scripts use, but which don't need to be directly runnable by + the player, should go in a subdirectory under + :file:`scripts_modactive/internal/` so they don't show up in the DFHack + `launcher ` command autocomplete lists. +- Scripts that you want to be available before a world is loaded (i.e. on the + DF title screen) or that you want to be runnable in any world, regardless + of whether your mod is active, should go in the + :file:`scripts_modinstalled/` folder. You can also have an :file:`internal/` + subfolder in here for private modules if you like. +- Finally, a :file:`README.md` file that has more information about your mod. + If you develop your mod using version control (recommended!), that + :file:`README.md` file can also serve as your git repository documentation. + +These files end up in a subdirectory under :file:`mods/` when players copy them +in or install them from the +`Steam Workshop `__, and in +:file:`data/installed_mods/` when the mod is selected as "active" for the first +time. + +What if I just want to distribute a simple script? +-------------------------------------------------- + +If your mod is just a script with no raws modifications, things get a bit +simpler. All you need is:: + + info.txt + scripts_modinstalled/yourscript.lua + README.md (optional) + +Adding your script to the :file:`scripts_modinstalled/` folder will allow +DFHack to find it and add your mod to the `script-paths`. Your script will be +runnable from the title screen and in any loaded world, regardless of whether +your mod is explicitly "active". A mod-maker's development environment ------------------------------------- -While you're writing your mod, you need a place to store your in-development -scripts that will: +Create a folder for development somewhere outside your Dwarf Fortress +installation directory (e.g. ``/path/to/mymods/``). If you work on multiple +mods, you might want to make a subdirectory for each mod. -- be directly runnable by DFHack -- not get lost when you upgrade DFHack +If you have changes to the raws, you'll have to copy them into DF's +``data/installed_mods/`` folder to have them take effect, but you can set +things up so that scripts are run directly from your dev directory. This way, +you can edit your scripts and have the changes available in the game +immediately: no copying, no restarting. -The recommended approach is to create a directory somewhere outside of your DF -installation (let's call it "/path/to/own-scripts") and do all your script -development in there. +How does this magic work? Just add a line like this to your +``dfhack-config/script-paths.txt`` file:: -Inside your DF installation folder, there is a file named -:file:`dfhack-config/script-paths.txt`. If you add a line like this to that -file:: - - +/path/to/own-scripts + +/path/to/mymods/example-mod/scripts_modinstalled Then that directory will be searched when you run DFHack commands from inside the game. The ``+`` at the front of the path means to search that directory -first, before any other script directory (like :file:`hack/scripts` or -:file:`raw/scripts`). That way, your latest changes will always be used instead -of older copies that you may have installed in a DF directory. - -For scripts with the same name, the `order of precedence ` will -be: - -1. ``own-scripts/`` -2. ``dfhack-config/scripts/`` -3. ``save/*/scripts/`` -4. ``hack/scripts/`` +first, before any other script directory (like :file:`hack/scripts` or other +versions of your mod in the DF mod folders). The structure of the game ------------------------- -"The game" is in the global variable `df `. The game's memory can be -found in ``df.global``, containing things like the list of all items, whether to -reindex pathfinding, et cetera. Also relevant to us in ``df`` are the various -types found in the game, e.g. ``df.pronoun_type`` which we will be using in this -guide. We'll explore more of the game structures below. +"The game" is in the global variable `df `. Most of the information +relevant to a script is found in ``df.global.world``, which contains things +like the list of all items, whether to reindex pathfinding, et cetera. Also +relevant to us are the various data types found in the game, e.g. +``df.pronoun_type`` which we will be using in this guide. We'll explore more of +the game structures below. Your first script ----------------- @@ -83,8 +141,8 @@ First line, we get the unit:: local unit = dfhack.gui.getSelectedUnit() -If no unit is selected, an error message will be printed (which can be silenced -by passing ``true`` to ``getSelectedUnit``) and ``unit`` will be ``nil``. +If no unit is selected, ``unit`` will be ``nil`` and an error message will be +printed (which can be silenced by passing ``true`` to ``getSelectedUnit``). If ``unit`` is ``nil``, we don't want the script to run anymore:: @@ -94,33 +152,32 @@ If ``unit`` is ``nil``, we don't want the script to run anymore:: Now, the field ``sex`` in a unit is an integer, but each integer corresponds to a string value ("it", "she", or "he"). We get this value by indexing the -bidirectional map ``df.pronoun_type``. Indexing the other way, incidentally, -with one of the strings, will yield its corresponding number. So:: +bidirectional map ``df.pronoun_type``. Indexing the other way, with one of the +strings, will yield its corresponding number. So:: local pronounTypeString = df.pronoun_type[unit.sex] print(pronounTypeString) -Simple. Save this as a Lua file in your own scripts directory and run it as -shown before when a unit is selected in the Dwarf Fortress UI. +Simple. Save this as a Lua file in your own scripts directory and run it from +`gui/launcher` when a unit is selected in the Dwarf Fortress UI. -Exploring DF structures ------------------------ +Exploring DF state +------------------ So how could you have known about the field and type we just used? Well, there are two main tools for discovering the various fields in the game's data structures. The first is the ``df-structures`` `repository `__ that contains XML files -describing the contents of the game's structures. These are complete, but +describing the layouts of the game's structures. These are complete, but difficult to read (for a human). The second option is the `gui/gm-editor` -script, an interactive data explorer. You can run the script while objects like -units are selected to view the data within them. You can also run -``gui/gm-editor scr`` to view the data for the current screen. Press :kbd:`?` -while the script is active to view help. +interface, an interactive data explorer. You can run the script while objects +like units are selected to view the data within them. Press :kbd:`?` while the +script is active to view help. Familiarising yourself with the many structs of the game will help with ideas immensely, and you can always ask for help in the `right places `. -Detecting triggers +Reacting to events ------------------ The common method for injecting new behaviour into the game is to define a @@ -130,7 +187,7 @@ provides two libraries for this, ``repeat-util`` and `eventful `. frames (paused or unpaused), ticks (unpaused), in-game days, months, or years. If you need to be aware the instant something happens, you'll need to run a check once a tick. Be careful not to do this gratuitously, though, since -running that often can slow down the game! +running callbacks too often can slow down the game! ``eventful``, on the other hand, is much more performance-friendly since it will only call your callback when a relevant event happens, like a reaction or job @@ -327,7 +384,8 @@ Then, let's make a ``repeat-util`` callback for once a tick:: repeatUtil.scheduleEvery(modId, 1, "ticks", function() Let's iterate over every active unit, and for every unit, iterate over their -worn items to calculate how much we are going to take from their on-foot movement timers:: +worn items to calculate how much we are going to take from their on-foot +movement timers:: for _, unit in ipairs(df.global.world.units.active) do local amount = 0 @@ -341,82 +399,89 @@ worn items to calculate how much we are going to take from their on-foot movemen end -- Subtract amount from on-foot movement timers if not on ground if not unit.flags1.on_ground then - dfhack.units.subtractActionTimers(unit, amount, df.unit_action_type_group.MovementFeet) + dfhack.units.subtractActionTimers(unit, amount, + df.unit_action_type_group.MovementFeet) end end -The structure of a full mod ---------------------------- - -For reference, `Tachy Guns `__ is a -full mod that conforms to this guide. - -Create a folder for mod projects somewhere outside your Dwarf Fortress -installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the -names for the mod folders within it. In the example below, we'll use a mod ID of -``example-mod``. I'm sure your mods will have more creative names! The -``example-mod`` mod will be developed in the ``/path/to/mymods/example-mod/`` -directory and has a basic structure that looks like this:: - - init.d/example-mod.lua - raw/objects/... - raw/scripts/example-mod.lua - raw/scripts/example-mod/... - README.md +Putting it all together +----------------------- -Let's go through that line by line. +Ok, you're all set up! Now, let's take a look at an example +``scripts_modinstalled/example-mod.lua`` file:: -* 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 ``scripts/`` that handles enabling and disabling your - mod. -* A subfolder for your mod under ``scripts/`` will contain all the internal - scripts and/or modules used by your mod. + -- main file for example-mod -It is a good idea to use a version control system to organize changes to your -mod code. You can create a separate Git repository for each of your mods. The -``README.md`` file will be your mod help text when people browse to your online -repository. + -- these lines indicate that the script supports the "enable" + -- API so you can start it by running "enable example-mod" and + -- stop it by running "disable example-mod" + --@module = true + --@enable = true -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``:: + -- this is the help text that will appear in `help` and + -- `gui/launcher`. see possible tags here: + -- https://docs.dfhack.org/en/latest/docs/Tags.html + --[====[ + example-mod + =========== - +/path/to/mymods/example-mod/scripts/ + Tags: fort | gameplay -Ok, you're all set up! Now, let's take a look at an example -``scripts/example-mod.lua`` file:: + Short one-sentence description ... - -- main setup and teardown for example-mod - -- this next line indicates that the script supports the "enable" - -- API so you can start it by running "enable example-mod" and stop - -- it by running "disable example-mod" - --@ enable = true + Longer description ... - local usage = [[ Usage ----- enable example-mod disable example-mod - ]] + ]====] + local repeatUtil = require('repeat-util') local eventful = require('plugins.eventful') -- you can reference global values or functions declared in any of -- your internal scripts - local moduleA = reqscript('example-mod/module-a') - local moduleB = reqscript('example-mod/module-b') - local moduleC = reqscript('example-mod/module-c') - local moduleD = reqscript('example-mod/module-d') + local moduleA = reqscript('internal/example-mod/module-a') + local moduleB = reqscript('internal/example-mod/module-b') + local moduleC = reqscript('internal/example-mod/module-c') + local moduleD = reqscript('internal/example-mod/module-d') + + local GLOBAL_KEY = 'example-mod' enabled = enabled or false - local modId = 'example-mod' + + function isEnabled() + -- this function is for the enabled API, the script won't show up on the + -- control panel without it + return enabled + end + + dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + dfhack.run_command('disable', 'example-mod') + + -- ensure our mod doesn't try to enable itself when a different + -- world is loaded where we are *not* active + dfhack.onStateChange[GLOBAL_KEY] = nil + + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + dfhack.run_command('enable', 'example-mod') + end + + if dfhack_flags.module then + return + end if not dfhack_flags.enable then - print(usage) + print(dfhack.script_help()) print() print(('Example mod is currently '):format( enabled and 'enabled' or 'disabled')) @@ -472,23 +537,17 @@ Ok, you're all set up! Now, let's take a look at an example enabled = false end -You can call ``enable example-mod`` and ``disable example-mod`` yourself while -developing, but for end users you can start your mod automatically from -``init.d/example-mod.lua``:: - - dfhack.run_command('enable example-mod') - -Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: +Inside ``scripts_modinstalled/internal/example-mod/module-a.lua`` you could +have code like this:: --@ module = true - -- The above line is required for reqscript to work function onLoad() -- global variables are exported -- do initialization here end - -- this is an internal function: local functions/variables - -- are not exported + -- this is a local function: local functions/variables + -- are not accessible to other scripts. local function usedByOnTick(unit) -- ... end @@ -499,6 +558,6 @@ Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this:: end end -The `reqscript ` function reloads scripts that have changed, so you can modify -your scripts while DF is running and just disable/enable your mod to load the -changes into your ongoing game! +The `reqscript ` function reloads scripts that have changed, so you +can modify your scripts while DF is running and just disable/enable your mod to +load the changes into your ongoing game! diff --git a/docs/plugins/3dveins.rst b/docs/plugins/3dveins.rst index 1d7acd94f..3112934ab 100644 --- a/docs/plugins/3dveins.rst +++ b/docs/plugins/3dveins.rst @@ -3,7 +3,7 @@ .. dfhack-tool:: :summary: Rewrite layer veins to expand in 3D space. - :tags: untested fort gameplay map + :tags: unavailable fort gameplay map Existing, flat veins are removed and new 3D veins that naturally span z-levels are generated in their place. The transformation preserves the mineral counts diff --git a/docs/plugins/add-spatter.rst b/docs/plugins/add-spatter.rst index fc3c507c2..6914690ea 100644 --- a/docs/plugins/add-spatter.rst +++ b/docs/plugins/add-spatter.rst @@ -3,7 +3,7 @@ add-spatter .. dfhack-tool:: :summary: Make tagged reactions produce contaminants. - :tags: untested adventure fort gameplay items + :tags: unavailable adventure fort gameplay items :no-command: Give some use to all those poisons that can be bought from caravans! The plugin diff --git a/docs/plugins/autogems.rst b/docs/plugins/autogems.rst index f18b671a1..6135b39d2 100644 --- a/docs/plugins/autogems.rst +++ b/docs/plugins/autogems.rst @@ -3,7 +3,7 @@ autogems .. dfhack-tool:: :summary: Automatically cut rough gems. - :tags: untested fort auto workorders + :tags: unavailable fort auto workorders :no-command: .. dfhack-command:: autogems-reload diff --git a/docs/plugins/autotrade.rst b/docs/plugins/autotrade.rst index 439eead40..24c1b42fa 100644 --- a/docs/plugins/autotrade.rst +++ b/docs/plugins/autotrade.rst @@ -3,7 +3,7 @@ autotrade .. dfhack-tool:: :summary: Quickly designate items to be traded. - :tags: untested fort productivity items stockpiles + :tags: unavailable fort productivity items stockpiles :no-command: When `enabled `, this plugin adds an option to the :kbd:`q` menu for diff --git a/docs/plugins/building-hacks.rst b/docs/plugins/building-hacks.rst index cce3479d8..b45b23be4 100644 --- a/docs/plugins/building-hacks.rst +++ b/docs/plugins/building-hacks.rst @@ -3,7 +3,7 @@ building-hacks .. dfhack-tool:: :summary: Provides a Lua API for creating powered workshops. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings :no-command: See `building-hacks-api` for more details. diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 71951f386..9cc7e68a2 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -20,7 +20,10 @@ building. Once all items are attached, the construction job will be unsuspended and a dwarf will come and build the building. If you have the `unsuspend` overlay enabled (it is enabled by default), then buildingplan-suspended buildings will appear with a ``P`` marker on the main map, as opposed to the -usual ``x`` marker for "regular" suspended buildings. +usual ``x`` marker for "regular" suspended buildings. If you have +`suspendmanager` running, then buildings will be left suspended when their +items are all attached and ``suspendmanager`` will unsuspend them for +construction when it is safe to do so. If you want to impose restrictions on which items are chosen for the buildings, buildingplan has full support for quality and material filters (see `below diff --git a/docs/plugins/burrows.rst b/docs/plugins/burrows.rst index 54812ece5..299656cf5 100644 --- a/docs/plugins/burrows.rst +++ b/docs/plugins/burrows.rst @@ -3,7 +3,7 @@ burrows .. dfhack-tool:: :summary: Auto-expand burrows as you dig. - :tags: untested fort auto design productivity map units + :tags: unavailable fort auto design productivity map units :no-command: .. dfhack-command:: burrow diff --git a/docs/plugins/changeitem.rst b/docs/plugins/changeitem.rst index fe9229a09..788b49247 100644 --- a/docs/plugins/changeitem.rst +++ b/docs/plugins/changeitem.rst @@ -3,7 +3,7 @@ changeitem .. dfhack-tool:: :summary: Change item material or base quality. - :tags: untested adventure fort armok items + :tags: unavailable adventure fort armok items By default, a change is only allowed if the existing and desired item materials are of the same subtype (for example wood -> wood, stone -> stone, etc). But diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 3acbe66cd..c24f352b0 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -3,7 +3,7 @@ channel-safely .. dfhack-tool:: :summary: Auto-manage channel designations to keep dwarves safe. - :tags: untested fort auto + :tags: unavailable fort auto Multi-level channel projects can be dangerous, and managing the safety of your dwarves throughout the completion of such projects can be difficult and time diff --git a/docs/plugins/confirm.rst b/docs/plugins/confirm.rst index f0a34b909..1e6270187 100644 --- a/docs/plugins/confirm.rst +++ b/docs/plugins/confirm.rst @@ -5,8 +5,9 @@ confirm :summary: Adds confirmation dialogs for destructive actions. :tags: fort interface -Now you can get the chance to avoid accidentally disbanding a squad or deleting a -hauling route in case you hit the key accidentally. +In the base game, it is frightenly easy to destroy hours of work with a single +misclick. Now you can avoid the consequences of accidentally disbanding a squad +(for example), or deleting a hauling route. Usage ----- diff --git a/docs/plugins/createitem.rst b/docs/plugins/createitem.rst index 7abe06cff..b287042c7 100644 --- a/docs/plugins/createitem.rst +++ b/docs/plugins/createitem.rst @@ -3,7 +3,7 @@ createitem .. dfhack-tool:: :summary: Create arbitrary items. - :tags: untested adventure fort armok items + :tags: unavailable adventure fort armok items You can create new items of any type and made of any material. A unit must be selected in-game to use this command. By default, items created are spawned at diff --git a/docs/plugins/deramp.rst b/docs/plugins/deramp.rst index 641f370a6..4a1f34548 100644 --- a/docs/plugins/deramp.rst +++ b/docs/plugins/deramp.rst @@ -3,7 +3,7 @@ deramp .. dfhack-tool:: :summary: Removes all ramps designated for removal from the map. - :tags: untested fort armok map + :tags: unavailable fort armok map It also removes any "floating" down ramps that can remain after a cave-in. diff --git a/docs/plugins/digFlood.rst b/docs/plugins/digFlood.rst index 70dd9b388..ee502371a 100644 --- a/docs/plugins/digFlood.rst +++ b/docs/plugins/digFlood.rst @@ -3,7 +3,7 @@ digFlood .. dfhack-tool:: :summary: Digs out veins as they are discovered. - :tags: untested fort auto map + :tags: unavailable fort auto map Once you register specific vein types, this tool will automatically designate tiles of those types of veins for digging as your miners complete adjacent diff --git a/docs/plugins/diggingInvaders.rst b/docs/plugins/diggingInvaders.rst index 4ea52013e..001636709 100644 --- a/docs/plugins/diggingInvaders.rst +++ b/docs/plugins/diggingInvaders.rst @@ -3,7 +3,7 @@ diggingInvaders .. dfhack-tool:: :summary: Invaders dig and destroy to get to your dwarves. - :tags: untested fort gameplay military units + :tags: unavailable fort gameplay military units Usage ----- diff --git a/docs/plugins/dwarfmonitor.rst b/docs/plugins/dwarfmonitor.rst index 47d49d1e0..45a5d8f85 100644 --- a/docs/plugins/dwarfmonitor.rst +++ b/docs/plugins/dwarfmonitor.rst @@ -3,7 +3,7 @@ dwarfmonitor .. dfhack-tool:: :summary: Report on dwarf preferences and efficiency. - :tags: untested fort inspection jobs units + :tags: unavailable fort inspection jobs units It can also show heads-up display widgets with live fort statistics. diff --git a/docs/plugins/dwarfvet.rst b/docs/plugins/dwarfvet.rst index 7480c10a6..b4dfe0ada 100644 --- a/docs/plugins/dwarfvet.rst +++ b/docs/plugins/dwarfvet.rst @@ -3,7 +3,7 @@ dwarfvet .. dfhack-tool:: :summary: Allows animals to be treated at animal hospitals. - :tags: untested fort gameplay animals + :tags: unavailable fort gameplay animals Annoyed that your dragons become useless after a minor injury? Well, with dwarfvet, injured animals will be treated at an animal hospital, which is simply diff --git a/docs/plugins/embark-assistant.rst b/docs/plugins/embark-assistant.rst index bb74f221d..282d4b122 100644 --- a/docs/plugins/embark-assistant.rst +++ b/docs/plugins/embark-assistant.rst @@ -3,7 +3,7 @@ embark-assistant .. dfhack-tool:: :summary: Embark site selection support. - :tags: untested embark fort interface + :tags: unavailable embark fort interface Run this command while the pre-embark screen is displayed to show extended (and reasonably correct) resource information for the embark rectangle as well as diff --git a/docs/plugins/embark-tools.rst b/docs/plugins/embark-tools.rst index 6a600019b..f320706ff 100644 --- a/docs/plugins/embark-tools.rst +++ b/docs/plugins/embark-tools.rst @@ -3,7 +3,7 @@ embark-tools .. dfhack-tool:: :summary: Extend the embark screen functionality. - :tags: untested embark fort interface + Usage ----- diff --git a/docs/plugins/faststart.rst b/docs/plugins/faststart.rst new file mode 100644 index 000000000..b39269e01 --- /dev/null +++ b/docs/plugins/faststart.rst @@ -0,0 +1,18 @@ +faststart +========= + +.. dfhack-tool:: + :summary: Makes the main menu appear sooner. + :tags: dfhack interface + :no-command: + +This plugin accelerates the initial "Loading..." screen that appears when the +game first starts, so you don't have to wait as long before the Main Menu +appears and you can start playing. + +Usage +----- + +:: + + enable faststart diff --git a/docs/plugins/fix-unit-occupancy.rst b/docs/plugins/fix-unit-occupancy.rst index 8589f765a..6ba01b712 100644 --- a/docs/plugins/fix-unit-occupancy.rst +++ b/docs/plugins/fix-unit-occupancy.rst @@ -3,7 +3,7 @@ fix-unit-occupancy .. dfhack-tool:: :summary: Fix phantom unit occupancy issues. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map If you see "unit blocking tile" messages that you can't account for (:bug:`3499`), this tool can help. diff --git a/docs/plugins/fixveins.rst b/docs/plugins/fixveins.rst index c13bbe9a1..552c5f8c3 100644 --- a/docs/plugins/fixveins.rst +++ b/docs/plugins/fixveins.rst @@ -3,7 +3,7 @@ fixveins .. dfhack-tool:: :summary: Restore missing mineral inclusions. - :tags: untested fort bugfix map + :tags: unavailable fort bugfix map This tool can also remove invalid references to mineral inclusions if you broke your embark with tools like `tiletypes`. diff --git a/docs/plugins/flows.rst b/docs/plugins/flows.rst index 209bb577c..56840d999 100644 --- a/docs/plugins/flows.rst +++ b/docs/plugins/flows.rst @@ -3,7 +3,7 @@ flows .. dfhack-tool:: :summary: Counts map blocks with flowing liquids. - :tags: untested fort inspection map + :tags: unavailable fort inspection map If you suspect that your magma sea leaks into HFS, you can use this tool to be sure without revealing the map. diff --git a/docs/plugins/follow.rst b/docs/plugins/follow.rst index 47db40bd8..e72f79ce0 100644 --- a/docs/plugins/follow.rst +++ b/docs/plugins/follow.rst @@ -3,7 +3,7 @@ follow .. dfhack-tool:: :summary: Make the screen follow the selected unit. - :tags: untested fort interface units + :tags: unavailable fort interface units Once you exit from the current menu or cursor mode, the screen will stay centered on the unit. Handy for watching dwarves running around. Deactivated by diff --git a/docs/plugins/forceequip.rst b/docs/plugins/forceequip.rst index d4c9a6c4a..2565d12c3 100644 --- a/docs/plugins/forceequip.rst +++ b/docs/plugins/forceequip.rst @@ -3,7 +3,7 @@ forceequip .. dfhack-tool:: :summary: Move items into a unit's inventory. - :tags: untested adventure fort animals items military units + :tags: unavailable adventure fort animals items military units This tool is typically used to equip specific clothing/armor items onto a dwarf, but can also be used to put armor onto a war animal or to add unusual items diff --git a/docs/plugins/generated-creature-renamer.rst b/docs/plugins/generated-creature-renamer.rst index ba1761a1b..ea386eacf 100644 --- a/docs/plugins/generated-creature-renamer.rst +++ b/docs/plugins/generated-creature-renamer.rst @@ -3,7 +3,7 @@ generated-creature-renamer .. dfhack-tool:: :summary: Automatically renames generated creatures. - :tags: untested adventure fort legends units + :tags: unavailable adventure fort legends units :no-command: .. dfhack-command:: list-generated diff --git a/docs/plugins/getplants.rst b/docs/plugins/getplants.rst index 756fa1aaa..05bac71c9 100644 --- a/docs/plugins/getplants.rst +++ b/docs/plugins/getplants.rst @@ -3,7 +3,7 @@ getplants .. dfhack-tool:: :summary: Designate trees for chopping and shrubs for gathering. - :tags: untested fort productivity plants + :tags: fort productivity plants Specify the types of trees to cut down and/or shrubs to gather by their plant names. diff --git a/docs/plugins/infiniteSky.rst b/docs/plugins/infiniteSky.rst index 7e8ec2f31..789709c9f 100644 --- a/docs/plugins/infiniteSky.rst +++ b/docs/plugins/infiniteSky.rst @@ -3,7 +3,7 @@ infiniteSky .. dfhack-tool:: :summary: Automatically allocate new z-levels of sky - :tags: untested fort auto design map + :tags: unavailable fort auto design map If enabled, this plugin will automatically allocate new z-levels of sky at the top of the map as you build up. Or it can allocate one or many additional levels diff --git a/docs/plugins/isoworldremote.rst b/docs/plugins/isoworldremote.rst index 2442f70ef..b792cb649 100644 --- a/docs/plugins/isoworldremote.rst +++ b/docs/plugins/isoworldremote.rst @@ -3,7 +3,7 @@ isoworldremote .. dfhack-tool:: :summary: Provides a remote API used by Isoworld. - :tags: untested dev graphics + :tags: unavailable dev graphics :no-command: See `remote` for related remote APIs. diff --git a/docs/plugins/jobutils.rst b/docs/plugins/jobutils.rst index f68c200b2..674c6897a 100644 --- a/docs/plugins/jobutils.rst +++ b/docs/plugins/jobutils.rst @@ -5,7 +5,7 @@ jobutils .. dfhack-tool:: :summary: Provides commands for interacting with jobs. - :tags: untested fort inspection jobs + :tags: unavailable fort inspection jobs :no-command: .. dfhack-command:: job diff --git a/docs/plugins/labormanager.rst b/docs/plugins/labormanager.rst index f808fcb59..731f706b8 100644 --- a/docs/plugins/labormanager.rst +++ b/docs/plugins/labormanager.rst @@ -3,7 +3,7 @@ labormanager .. dfhack-tool:: :summary: Automatically manage dwarf labors. - :tags: untested fort auto labors + :tags: unavailable fort auto labors Labormanager is derived from `autolabor` but uses a completely different approach to assigning jobs to dwarves. While autolabor tries to keep as many diff --git a/docs/plugins/lair.rst b/docs/plugins/lair.rst index 77ad0a045..82c5e211c 100644 --- a/docs/plugins/lair.rst +++ b/docs/plugins/lair.rst @@ -3,7 +3,7 @@ lair .. dfhack-tool:: :summary: Mark the map as a monster lair. - :tags: untested fort armok map + :tags: unavailable fort armok map This avoids item scatter when the fortress is abandoned. diff --git a/docs/plugins/luasocket.rst b/docs/plugins/luasocket.rst index 69f566e24..1aa320ed7 100644 --- a/docs/plugins/luasocket.rst +++ b/docs/plugins/luasocket.rst @@ -3,7 +3,7 @@ luasocket .. dfhack-tool:: :summary: Provides a Lua API for accessing network sockets. - :tags: untested dev + :tags: unavailable dev :no-command: See `luasocket-api` for details. diff --git a/docs/plugins/manipulator.rst b/docs/plugins/manipulator.rst index b4c0d6160..734800e77 100644 --- a/docs/plugins/manipulator.rst +++ b/docs/plugins/manipulator.rst @@ -3,7 +3,7 @@ manipulator .. dfhack-tool:: :summary: An in-game labor management interface. - :tags: untested fort productivity labors + :tags: unavailable fort productivity labors :no-command: It is equivalent to the popular Dwarf Therapist utility. diff --git a/docs/plugins/map-render.rst b/docs/plugins/map-render.rst index bbd01aa97..4f3c6ba72 100644 --- a/docs/plugins/map-render.rst +++ b/docs/plugins/map-render.rst @@ -3,7 +3,7 @@ map-render .. dfhack-tool:: :summary: Provides a Lua API for re-rendering portions of the map. - :tags: untested dev graphics + :tags: unavailable dev graphics :no-command: See `map-render-api` for details. diff --git a/docs/plugins/mode.rst b/docs/plugins/mode.rst index 42c7f6fcc..061e3a548 100644 --- a/docs/plugins/mode.rst +++ b/docs/plugins/mode.rst @@ -3,7 +3,7 @@ mode .. dfhack-tool:: :summary: See and change the game mode. - :tags: untested armok dev gameplay + :tags: unavailable armok dev gameplay .. warning:: diff --git a/docs/plugins/mousequery.rst b/docs/plugins/mousequery.rst index 0e49d3924..bff110f0e 100644 --- a/docs/plugins/mousequery.rst +++ b/docs/plugins/mousequery.rst @@ -3,7 +3,7 @@ mousequery .. dfhack-tool:: :summary: Adds mouse controls to the DF interface. - :tags: untested fort productivity interface + :tags: unavailable fort productivity interface Adds mouse controls to the DF interface. For example, with ``mousequery`` you can click on buildings to configure them, hold the mouse button to draw dig diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 6fe159899..08e05c6ca 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -48,8 +48,11 @@ is open via an `overlay` widget. There are hotkeys assigned to export, import, sort, and clear. You can also click on the hotkey hints as if they were buttons. Clearing will ask for confirmation before acting. -If you want to change where the hotkey hints appear, you can move them via -`gui/overlay`. +If you want to change where the overlay panel appears, you can move it via +`gui/overlay`. If you just need to get the overlay out of the way temporarily, +for example to read a long description of a historical figure when choosing a +subject for a statue, click on the small arrow in the upper right corner of the +overlay panel. Click on the arrow again to restore the panel. The orders library ------------------ diff --git a/docs/plugins/petcapRemover.rst b/docs/plugins/petcapRemover.rst index 798e39f32..4f6ea4160 100644 --- a/docs/plugins/petcapRemover.rst +++ b/docs/plugins/petcapRemover.rst @@ -3,7 +3,7 @@ petcapRemover .. dfhack-tool:: :summary: Modify the pet population cap. - :tags: untested fort auto animals + :tags: unavailable fort auto animals In vanilla DF, pets will not reproduce unless the population is below 50 and the number of children of that species is below a certain percentage. This plugin diff --git a/docs/plugins/plants.rst b/docs/plugins/plants.rst index cdcc0daac..281b295cf 100644 --- a/docs/plugins/plants.rst +++ b/docs/plugins/plants.rst @@ -5,7 +5,7 @@ plants .. dfhack-tool:: :summary: Provides commands that interact with plants. - :tags: untested adventure fort armok map plants + :tags: unavailable adventure fort armok map plants :no-command: .. dfhack-command:: plant diff --git a/docs/plugins/power-meter.rst b/docs/plugins/power-meter.rst index 8ffc79e3e..f3a76c60a 100644 --- a/docs/plugins/power-meter.rst +++ b/docs/plugins/power-meter.rst @@ -3,7 +3,7 @@ power-meter .. dfhack-tool:: :summary: Allow pressure plates to measure power. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings :no-command: If you run `gui/power-meter` while building a pressure plate, the pressure diff --git a/docs/plugins/prospector.rst b/docs/plugins/prospector.rst index 3fb47cc57..4628d11c8 100644 --- a/docs/plugins/prospector.rst +++ b/docs/plugins/prospector.rst @@ -5,7 +5,7 @@ prospector .. dfhack-tool:: :summary: Provides commands that help you analyze natural resources. - :tags: untested embark fort armok inspection map + :tags: embark fort armok inspection map :no-command: .. dfhack-command:: prospect diff --git a/docs/plugins/rename.rst b/docs/plugins/rename.rst index 3c6ed4a77..903a1a1d4 100644 --- a/docs/plugins/rename.rst +++ b/docs/plugins/rename.rst @@ -3,7 +3,7 @@ rename .. dfhack-tool:: :summary: Easily rename things. - :tags: untested adventure fort productivity buildings stockpiles units + :tags: unavailable adventure fort productivity buildings stockpiles units Use `gui/rename` for an in-game interface. diff --git a/docs/plugins/rendermax.rst b/docs/plugins/rendermax.rst index 370771d6b..91faa6f5c 100644 --- a/docs/plugins/rendermax.rst +++ b/docs/plugins/rendermax.rst @@ -3,7 +3,7 @@ rendermax .. dfhack-tool:: :summary: Modify the map lighting. - :tags: untested adventure fort gameplay graphics + :tags: unavailable adventure fort gameplay graphics This plugin provides a collection of OpenGL lighting filters that affect how the map is drawn to the screen. diff --git a/docs/plugins/search.rst b/docs/plugins/search.rst index 8e1a93143..c1493f992 100644 --- a/docs/plugins/search.rst +++ b/docs/plugins/search.rst @@ -5,7 +5,7 @@ search .. dfhack-tool:: :summary: Adds search capabilities to the UI. - :tags: untested fort productivity interface + :tags: unavailable fort productivity interface :no-command: Search options are added to the Stocks, Animals, Trading, Stockpile, Noble diff --git a/docs/plugins/siege-engine.rst b/docs/plugins/siege-engine.rst index fa18dcd48..57693fa11 100644 --- a/docs/plugins/siege-engine.rst +++ b/docs/plugins/siege-engine.rst @@ -3,7 +3,7 @@ siege-engine .. dfhack-tool:: :summary: Extend the functionality and usability of siege engines. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings :no-command: Siege engines in DF haven't been updated since the game was 2D, and can only aim diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 3a8c7c0f5..067e188fd 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -3,7 +3,7 @@ sort .. dfhack-tool:: :summary: Sort lists shown in the DF interface. - :tags: untested fort productivity interface + :tags: unavailable fort productivity interface :no-command: .. dfhack-command:: sort-items diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 448369cd8..95ce852ae 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -3,7 +3,7 @@ spectate .. dfhack-tool:: :summary: Automatically follow productive dwarves. - :tags: untested fort interface + :tags: unavailable fort interface Usage ----- diff --git a/docs/plugins/steam-engine.rst b/docs/plugins/steam-engine.rst index 11d0145f3..532b311d0 100644 --- a/docs/plugins/steam-engine.rst +++ b/docs/plugins/steam-engine.rst @@ -3,7 +3,7 @@ steam-engine .. dfhack-tool:: :summary: Allow modded steam engine buildings to function. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings :no-command: The steam-engine plugin detects custom workshops with the string diff --git a/docs/plugins/stockflow.rst b/docs/plugins/stockflow.rst index f501fc904..29b7838fc 100644 --- a/docs/plugins/stockflow.rst +++ b/docs/plugins/stockflow.rst @@ -3,7 +3,7 @@ stockflow .. dfhack-tool:: :summary: Queue manager jobs based on free space in stockpiles. - :tags: untested fort auto stockpiles workorders + :tags: unavailable fort auto stockpiles workorders With this plugin, the fortress bookkeeper can tally up free space in specific stockpiles and queue jobs through the manager to produce items to fill the free diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 01cd3159b..560ae2497 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -1,41 +1,477 @@ -.. _stocksettings: - stockpiles ========== .. dfhack-tool:: - :summary: Import and export stockpile settings. + :summary: Import, export, or modify stockpile settings and features. :tags: fort design productivity stockpiles - :no-command: - -.. dfhack-command:: savestock - :summary: Exports the configuration of the selected stockpile. -.. dfhack-command:: loadstock - :summary: Imports configuration for the selected stockpile. - -Select a stockpile in the UI first to use these commands. +If you are importing or exporting setting and don't want to specify a building +ID, select a stockpile in the UI before running the command. Usage ----- -``savestock `` - Saves the currently highlighted stockpile's settings to a file in your - Dwarf Fortress folder. This file can be used to copy settings between game - saves or players. -``loadstock `` - Loads a saved stockpile settings file and applies it to the currently - selected stockpile. +:: + + stockpiles [status] + stockpiles list [] + stockpiles export [] + stockpiles import [] -Filenames with spaces are not supported. Generated materials, divine metals, -etc. are not saved as they are different in every world. +Exported stockpile settings are saved in the ``dfhack-config/stockpiles`` +folder, where you can view and delete them, if desired. Names can only +contain numbers, letters, periods, and underscores. + +The names of library settings files are all prefixed by the string ``library/``. +You can specify library files explicitly by including the prefix, or you can +just write the short name to use a player-exported file by that name if it +exists, and the library file if it doesn't. Examples -------- -``savestock food`` - Export the stockpile settings for the currently selected stockpile to a - file named ``food.dfstock``. -``loadstock food`` - Set the selected stockpile settings to those saved in the ``food.dfstock`` - file. +``stockpiles`` + Shows the list of all your stockpiles and some relevant statistics. +``stockpiles list`` + Shows the list of previously exported stockpile settings files, including + the stockpile configuration library. +``stockpiles list plants`` + Shows the list of exported stockpile settings files that include the + substring ``plants``. +``stockpiles import library/plants`` + Imports the library ``plants`` settings file into the currently selected + stockpile. +``stockpiles import plants`` + Imports a player-exported settings file named ``plants``, or the library + ``plants`` settings file if a player-exported file by that name doesn't + exist. +``stockpiles import -m enable plants`` + Enables plants in the selected stockpile. +``stockpiles import -m disable cat_food -f tallow`` + Disables all tallow in the selected food stockpile. +``stockpiles export mysettings`` + Export the settings for the currently selected stockpile to a file named + ``dfhack-config/stockpiles/mysettings.dfstock``. +``stockpiles export mysettings -i categories,types`` + Export the stockpile category and item settings, but ignore the container + and general settings. This allows you to import the configuration later + without touching the container and general settings of the target + stockpile. + +Options +------- + +``-s``, ``--stockpile `` + Specify a specific stockpile ID instead of using the one currently selected + in the UI. +``-i``, ``--include `` + When exporting, you can include this option to select only specific elements + of the stockpile to record. If not specified, everything is included. When + the file is later imported, only the included settings will be modified. The + options are explained below in the next section. +``-m``, ``--mode (set|enable|disable)`` + When importing, choose the algorithm used to apply the settings. In ``set`` + mode (the default), the stockpile is cleared and the settings in the file + are enabled. In ``enable`` mode, enabled settings in the file are *added* + to the stockpile, but no other settings are changed. In ``disable`` mode, + enabled settings in the file are *removed* from the current stockpile + configuration, and nothing else is changed. +``-f``, ``--filter [,...]`` + When importing, only modify the settings that contain at least one of the + given substrings. + +Configuration elements +---------------------- + +The different configuration elements you can include in an exported settings +file are: + +:containers: Max bins, max barrels, and num wheelbarrows. +:general: Whether the stockpile takes from links only and whether organic + and/or inorganic materials are allowed. +:categories: The top-level categories of items that are enabled for the + stockpile, like Ammo, Finished goods, or Stone. +:types: The elements below the categories, which include the sub-categories, the + specific item types, and any toggles the category might have (like Prepared + meals for the Food category). + +.. _stockpiles-library: + +The stockpiles settings library +------------------------------- + +DFHack comes with a library of useful stockpile settings files that are ready +for import. If the stockpile configuration that you need isn't directly +represented, you can often use the ``enable`` and ``disable`` modes and/or +the ``filter`` option to transform an existing saved stockpile setting. Some +stockpile configurations can only be achieved with filters since the stockpile +lists are different for each world. For example, to disable all tallow in your +main food stockpile, you'd run this command:: + + stockpiles import cat_food -m disable -f tallow + +Top-level categories +~~~~~~~~~~~~~~~~~~~~ + +Each stockpile category has a file that allows you to enable or disable the +entire category, or with a filter, any matchable subset thereof:: + + cat_ammo + cat_animals + cat_armor + cat_bars_blocks + cat_cloth + cat_coins + cat_corpses + cat_finished_goods + cat_food + cat_furniture + cat_gems + cat_leather + cat_refuse + cat_sheets + cat_stone + cat_weapons + cat_wood + +For many of the categories, there are also flags and subcategory prefixes that +you can match with filters and convenient pre-made settings files that +manipulate interesting category subsets. + +Ammo stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + type/ + mats/ + other/ + core/ + total/ + +Settings files:: + + bolts + metalammo + boneammo + woodammo + +Example commands for a stockpile of metal bolts:: + + stockpiles import cat_ammo -f mats/,core/,total/ + stockpiles import -m enable bolts + +Animal stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Flags:: + + cages + traps + +Settings files:: + + cages + traps + +Example commands for a stockpile of empty cages:: + + stockpiles import cages + +Or, using the flag for the same effect:: + + stockpiles import cat_animals -f cages + +Armor stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Flags and subcategory prefixes:: + + nouse + canuse + body/ + head/ + feet/ + hands/ + legs/ + shield/ + mats/ + other/ + core/ + total/ + +Settings files:: + + metalarmor + otherarmor + ironarmor + bronzearmor + copperarmor + steelarmor + usablearmor + unusablearmor + +Example commands for a stockpile of sub-masterwork meltable armor:: + + stockpiles import cat_armor + stockpiles import -m disable -f other/,core/mas,core/art cat_armor + +Bar stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + mats/bars/ + other/bars/ + mats/blocks/ + other/blocks/ + +Settings files:: + + bars + metalbars + ironbars + pigironbars + steelbars + otherbars + coal + potash + ash + pearlash + soap + blocks + +Example commands for a stockpile of blocks:: + + stockpiles import blocks + +Cloth stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + thread/silk/ + thread/plant/ + thread/yarn/ + thread/metal/ + cloth/silk/ + cloth/plant/ + cloth/yarn/ + cloth/metal/ + +Settings files:: + + thread + adamantinethread + cloth + adamantinecloth + +Notes: + +* ``thread`` and ``cloth`` settings files set all materials that are not + adamantine. + +Finished goods stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + type/ + mats/ + other/ + core/ + total/ + +Settings files:: + + stonetools + woodtools + crafts + goblets + toys + +Example commands for a toy stockpile:: + + stockpiles import cat_furniture -f mats/,other/,core/,total/ + stockpiles import -m enable toys + +Food stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Flags and subcategory prefixes:: + + preparedmeals + meat/ + fish/prepared/ + fish/unprepared/ + egg/ + plants/ + drink/plant/ + drink/animal/ + cheese/plant/ + cheese/animal/ + seeds/ + leaves/ + powder/plant/ + powder/animal/ + glob/ + liquid/plant/ + liquid/animal/ + liquid/misc/ + paste/ + pressed/ + +Settings files:: + + preparedmeals + unpreparedfish + plants + booze + seeds + dye + miscliquid + wax + +Example commands for a kitchen ingredients stockpile:: + + stockpiles import cat_food -f meat/,fish/prepared/,egg/,cheese/,leaves/,powder/,glob/,liquid/plant/,paste/,pressed/ + stockpiles import cat_food -m enable -f milk,royal_jelly + stockpiles import dye -m disable + stockpiles import cat_food -m disable -f tallow,thread,liquid/misc/ + +Furniture stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + type/ + mats/ + other/ + core/ + total/ + +Settings files:: + + pots + bags + buckets + sand + +* Because of the limitations of Dwarf Fortress, ``bags`` cannot distinguish + between empty bags and bags filled with gypsum powder. + +Example commands for a sand bag stockpile:: + + stockpiles import cat_furniture + stockpiles import cat_furniture -m disable -f type/ + stockpiles import sand -m enable + +Gem stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + mats/rough/ + mats/cut/ + other/rough/ + other/cut/ + +Settings files:: + + roughgems + roughglass + cutgems + cutglass + cutstone + +Refuse stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Flags and subcategory prefixes:: + + rawhide/fresh + rawhide/rotten + type/ + corpses/ + bodyparts/ + skulls/ + bones/ + hair/ + shells/ + teeth/ + horns/ + +Settings files:: + + rawhides + tannedhides + usablehair + +Notes: + +* ``usablehair`` Only hair and wool that can make usable clothing is included, + i.e. from sheep, llamas, alpacas, and trolls. + +Example commands for a craftable refuse stockpile:: + + stockpiles import cat_refuse -f skulls/,bones/,shells',teeth/,horns/ + stockpiles import usablehair -m enable + +Sheet stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subcategory prefixes:: + + paper/ + parchment/ + +Stone stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Settings files:: + + metalore + ironore + economic + flux + plasterproducing + coalproducing + otherstone + bauxite + clay + +Weapon stockpile adjustments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Flags and subcategory prefixes:: + + nouse + canuse + type/weapon/ + type/trapcomp/ + mats/ + other/ + core/ + total/ + +Settings files:: + + metalweapons + stoneweapons + otherweapons + trapcomponents + ironweapons + silverweapons + bronzeweapons + copperweapons + steelweapons + platinumweapons + adamantineweapons + usableweapons + unusableweapons + +Example commands for a non-metallic trap components stockpile:: + + stockpiles import cat_weapons + stockpiles import cat_weapons -m disable -f type/weapon/ + stockpiles metalweapons -m disable diff --git a/docs/plugins/stocks.rst b/docs/plugins/stocks.rst index c924b748c..f8db4ee67 100644 --- a/docs/plugins/stocks.rst +++ b/docs/plugins/stocks.rst @@ -3,7 +3,7 @@ stocks .. dfhack-tool:: :summary: Enhanced fortress stock management interface. - :tags: untested fort productivity items + :tags: unavailable fort productivity items When the plugin is enabled, two new hotkeys become available: diff --git a/docs/plugins/title-folder.rst b/docs/plugins/title-folder.rst index 32818071b..ee4068547 100644 --- a/docs/plugins/title-folder.rst +++ b/docs/plugins/title-folder.rst @@ -3,7 +3,7 @@ title-folder .. dfhack-tool:: :summary: Displays the DF folder name in the window title bar. - :tags: untested interface + :tags: unavailable interface :no-command: Usage diff --git a/docs/plugins/title-version.rst b/docs/plugins/title-version.rst deleted file mode 100644 index 18c7c3ba8..000000000 --- a/docs/plugins/title-version.rst +++ /dev/null @@ -1,14 +0,0 @@ -title-version -============= - -.. dfhack-tool:: - :summary: Displays the DFHack version on DF's title screen. - :tags: untested interface - :no-command: - -Usage ------ - -:: - - enable title-version diff --git a/docs/plugins/trackstop.rst b/docs/plugins/trackstop.rst index 6ae23dcd0..041bcac2c 100644 --- a/docs/plugins/trackstop.rst +++ b/docs/plugins/trackstop.rst @@ -3,7 +3,7 @@ trackstop .. dfhack-tool:: :summary: Add dynamic configuration options for track stops. - :tags: untested fort gameplay buildings + :tags: unavailable fort gameplay buildings :no-command: When enabled, this plugin adds a :kbd:`q` menu for track stops, which is diff --git a/docs/plugins/tubefill.rst b/docs/plugins/tubefill.rst index eb568ef03..a8684c765 100644 --- a/docs/plugins/tubefill.rst +++ b/docs/plugins/tubefill.rst @@ -3,7 +3,7 @@ tubefill .. dfhack-tool:: :summary: Replenishes mined-out adamantine. - :tags: untested fort armok map + :tags: unavailable fort armok map Veins that were originally hollow will be left alone. diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index dc5ed19a8..f9467e333 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -3,7 +3,7 @@ tweak .. dfhack-tool:: :summary: A collection of tweaks and bugfixes. - :tags: untested adventure fort armok bugfix fps interface + :tags: unavailable adventure fort armok bugfix fps interface Usage ----- diff --git a/docs/plugins/workNow.rst b/docs/plugins/workNow.rst index 66bc822ae..cec39c667 100644 --- a/docs/plugins/workNow.rst +++ b/docs/plugins/workNow.rst @@ -3,7 +3,7 @@ workNow .. dfhack-tool:: :summary: Reduce the time that dwarves idle after completing a job. - :tags: untested fort auto labors + :tags: unavailable fort auto labors After finishing a job, dwarves will wander away for a while before picking up a new job. This plugin will automatically poke the game to assign dwarves to new diff --git a/docs/plugins/workflow.rst b/docs/plugins/workflow.rst index 72b7a8e2b..c95054c0e 100644 --- a/docs/plugins/workflow.rst +++ b/docs/plugins/workflow.rst @@ -3,7 +3,7 @@ workflow .. dfhack-tool:: :summary: Manage automated item production rules. - :tags: untested fort auto jobs + :tags: unavailable fort auto jobs Manage repeat jobs according to stock levels. `gui/workflow` provides a simple front-end integrated in the game UI. diff --git a/docs/plugins/zone.rst b/docs/plugins/zone.rst index 9d9991dde..af6ee2b5f 100644 --- a/docs/plugins/zone.rst +++ b/docs/plugins/zone.rst @@ -3,7 +3,7 @@ zone .. dfhack-tool:: :summary: Manage activity zones, cages, and the animals therein. - :tags: untested fort productivity animals buildings + :tags: unavailable fort productivity animals buildings Usage ----- diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 92db43563..31cfb007d 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -1,5 +1,5 @@ project(dfapi) -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.21) # prevent CMake warnings about INTERFACE_LINK_LIBRARIES vs LINK_INTERFACE_LIBRARIES cmake_policy(SET CMP0022 NEW) @@ -125,6 +125,7 @@ set(MODULE_HEADERS include/modules/Burrows.h include/modules/Constructions.h include/modules/DFSDL.h + include/modules/DFSteam.h include/modules/Designations.h include/modules/EventManager.h include/modules/Filesystem.h @@ -154,6 +155,7 @@ set(MODULE_SOURCES modules/Burrows.cpp modules/Constructions.cpp modules/DFSDL.cpp + modules/DFSteam.cpp modules/Designations.cpp modules/EventManager.cpp modules/Filesystem.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 01bc89480..a1e3b60e5 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -49,6 +49,7 @@ distribution. #include "PluginManager.h" #include "ModuleFactory.h" #include "modules/DFSDL.h" +#include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" @@ -272,7 +273,7 @@ static std::string dfhack_version_desc() if (Version::is_release()) s << "(release)"; else - s << "(development build " << Version::git_description() << ")"; + s << "(git: " << Version::git_commit(true) << ")"; s << " on " << (sizeof(void*) == 8 ? "x86_64" : "x86"); if (strlen(Version::dfhack_build_id())) s << " [build ID: " << Version::dfhack_build_id() << "]"; @@ -440,6 +441,12 @@ bool Core::addScriptPath(std::string path, bool search_before) return true; } +bool Core::setModScriptPaths(const std::vector &mod_script_paths) { + std::lock_guard lock(script_path_mutex); + script_paths[2] = mod_script_paths; + return true; +} + bool Core::removeScriptPath(std::string path) { std::lock_guard lock(script_path_mutex); @@ -464,20 +471,21 @@ void Core::getScriptPaths(std::vector *dest) std::lock_guard lock(script_path_mutex); dest->clear(); std::string df_path = this->p->getPath() + "/"; - for (auto it = script_paths[0].begin(); it != script_paths[0].end(); ++it) - dest->push_back(*it); + for (auto & path : script_paths[0]) + dest->emplace_back(path); dest->push_back(df_path + CONFIG_PATH + "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->push_back(df_path + "/save/" + save + "/scripts"); + dest->emplace_back(df_path + "save/" + save + "/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); + dest->emplace_back(df_path + "hack/scripts"); + for (auto & path : script_paths[2]) + dest->emplace_back(path); + for (auto & path : script_paths[1]) + dest->emplace_back(path); } - std::string Core::findScript(std::string name) { std::vector paths; @@ -526,6 +534,21 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) return true; } +static void loadModScriptPaths(color_ostream &out) { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + std::vector mod_script_paths; + Lua::CallLuaModuleFunction(out, L, "script-manager", "get_mod_script_paths", 0, 1, + Lua::DEFAULT_LUA_LAMBDA, + [&](lua_State *L) { + Lua::GetVector(L, mod_script_paths); + }); + DEBUG(script,out).print("final mod script paths:\n"); + for (auto & path : mod_script_paths) + DEBUG(script,out).print(" %s\n", path.c_str()); + Core::getInstance().setModScriptPaths(mod_script_paths); +} + static std::map state_change_event_map; static void sc_event_map_init() { if (!state_change_event_map.size()) @@ -763,6 +786,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } } + part = GetAliasCommand(part, true); + Plugin * plug = (*plug_mgr)[part]; if(!plug) @@ -1279,12 +1304,25 @@ static void run_dfhack_init(color_ostream &out, Core *core) return; } + // if we're running on Steam Deck, hide the terminal by default + if (DFSteam::DFIsSteamRunningOnSteamDeck()) + core->getConsole().hide(); + // load baseline defaults core->loadScriptFile(out, CONFIG_PATH + "init/default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); loadScriptFiles(core, out, prefixes, CONFIG_PATH + "init"); + + // if the option is set, hide the terminal + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + Lua::CallLuaModuleFunction(out, L, "dfhack", "getHideConsoleOnStartup", 0, 1, + Lua::DEFAULT_LUA_LAMBDA, [&](lua_State* L) { + if (lua_toboolean(L, -1)) + core->getConsole().hide(); + }, false); } // Load dfhack.init in a dedicated thread (non-interactive console mode) @@ -1644,6 +1682,8 @@ bool Core::Init() fatal("cannot bind SDL libraries"); return false; } + if (DFSteam::init(con)) + std::cerr << "Found Steam.\n"; std::cerr << "Initializing textures.\n"; Textures::init(con); // create mutex for syncing with interactive tasks @@ -2052,7 +2092,8 @@ void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event eve if (!df::global::world) return; - std::string rawFolder = "save/" + (df::global::world->cur_savegame.save_dir) + "/init"; + + std::string rawFolder = !isWorldLoaded() ? "" : "save/" + World::ReadWorldFolder() + "/init"; auto i = table.find(event); if ( i != table.end() ) { @@ -2111,14 +2152,25 @@ void Core::onStateChange(color_ostream &out, state_change_event event) switch (event) { case SC_CORE_INITIALIZED: - { - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - Lua::CallLuaModuleFunction(con, L, "helpdb", "refresh"); - Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); - } + { + loadModScriptPaths(out); + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + Lua::CallLuaModuleFunction(con, L, "helpdb", "refresh"); + Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); break; + } case SC_WORLD_LOADED: + { + loadModScriptPaths(out); + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + Lua::CallLuaModuleFunction(con, L, "script-manager", "reload", 1, 0, + [](lua_State* L) { + Lua::Push(L, true); + }); + // fallthrough + } case SC_WORLD_UNLOADED: case SC_MAP_LOADED: case SC_MAP_UNLOADED: @@ -2183,6 +2235,10 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (event == SC_WORLD_UNLOADED) { Persistence::Internal::clear(); + loadModScriptPaths(out); + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); } } @@ -2235,6 +2291,7 @@ int Core::Shutdown ( void ) allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); + DFSteam::cleanup(); memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); return -1; @@ -2657,13 +2714,14 @@ std::map> Core::ListAliases() return aliases; } -std::string Core::GetAliasCommand(const std::string &name, const std::string &default_) +std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) { std::lock_guard lock(alias_mutex); - if (IsAlias(name)) - return join_strings(" ", aliases[name]); - else - return default_; + if (!IsAlias(name) || aliases[name].empty()) + return name; + if (ignore_params) + return aliases[name][0]; + return join_strings(" ", aliases[name]); } ///////////////// diff --git a/library/DFHackVersion.cpp b/library/DFHackVersion.cpp index 7746ebece..42aac251e 100644 --- a/library/DFHackVersion.cpp +++ b/library/DFHackVersion.cpp @@ -1,6 +1,8 @@ #define NO_DFHACK_VERSION_MACROS #include "DFHackVersion.h" #include "git-describe.h" +#include + namespace DFHack { namespace Version { int dfhack_abi_version() @@ -27,9 +29,10 @@ namespace DFHack { { return DFHACK_GIT_DESCRIPTION; } - const char *git_commit() + const char* git_commit(bool short_hash) { - return DFHACK_GIT_COMMIT; + static std::string shorty(DFHACK_GIT_COMMIT, 0, 7); + return short_hash ? shorty.c_str() : DFHACK_GIT_COMMIT; } const char *git_xml_commit() { diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index c9bdc3021..08903c7bd 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1718,6 +1718,7 @@ static const LuaWrapper::FunctionReg dfhack_textures_module[] = { WRAPM(Textures, getControlPanelTexposStart), WRAPM(Textures, getThinBordersTexposStart), WRAPM(Textures, getMediumBordersTexposStart), + WRAPM(Textures, getBoldBordersTexposStart), WRAPM(Textures, getPanelBordersTexposStart), WRAPM(Textures, getWindowBordersTexposStart), { NULL, NULL } @@ -2728,13 +2729,9 @@ static int filesystem_listdir_recursive(lua_State *L) include_prefix = lua_toboolean(L, 3); std::map files; int err = DFHack::Filesystem::listdir_recursive(dir, files, depth, include_prefix); - if (err) - { + if (err != 0 && err != -1) { lua_pushnil(L); - if (err == -1) - lua_pushfstring(L, "max depth exceeded: %d", depth); - else - lua_pushstring(L, strerror(err)); + lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); return 3; } diff --git a/library/include/Core.h b/library/include/Core.h index 6c99e62be..2e022d6ca 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -158,6 +158,7 @@ namespace DFHack bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); bool addScriptPath(std::string path, bool search_before = false); + bool setModScriptPaths(const std::vector &mod_script_paths); bool removeScriptPath(std::string path); std::string findScript(std::string name); void getScriptPaths(std::vector *dest); @@ -173,7 +174,7 @@ namespace DFHack bool RunAlias(color_ostream &out, const std::string &name, const std::vector ¶meters, command_result &result); std::map> ListAliases(); - std::string GetAliasCommand(const std::string &name, const std::string &default_ = ""); + std::string GetAliasCommand(const std::string &name, bool ignore_params = false); std::string getHackPath(); @@ -239,7 +240,7 @@ namespace DFHack std::vector> allModules; DFHack::PluginManager * plug_mgr; - std::vector script_paths[2]; + std::vector script_paths[3]; std::mutex script_path_mutex; // hotkey-related stuff diff --git a/library/include/DFHackVersion.h b/library/include/DFHackVersion.h index 1b69dfe55..fbf2539bf 100644 --- a/library/include/DFHackVersion.h +++ b/library/include/DFHackVersion.h @@ -8,7 +8,7 @@ namespace DFHack { int dfhack_abi_version(); const char *git_description(); - const char *git_commit(); + const char* git_commit(bool short_hash = false); const char *git_xml_commit(); const char *git_xml_expected_commit(); bool git_xml_match(); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index b19fc31de..cab3ee9cc 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -342,26 +342,6 @@ namespace DFHack {namespace Lua { DFHACK_EXPORT void PushInterfaceKeys(lua_State *L, const std::set &keys); - template - void PushVector(lua_State *state, const T &pvec, bool addn = false) - { - lua_createtable(state,pvec.size(), addn?1:0); - - if (addn) - { - lua_pushinteger(state, pvec.size()); - lua_setfield(state, -2, "n"); - } - - for (size_t i = 0; i < pvec.size(); i++) - { - Push(state, pvec[i]); - lua_rawseti(state, -2, i+1); - } - } - - DFHACK_EXPORT void GetVector(lua_State *state, std::vector &pvec, int idx = 1); - DFHACK_EXPORT int PushPosXYZ(lua_State *state, const df::coord &pos); DFHACK_EXPORT int PushPosXY(lua_State *state, const df::coord2d &pos); @@ -412,6 +392,26 @@ namespace DFHack {namespace Lua { lua_settable(state, -3); } + template + void PushVector(lua_State *state, const T &pvec, bool addn = false) + { + lua_createtable(state,pvec.size(), addn?1:0); + + if (addn) + { + lua_pushinteger(state, pvec.size()); + lua_setfield(state, -2, "n"); + } + + for (size_t i = 0; i < pvec.size(); i++) + { + Push(state, pvec[i]); + lua_rawseti(state, -2, i+1); + } + } + + DFHACK_EXPORT void GetVector(lua_State *state, std::vector &pvec, int idx = 1); + DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true); DFHACK_EXPORT bool IsCoreContext(lua_State *state); diff --git a/library/include/modules/DFSteam.h b/library/include/modules/DFSteam.h new file mode 100644 index 000000000..3144830da --- /dev/null +++ b/library/include/modules/DFSteam.h @@ -0,0 +1,32 @@ +#pragma once + +#include "ColorText.h" +#include "Export.h" + +namespace DFHack +{ + +/** + * The DFSteam module - provides access to Steam functions without actually + * requiring build-time linkage to Steam + * \ingroup grp_modules + * \ingroup grp_dfsdl + */ +namespace DFSteam +{ + +/** + * Call this on DFHack init so we can load the function pointers. Returns false on + * failure. + */ +bool init(DFHack::color_ostream& out); + +/** + * Call this when DFHack is being unloaded. + */ +void cleanup(); + +DFHACK_EXPORT bool DFIsSteamRunningOnSteamDeck(); + +} +} diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index a438f01f4..95e628d5a 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -56,6 +56,7 @@ DFHACK_EXPORT long getControlPanelTexposStart(); */ DFHACK_EXPORT long getThinBordersTexposStart(); DFHACK_EXPORT long getMediumBordersTexposStart(); +DFHACK_EXPORT long getBoldBordersTexposStart(); DFHACK_EXPORT long getPanelBordersTexposStart(); DFHACK_EXPORT long getWindowBordersTexposStart(); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 2ee03dfed..78d978147 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -51,6 +51,18 @@ if dfhack.is_core_context then SC_UNPAUSED = 8 end +-- User-changeable options + +dfhack.HIDE_CONSOLE_ON_STARTUP = true +function dfhack.getHideConsoleOnStartup() + return dfhack.HIDE_CONSOLE_ON_STARTUP +end + +dfhack.HIDE_ARMOK_TOOLS = false +function dfhack.getHideArmokTools() + return dfhack.HIDE_ARMOK_TOOLS +end + -- Error handling safecall = dfhack.safecall @@ -702,7 +714,7 @@ local warned_scripts = {} function dfhack.run_script(name,...) if not warned_scripts[name] then local helpdb = require('helpdb') - if helpdb.is_entry(name) and helpdb.get_entry_tags(name).untested then + if helpdb.is_entry(name) and helpdb.get_entry_tags(name).unavailable 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.') @@ -770,7 +782,7 @@ function dfhack.run_script_with_env(envVars, name, flags, ...) elseif ((type(v.required) == 'boolean' and v.required) or (type(v.required) == 'function' and v.required(flags))) then if not script_flags[flag] then - local msg = v.error or 'Flag "' .. flag .. '" not recognized' + local msg = v.error or ('Flag "' .. flag .. '" not recognized') error(name .. ': ' .. msg) end end diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 7791f3685..b2c90d076 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -745,6 +745,7 @@ end local NO_LOGIC_SCREENS = { 'viewscreen_loadgamest', + 'viewscreen_adopt_regionst', 'viewscreen_export_regionst', 'viewscreen_choose_game_typest', 'viewscreen_worldst', @@ -866,8 +867,17 @@ function ZScreen:onGetSelectedPlant() return zscreen_get_any(self, 'Plant') end --------------------------- --- Framed screen object -- +-- convenience subclass for modal dialogs +ZScreenModal = defclass(ZScreenModal, ZScreen) +ZScreenModal.ATTRS{ + defocusable = false, + force_pause = true, + pass_pause = false, + pass_movement_keys = false, + pass_mouse_clicks = false, +} + +-- Framed screen object -------------------------- -- Plain grey-colored frame. @@ -916,8 +926,11 @@ end WINDOW_FRAME = make_frame('Window', true) PANEL_FRAME = make_frame('Panel', false) MEDIUM_FRAME = make_frame('Medium', false) +BOLD_FRAME = make_frame('Bold', true) INTERIOR_FRAME = make_frame('Thin', false) INTERIOR_FRAME.signature_pen = false +INTERIOR_MEDIUM_FRAME = copyall(MEDIUM_FRAME) +INTERIOR_MEDIUM_FRAME.signature_pen = false -- for compatibility with pre-steam code GREY_LINE_FRAME = WINDOW_FRAME diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 3b2470114..1b7858416 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -4,9 +4,6 @@ local _ENV = mkmodule('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') -local utils = require('utils') - -local dscreen = dfhack.screen MessageBox = defclass(MessageBox, gui.FramedScreen) @@ -66,6 +63,7 @@ function MessageBox:onInput(keys) elseif (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) and self.on_cancel then self.on_cancel() end + gui.markMouseClicksHandled(keys) return true end return self:inputToSubviews(keys) @@ -135,6 +133,7 @@ function InputBox:onInput(keys) if self.on_cancel then self.on_cancel() end + gui.markMouseClicksHandled(keys) return true end return self:inputToSubviews(keys) @@ -218,9 +217,9 @@ end function ListBox:onRenderFrame(dc,rect) ListBox.super.onRenderFrame(self,dc,rect) - --if self.select2_hint then - -- dc:seek(rect.x1+2,rect.y2):key('SEC_SELECT'):string(': '..self.select2_hint,COLOR_GREY) - --end + if self.select2_hint then + dc:seek(rect.x1+2,rect.y2):string('Shift-Click', COLOR_LIGHTGREEN):string(': '..self.select2_hint, COLOR_GREY) + end end function ListBox:getWantedFrameSize() @@ -236,6 +235,7 @@ function ListBox:onInput(keys) if self.on_cancel then self.on_cancel() end + gui.markMouseClicksHandled(keys) return true end return self:inputToSubviews(keys) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index b86e31710..c7cde698c 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1121,10 +1121,10 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) end end - if token.tile then + if token.tile or (hovered and token.htile) then x = x + 1 if dc then - local tile = getval(token.tile) + local tile = hovered and getval(token.htile or token.tile) or getval(token.tile) local tile_pen = tonumber(tile) and to_pen{tile=tile} or tile dc:char(nil, tile_pen) if token.width then @@ -1361,11 +1361,11 @@ function Label:onInput(keys) return true end if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then - self:on_click() + self.on_click() return true end if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then - self:on_rclick() + self.on_rclick() return true end for k,v in pairs(self.scroll_keys) do @@ -1488,6 +1488,8 @@ CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label) CycleHotkeyLabel.ATTRS{ key=DEFAULT_NIL, key_back=DEFAULT_NIL, + key_sep=': ', + val_gap=1, label=DEFAULT_NIL, label_width=DEFAULT_NIL, label_below=false, @@ -1499,17 +1501,16 @@ CycleHotkeyLabel.ATTRS{ function CycleHotkeyLabel:init() self:setOption(self.initial_option) - local val_gap = 1 if self.label_below then - val_gap = 0 + (self.key_back and 1 or 0) + (self.key and 3 or 0) + self.val_gap = 0 + (self.key_back and 1 or 0) + (self.key and 3 or 0) end self:setText{ self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, - {key=self.key, key_sep=': ', text=self.label, width=self.label_width, + {key=self.key, key_sep=self.key_sep, text=self.label, width=self.label_width, on_activate=self:callback('cycle')}, self.label_below and NEWLINE or '', - {gap=val_gap, text=self:callback('getOptionLabel'), + {gap=self.val_gap, text=self:callback('getOptionLabel'), pen=self:callback('getOptionPen')}, } end @@ -1560,17 +1561,17 @@ function CycleHotkeyLabel:setOption(value_or_index, call_on_change) end end -local function cyclehotkeylabel_getOptionElem(self, option_idx, key) +local function cyclehotkeylabel_getOptionElem(self, option_idx, key, require_key) option_idx = option_idx or self.option_idx local option = self.options[option_idx] if type(option) == 'table' then return option[key] end - return option + return not require_key and option or nil end function CycleHotkeyLabel:getOptionLabel(option_idx) - return cyclehotkeylabel_getOptionElem(self, option_idx, 'label') + return getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'label')) end function CycleHotkeyLabel:getOptionValue(option_idx) @@ -1578,7 +1579,7 @@ function CycleHotkeyLabel:getOptionValue(option_idx) end function CycleHotkeyLabel:getOptionPen(option_idx) - local pen = cyclehotkeylabel_getOptionElem(self, option_idx, 'pen') + local pen = getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'pen', true)) if type(pen) == 'string' then return nil end return pen end @@ -2293,4 +2294,141 @@ function TabBar:onInput(keys) end end +-------------------------------- +-- RangeSlider +-- + +RangeSlider = defclass(RangeSlider, Widget) +RangeSlider.ATTRS{ + num_stops=DEFAULT_NIL, + get_left_idx_fn=DEFAULT_NIL, + get_right_idx_fn=DEFAULT_NIL, + on_left_change=DEFAULT_NIL, + on_right_change=DEFAULT_NIL, +} + +function RangeSlider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function RangeSlider:init() + if self.num_stops < 2 then error('too few RangeSlider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function rangeslider_get_width_per_idx(self) + return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function RangeSlider:onInput(keys) + if not keys._MOUSE_L_DOWN then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = rangeslider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_left_change(self.get_left_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_right_change(self.get_right_idx_fn() + 1) + end + return true +end + +local function rangeslider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + if new_right_idx > self.num_stops then + return + end + end + end + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + self.on_right_change(new_right_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function RangeSlider:onRenderBody(dc, rect) + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = rangeslider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tabs + dc:seek(width_per_idx*(left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + rangeslider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + return _ENV diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index df5482aec..4ce078a22 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -12,6 +12,8 @@ local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' local SCRIPT_DOC_BEGIN = '[====[' local SCRIPT_DOC_END = ']====]' +local GLOBAL_KEY = 'HELPDB' + -- enums local ENTRY_TYPES = { BUILTIN='builtin', @@ -423,6 +425,14 @@ function refresh() ensure_db() end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc ~= SC_WORLD_LOADED then + return + end + -- pick up widgets from active mods + refresh() +end + local function parse_blocks(text) local blocks = {} for line in text:gmatch('[^\n]*') do @@ -778,7 +788,11 @@ function ls(filter_str, skip_tags, show_dev_commands, exclude_strs) table.insert(excludes, {str=argparse.stringList(exclude_strs)}) end if not show_dev_commands then - table.insert(excludes, {tag='dev'}) + local dev_tags = {'dev', 'unavailable'} + if dfhack.getHideArmokTools() then + table.insert(dev_tags, 'armok') + end + table.insert(excludes, {tag=dev_tags}) end list_entries(skip_tags, include, excludes) end @@ -803,7 +817,16 @@ function tags(tag) local skip_tags = true local include = {entry_type={ENTRY_TYPES.COMMAND}, tag=tag} - list_entries(skip_tags, include) + + local excludes = {tag={}} + if tag ~= 'unavailable' then + table.insert(excludes.tag, 'unavailable') + end + if tag ~= 'armok' and dfhack.getHideArmokTools() then + table.insert(excludes.tag, 'armok') + end + + list_entries(skip_tags, include, excludes) end return _ENV diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index cc5dd9fe3..450012357 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -2,23 +2,31 @@ local _ENV = mkmodule('script-manager') local utils = require('utils') +--------------------- +-- enabled API + -- for each script that can be loaded as a module, calls cb(script_name, env) -function foreach_module_script(cb) +function foreach_module_script(cb, preprocess_script_file_fn) for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do local files = dfhack.filesystem.listdir_recursive( script_path, nil, false) if not files then goto skip_path end for _,f in ipairs(files) do - if not f.isdir and - f.path:endswith('.lua') and - not f.path:startswith('test/') and - not f.path:startswith('internal/') then - local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua' - local ok, script_env = pcall(reqscript, script_name) - if ok then - cb(script_name, script_env) - end + if f.isdir or not f.path:endswith('.lua') or + f.path:startswith('.git') or + f.path:startswith('test/') or + f.path:startswith('internal/') then + goto continue + end + if preprocess_script_file_fn then + preprocess_script_file_fn(script_path, f.path) + end + local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua' + local ok, script_env = pcall(reqscript, script_name) + if ok then + cb(script_name, script_env) end + ::continue:: end ::skip_path:: end @@ -39,9 +47,17 @@ local function process_script(env_name, env) enabled_map[env_name] = fn end -function reload() +function reload(refresh_active_mod_scripts) enabled_map = utils.OrderedTable() - foreach_module_script(process_script) + local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) + if script_path:find('scripts_modactive') then + internal_script = dfhack.internal.scripts[script_path..'/'..script_name] + if internal_script then + internal_script.env = nil + end + end + end or nil + foreach_module_script(process_script, force_refresh_fn) end local function ensure_loaded() @@ -57,4 +73,107 @@ function list() end end +--------------------- +-- mod script paths + +-- this perhaps could/should be queried from the Steam API +-- are there any installation configurations where this will be wrong, though? +local WORKSHOP_MODS_PATH = '../../workshop/content/975370/' +local MODS_PATH = 'mods/' +local INSTALLED_MODS_PATH = 'data/installed_mods/' + +-- last instance of the same version of the same mod wins, so read them in this +-- order (in increasing order of liklihood that players may have made custom +-- changes to the files) +local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH} + +local function get_mod_id_and_version(path) + local idfile = path .. '/info.txt' + local ok, lines = pcall(io.lines, idfile) + if not ok then return end + local id, version + for line in lines do + if not id then + _,_,id = line:find('^%[ID:([^%]]+)%]') + end + if not version then + -- note this doesn't include the closing brace since some people put + -- non-number characters in here, and DF only reads the digits as the + -- numeric version + _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') + end + -- note that we do *not* want to break out of this loop early since + -- lines has to hit EOF to close the file + end + return id, version +end + +local function add_script_path(mod_script_paths, path) + if dfhack.filesystem.isdir(path) then + print('indexing mod scripts: ' .. path) + table.insert(mod_script_paths, path) + end +end + +local function add_script_paths(mod_script_paths, base_path, include_modactive) + if not base_path:endswith('/') then + base_path = base_path .. '/' + end + if include_modactive then + add_script_path(mod_script_paths, base_path..'scripts_modactive') + end + add_script_path(mod_script_paths, base_path..'scripts_modinstalled') +end + +function get_mod_script_paths() + -- ordered map of mod id -> {handled=bool, versions=map of version -> path} + local mods = utils.OrderedTable() + local mod_script_paths = {} + + -- if a world is loaded, process active mods first, and lock to active version + if dfhack.isWorldLoaded() then + for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do + path = tostring(path.value) + if not path:startswith(INSTALLED_MODS_PATH) then goto continue end + local id = get_mod_id_and_version(path) + if not id then goto continue end + mods[id] = {handled=true} + add_script_paths(mod_script_paths, path, true) + ::continue:: + end + end + + -- assemble version -> path maps for all (non-handled) mod source dirs + for _,mod_path_root in ipairs(MOD_PATH_ROOTS) do + local files = dfhack.filesystem.listdir_recursive(mod_path_root, 0) + if not files then goto skip_path_root end + for _,f in ipairs(files) do + if not f.isdir then goto continue end + local id, version = get_mod_id_and_version(f.path) + if not id or not version then goto continue end + local mod = ensure_key(mods, id) + if mod.handled then goto continue end + ensure_key(mod, 'versions')[version] = f.path + ::continue:: + end + ::skip_path_root:: + end + + -- add script paths from most recent version of all not-yet-handled mods + for _,v in pairs(mods) do + if v.handled then goto continue end + local max_version, path + for version,mod_path in pairs(v.versions) do + if not max_version or max_version < version then + path = mod_path + max_version = version + end + end + add_script_paths(mod_script_paths, path) + ::continue:: + end + + return mod_script_paths +end + return _ENV diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 2e03a8616..3883439f1 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -2,9 +2,9 @@ local _ENV = mkmodule('utils') local df = df -function getval(obj) +function getval(obj, ...) if type(obj) == 'function' then - return obj() + return obj(...) else return obj end diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 2b6fc8ec8..b9e61c863 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -68,6 +68,7 @@ using namespace DFHack; #include "df/building_stockpilest.h" #include "df/building_trapst.h" #include "df/building_water_wheelst.h" +#include "df/building_weaponst.h" #include "df/building_wellst.h" #include "df/building_workshopst.h" #include "df/buildings_other_id.h" @@ -591,6 +592,12 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in obj->gate_flags.bits.closed = false; break; } + case building_type::Weapon: + { + if (VIRTUAL_CAST_VAR(obj, df::building_weaponst, bld)) + obj->gate_flags.bits.closed = false; + break; + } default: break; } @@ -792,10 +799,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, if (!allow_occupied && block->occupancy[btile.x][btile.y].bits.building) allowed = false; - else + else if (!allow_wall) { - auto tile = block->tiletype[btile.x][btile.y]; - if (!allow_wall && !HighPassable(tile)) + auto &tt = block->tiletype[btile.x][btile.y]; + auto &des = block->designation[btile.x][btile.y]; + if (!HighPassable(tt) || + des.bits.flow_size > 1 || + (des.bits.flow_size >= 1 && des.bits.liquid_type == df::tile_liquid::Magma)) allowed = false; } @@ -1105,31 +1115,17 @@ static void createDesign(df::building *bld, bool rough) static int getMaxStockpileId() { - auto &vec = world->buildings.other[buildings_other_id::STOCKPILE]; int max_id = 0; - - for (size_t i = 0; i < vec.size(); i++) - { - auto bld = strict_virtual_cast(vec[i]); - if (bld) - max_id = std::max(max_id, bld->stockpile_number); - } - + for (auto bld : world->buildings.other.STOCKPILE) + max_id = std::max(max_id, bld->stockpile_number); return max_id; } static int getMaxCivzoneId() { - auto &vec = world->buildings.other[buildings_other_id::ANY_ZONE]; int max_id = 0; - - for (size_t i = 0; i < vec.size(); i++) - { - auto bld = strict_virtual_cast(vec[i]); - if (bld) - max_id = std::max(max_id, bld->zone_num); - } - + for (auto bld : world->buildings.other.ANY_ZONE) + max_id = std::max(max_id, bld->zone_num); return max_id; } @@ -1635,19 +1631,25 @@ StockpileIterator& StockpileIterator::operator++() { if (block) { // Check the next item in the current block. ++current; + } + else if (stockpile->x2 < 0 || stockpile->y2 < 0 || stockpile->z < 0 || stockpile->x1 > world->map.x_count - 1 || stockpile->y1 > world->map.y_count - 1 || stockpile->z > world->map.z_count - 1) { + // if the stockpile bounds exist outside of valid map plane then no items can be in the stockpile + block = NULL; + item = NULL; + return *this; } else { // Start with the top-left block covering the stockpile. - block = Maps::getTileBlock(stockpile->x1, stockpile->y1, stockpile->z); + block = Maps::getTileBlock(std::min(std::max(stockpile->x1, 0), world->map.x_count-1), std::min(std::max(stockpile->y1, 0), world->map.y_count-1), stockpile->z); current = 0; } while (current >= block->items.size()) { // Out of items in this block; find the next block to search. - if (block->map_pos.x + 16 <= stockpile->x2) { + if (block->map_pos.x + 16 <= std::min(stockpile->x2, world->map.x_count-1)) { block = Maps::getTileBlock(block->map_pos.x + 16, block->map_pos.y, stockpile->z); current = 0; - } else if (block->map_pos.y + 16 <= stockpile->y2) { - block = Maps::getTileBlock(stockpile->x1, block->map_pos.y + 16, stockpile->z); + } else if (block->map_pos.y + 16 <= std::min(stockpile->y2, world->map.y_count-1)) { + block = Maps::getTileBlock(std::max(stockpile->x1, 0), block->map_pos.y + 16, stockpile->z); current = 0; } else { // All items in all blocks have been checked. diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp new file mode 100644 index 000000000..b27cc1744 --- /dev/null +++ b/library/modules/DFSteam.cpp @@ -0,0 +1,81 @@ +#include "Internal.h" + +#include "modules/DFSteam.h" + +#include "Debug.h" +#include "PluginManager.h" + +namespace DFHack +{ +DBG_DECLARE(core, dfsteam, DebugCategory::LINFO); +} + +using namespace DFHack; + +static DFLibrary* g_steam_handle = nullptr; +static const std::vector STEAM_LIBS { + "steam_api64.dll", + "steam_api", // TODO: validate this on OSX + "libsteam_api.so" // TODO: validate this on Linux +}; + +bool (*g_SteamAPI_Init)() = nullptr; +void (*g_SteamAPI_Shutdown)() = nullptr; +void* (*g_SteamInternal_FindOrCreateUserInterface)(int, const char*) = nullptr; +bool (*g_SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck)(void*) = nullptr; + +bool DFSteam::init(color_ostream& out) { + for (auto& lib_str : STEAM_LIBS) { + if ((g_steam_handle = OpenPlugin(lib_str.c_str()))) + break; + } + if (!g_steam_handle) { + DEBUG(dfsteam, out).print("steam library not found; stubbing calls\n"); + return false; + } + +#define bind(handle, name) \ + g_##name = (decltype(g_##name))LookupPlugin(handle, #name); \ + if (!g_##name) { \ + WARN(dfsteam, out).print("steam library function not found: " #name "\n"); \ + } + + bind(g_steam_handle, SteamAPI_Init); + bind(g_steam_handle, SteamAPI_Shutdown); + + // TODO: can we remove this initialization of the Steam API once we move to dfhooks? + if (!g_SteamAPI_Init || !g_SteamAPI_Shutdown || !g_SteamAPI_Init()) { + DEBUG(dfsteam, out).print("steam detected but cannot be initialized\n"); + return false; + } + + bind(g_steam_handle, SteamInternal_FindOrCreateUserInterface); + bind(g_steam_handle, SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck); +#undef bind + + DEBUG(dfsteam, out).print("steam library linked\n"); + return true; +} + +void DFSteam::cleanup() { + if (!g_steam_handle) + return; + + if (g_SteamAPI_Shutdown) + g_SteamAPI_Shutdown(); + + ClosePlugin(g_steam_handle); + g_steam_handle = nullptr; +} + +bool DFSteam::DFIsSteamRunningOnSteamDeck() { + if (!g_SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck) + return false; + + if (!g_SteamInternal_FindOrCreateUserInterface) + return false; + + void* SteamUtils = g_SteamInternal_FindOrCreateUserInterface(0, "SteamUtils010"); + + return g_SteamAPI_ISteamUtils_IsSteamRunningOnSteamDeck(SteamUtils); +} diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 6ea0a5ff0..44642b5fa 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -84,6 +84,7 @@ using namespace DFHack; #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_titlest.h" #include "df/world.h" const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size @@ -144,6 +145,17 @@ static std::map getFocusStringsHandle ); \ static void getFocusStrings_##screen_type(std::string &baseFocus, std::vector &focusStrings, VIEWSCREEN(screen_type) *screen) +DEFINE_GET_FOCUS_STRING_HANDLER(title) +{ + if (screen->managing_mods) + focusStrings.push_back(baseFocus + "/Mods"); + else if (game->main_interface.settings.open) + focusStrings.push_back(baseFocus + "/Settings"); + + if (focusStrings.empty()) + focusStrings.push_back(baseFocus + "/Default"); +} + DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) { std::string newFocusString; diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index bfdd525be..03d6cda4a 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -720,7 +720,14 @@ df::coord Items::getPosition(df::item *item) return item->pos; } -static char quality_table[] = { 0, '-', '+', '*', '=', '@' }; +static const char quality_table[] = { + '\0', // (base) + '-', // well-crafted + '+', // finely-crafted + '*', // superior quality + '\xF0', // (≡) exceptional + '\x0F' // (☼) masterful +}; static void addQuality(std::string &tmp, int quality) { @@ -825,7 +832,7 @@ std::string Items::getDescription(df::item *item, int type, bool decorate) addQuality(tmp, item->getQuality()); if (item->isImproved()) { - tmp = "<" + tmp + ">"; + tmp = '\xAE' + tmp + '\xAF'; // («) + tmp + (») addQuality(tmp, item->getImprovementQuality()); } } diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index b88a078b1..c329220c9 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -60,6 +60,7 @@ using namespace std; #include "df/flow_info.h" #include "df/job.h" #include "df/plant.h" +#include "df/plant_root_tile.h" #include "df/plant_tree_info.h" #include "df/plant_tree_tile.h" #include "df/region_map_entry.h" @@ -805,14 +806,16 @@ void MapExtras::BlockInfo::prepare(Block *mblock) // If the block is at or above the plant's base level, we use the body array // otherwise we use the roots. // TODO: verify that the tree bounds intersect the block. - df::plant_tree_tile tile; + bool has_tree_tile = false; int z_diff = block->map_pos.z - pp->pos.z; - if (z_diff >= 0) - tile = info->body[z_diff][xx + (yy*info->dim_x)]; - else - tile = info->roots[-1 - z_diff][xx + (yy*info->dim_x)]; - if (tile.whole && !(tile.bits.blocked)) - { + if (z_diff >= 0) { + df::plant_tree_tile tile = info->body[z_diff][xx + (yy * info->dim_x)]; + has_tree_tile = tile.whole && !(tile.bits.blocked); + } else { + df::plant_root_tile tile = info->roots[-1 - z_diff][xx + (yy * info->dim_x)]; + has_tree_tile = tile.whole && !(tile.bits.blocked); + } + if (has_tree_tile) { df::coord pos = pp->pos; pos.x = pos.x - (info->dim_x / 2) + xx; pos.y = pos.y - (info->dim_y / 2) + yy; diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index ddcd6078f..2e831075d 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -57,6 +57,7 @@ using namespace std; #include "df/flow_info.h" #include "df/map_block_column.h" #include "df/plant.h" +#include "df/plant_root_tile.h" #include "df/plant_tree_info.h" #include "df/plant_tree_tile.h" #include "df/region_map_entry.h" diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index d73b59922..7a1ef249f 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -439,6 +439,7 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat) const return true; if (cat.bits.milk && linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0) return true; + TEST(gem, IS_GEM); return false; } diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 25d44695a..a30f879a0 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -26,6 +26,7 @@ static long g_on_off_texpos_start = -1; static long g_control_panel_texpos_start = -1; static long g_thin_borders_texpos_start = -1; static long g_medium_borders_texpos_start = -1; +static long g_bold_borders_texpos_start = -1; static long g_panel_borders_texpos_start = -1; static long g_window_borders_texpos_start = -1; @@ -134,6 +135,8 @@ void Textures::init(color_ostream &out) { &g_thin_borders_texpos_start); g_num_dfhack_textures += load_textures(out, "hack/data/art/border-medium.png", &g_medium_borders_texpos_start); + g_num_dfhack_textures += load_textures(out, "hack/data/art/border-bold.png", + &g_bold_borders_texpos_start); g_num_dfhack_textures += load_textures(out, "hack/data/art/border-panel.png", &g_panel_borders_texpos_start); g_num_dfhack_textures += load_textures(out, "hack/data/art/border-window.png", @@ -202,6 +205,10 @@ long Textures::getMediumBordersTexposStart() { return g_medium_borders_texpos_start; } +long Textures::getBoldBordersTexposStart() { + return g_bold_borders_texpos_start; +} + long Textures::getPanelBordersTexposStart() { return g_panel_borders_texpos_start; } diff --git a/library/xml b/library/xml index dbba09514..ae268954d 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit dbba095144e631b111d793aae0af2601fffe8e96 +Subproject commit ae268954da3cde1c4e08f6d62273a17aae0d94da diff --git a/package/windows/CMakeLists.txt b/package/windows/CMakeLists.txt index a5877f117..d92d687c4 100644 --- a/package/windows/CMakeLists.txt +++ b/package/windows/CMakeLists.txt @@ -1,7 +1,38 @@ project(package_windows) if(WIN32) - add_executable(launchdf WIN32 launchdf.c) - install(TARGETS launchdf - DESTINATION ${DFHACK_DATA_DESTINATION}) + if (BUILD_DFLAUNCH) + if ((DEFINED ENV{steam_username}) AND (DEFINED ENV{steam_password})) + # download Steam SDK + set (STEAMAPI_DIR ${dfhack_SOURCE_DIR}/depends/steam) + file(DOWNLOAD "https://partner.steamgames.com/downloads/steamworks_sdk_156.zip" + ${STEAMAPI_DIR}/steamworks_sdk_156.zip + EXPECTED_HASH MD5=af5a579990dbe5ae4c1b0689260d001b + USERPWD $ENV{steam_username}:$ENV{steam_password} + STATUS STEAM_SDK_DOWNLOAD_STATUS + SHOW_PROGRESS + ) + list(GET STEAM_SDK_DOWNLOAD_STATUS 0 STEAM_SDK_DL_STATUS_CODE) + list(GET STEAM_SDK_DOWNLOAD_STATUS 1 STEAM_SDK_DL_ERROR_MESSAGE) + if (NOT (${STEAM_SDK_DL_STATUS_CODE} EQUAL 0)) + message(FATAL_ERROR "Steam SDK download: " ${STEAM_SDK_DL_ERROR_MESSAGE}) + else () + message(STATUS "Steam SDK download: " ${STEAM_SDK_DL_ERROR_MESSAGE}) + file(ARCHIVE_EXTRACT + INPUT ${STEAMAPI_DIR}/steamworks_sdk_156.zip + DESTINATION ${STEAMAPI_DIR}) + set(STEAMAPI_LIBRARY "${STEAMAPI_DIR}/sdk/redistributable_bin/win64/steam_api64.lib") + set(STEAMAPI_SOURCE_DIR "${STEAMAPI_DIR}/sdk/public/steam") + set(STEAMAPI_SHARED_LIBRARY "${STEAMAPI_DIR}/sdk/redistributable_bin/win64/steam_api64.dll") + endif() + else() + message(SEND_ERROR "Need to set steam_username and steam_password in environment to download Steamworks SDK") + endif() + + include_directories(${STEAMAPI_SOURCE_DIR}) + link_libraries(${STEAMAPI_LIBRARY}) + add_executable(launchdf WIN32 launchdf.cpp) + install(TARGETS launchdf DESTINATION ${DFHACK_DATA_DESTINATION}) + install(FILES ${STEAMAPI_SHARED_LIBRARY} DESTINATION ${DFHACK_DATA_DESTINATION}) + endif() endif() diff --git a/package/windows/launchdf.c b/package/windows/launchdf.c deleted file mode 100644 index 9f4b01fcb..000000000 --- a/package/windows/launchdf.c +++ /dev/null @@ -1,28 +0,0 @@ -#include - -int WINAPI wWinMain(HINSTANCE hi, HINSTANCE hpi, PWSTR cmd, int ns) -{ - STARTUPINFOA si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - if (CreateProcessA("Dwarf Fortress.exe", - NULL, - NULL, - NULL, - FALSE, - 0, - NULL, - NULL, - &si, - &pi) == 0) - { - MessageBoxA(NULL, "could not launch 'Dwarf Fortress.exe'", NULL, 0); - exit(1); - } - - exit(0); -} diff --git a/package/windows/launchdf.cpp b/package/windows/launchdf.cpp new file mode 100644 index 000000000..a84465f53 --- /dev/null +++ b/package/windows/launchdf.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include "steam_api.h" + +#include + +const uint32 DFHACK_STEAM_APPID = 2346660; +const uint32 DF_STEAM_APPID = 975370; + +static BOOL is_running_on_wine() { + typedef const char* (CDECL wine_get_version)(void); + static wine_get_version* pwine_get_version; + HMODULE hntdll = GetModuleHandle("ntdll.dll"); + if(!hntdll) + return FALSE; + + pwine_get_version = (wine_get_version*) GetProcAddress(hntdll, "wine_get_version"); + return !!pwine_get_version; +} + +static LPCWSTR launch_via_steam_posix() { + const char* argv[] = { "/bin/sh", "-c", "\"steam -applaunch 975370\"", NULL }; + + // does not return on success + _execv(argv[0], argv); + + return L"Could not launch Dwarf Fortress"; +} + +static LPCWSTR launch_via_steam_windows() { + STARTUPINFOW si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + WCHAR steamPath[1024] = L""; + DWORD datasize = 1024; + + LONG retCode = RegGetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Valve\\Steam", + L"SteamExe", RRF_RT_REG_SZ, NULL, &steamPath, &datasize); + + if (retCode != ERROR_SUCCESS) + return L"Could not find Steam client executable"; + + WCHAR commandLine[1024] = L"steam.exe -applaunch 975370"; + + BOOL res = CreateProcessW(steamPath, commandLine, + NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + + if (res) + { + WaitForSingleObject(pi.hProcess, INFINITE); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return NULL; + } + else + { + return L"Could not launch Dwarf Fortress"; + } +} + +static LPCWSTR launch_direct() { + STARTUPINFOW si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + BOOL res = CreateProcessW(L"Dwarf Fortress.exe", + NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); + + if (res) + { + WaitForSingleObject(pi.hProcess, INFINITE); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return NULL; + } + + return L"Could not launch via non-steam fallback method"; +} + +DWORD findDwarfFortressProcess() +{ + PROCESSENTRY32W entry; + entry.dwSize = sizeof(PROCESSENTRY32W); + + const auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); + + if (!Process32FirstW(snapshot, &entry)) + { + CloseHandle(snapshot); + return -1; + } + + do { + std::wstring executableName(entry.szExeFile); + if (executableName == L"Dwarf Fortress.exe") + { + CloseHandle(snapshot); + return entry.th32ProcessID; + } + } while (Process32NextW(snapshot, &entry)); + + CloseHandle(snapshot); + return -1; +} + +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { + + // initialize steam context + if (SteamAPI_RestartAppIfNecessary(DFHACK_STEAM_APPID)) + { + exit(0); + } + + if (!SteamAPI_Init()) + { + // could not initialize steam context, attempt fallback launch + LPCWSTR err = launch_direct(); + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + exit(0); + } + + bool wine = is_running_on_wine(); + + if (wine) + { + // attempt launch via steam client + LPCWSTR err = launch_via_steam_posix(); + + if (err != NULL) + // steam client launch failed, attempt fallback launch + err = launch_direct(); + + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + exit(0); + } + + // steam detected and not running in wine + + bool df_installed = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); + + if (!df_installed) + { + // Steam DF is not installed. Assume DF is installed in same directory as DFHack and do a fallback launch + LPCWSTR err = launch_direct(); + + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + exit(0); + } + + // obtain DF app path + + char buf[2048] = ""; + + int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); + std::string dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + + int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); + std::string df_install_folder = (b2 != -1) ? std::string(buf) : ""; + + + if (df_install_folder != dfhack_install_folder) + { + // DF and DFHack are not installed in the same library + MessageBoxW(NULL, L"DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting.", NULL, 0); + exit(1); + } + + DWORD df_pid = findDwarfFortressProcess(); + + if (df_pid == -1) + { + LPCWSTR err = launch_via_steam_windows(); + if (err != NULL) + { + MessageBoxW(NULL, err, NULL, 0); + exit(1); + } + int counter = 0; + + do { + if (counter++ > 60) + { + MessageBoxW(NULL, L"Dwarf Fortress took too long to launch, aborting", NULL, 0); + exit(1); + } + Sleep(1000); + df_pid = findDwarfFortressProcess(); + } while (df_pid == -1); + } + + HANDLE hDF = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, df_pid); + + // in the future open an IPC connection so that we can proxy SteamAPI calls for the DFSteam module + + // this will eventuallyh need to become a loop with a WaitForMultipleObjects call + WaitForSingleObject(hDF, INFINITE); + + CloseHandle(hDF); + + exit(0); +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 943bc311c..67faaec41 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -113,6 +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(faststart faststart.cpp) dfhack_plugin(filltraffic filltraffic.cpp) #dfhack_plugin(fix-unit-occupancy fix-unit-occupancy.cpp) #dfhack_plugin(fixveins fixveins.cpp) @@ -120,7 +121,7 @@ dfhack_plugin(filltraffic filltraffic.cpp) #dfhack_plugin(follow follow.cpp) #dfhack_plugin(forceequip forceequip.cpp) #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) -#dfhack_plugin(getplants getplants.cpp) +dfhack_plugin(getplants getplants.cpp) dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) #dfhack_plugin(infiniteSky infiniteSky.cpp) #dfhack_plugin(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) @@ -141,7 +142,7 @@ dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) #dfhack_plugin(petcapRemover petcapRemover.cpp) #dfhack_plugin(plants plants.cpp) dfhack_plugin(probe probe.cpp) -#dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) +dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) dfhack_plugin(regrass regrass.cpp) add_subdirectory(remotefortressreader) @@ -162,7 +163,6 @@ dfhack_plugin(strangemood strangemood.cpp) dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua) dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(title-folder title-folder.cpp) -#dfhack_plugin(title-version title-version.cpp) #dfhack_plugin(trackstop trackstop.cpp) #dfhack_plugin(tubefill tubefill.cpp) #add_subdirectory(tweak) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index db7582c09..a0cea765f 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -6,11 +6,6 @@ if(UNIX) endif() endif() -include_directories("${dfhack_SOURCE_DIR}/library/include") -include_directories("${dfhack_SOURCE_DIR}/library/proto") -include_directories("${CMAKE_CURRENT_SOURCE_DIR}/proto") -include_directories("${dfhack_SOURCE_DIR}/library/depends/xgetopt") - macro(car var) set(${var} ${ARGV1}) endmacro() @@ -123,6 +118,11 @@ macro(dfhack_plugin) add_library(${PLUGIN_NAME} MODULE ${PLUGIN_SOURCES}) ide_folder(${PLUGIN_NAME} "Plugins") + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/include") + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/proto") + target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/depends/xgetopt") + if(NUM_PROTO) add_dependencies(${PLUGIN_NAME} generate_proto_${PLUGIN_NAME}) target_link_libraries(${PLUGIN_NAME} dfhack protobuf-lite dfhack-version ${PLUGIN_LINK_LIBRARIES}) diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index fc8b1e43b..d404960b4 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -61,7 +61,25 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +enum ConfigValues { + CONFIG_IS_ENABLED = 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); +} // Here go all the command declarations... // mostly to allow having the mandatory stuff on top of the file and commands on the bottom @@ -250,6 +268,29 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } +DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != autoclothing_enabled) { + auto enabled = World::GetPersistentData("autoclothing/enabled"); + autoclothing_enabled = enable; + DEBUG(report, out).print("%s from the API; persisting\n", + autoclothing_enabled ? "enabled" : "disabled"); + set_config_bool(enabled, CONFIG_IS_ENABLED, autoclothing_enabled); + if (enable) + do_autoclothing(); + } + else { + DEBUG(report, out).print("%s from the API, but already %s; no action\n", + autoclothing_enabled ? "enabled" : "disabled", + autoclothing_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + // Whatever you put here will be done in each game step. Don't abuse it. // It's optional, so you can just comment it out like this if you don't need it. @@ -389,6 +430,7 @@ command_result autoclothing(color_ostream &out, vector & parameters) if (parameters.size() == 0) { CoreSuspender suspend; + out << "Automatic clothing management is currently " << (autoclothing_enabled ? "enabled" : "disabled") << "." << endl; out << "Currently set " << clothingOrders.size() << " automatic clothing orders" << endl; for (size_t i = 0; i < clothingOrders.size(); i++) { @@ -523,7 +565,7 @@ static void find_needed_clothing_items() if (!item) { - WARN(cycle).print("Invalid inventory item ID: %d\n", ownedItem); + DEBUG(cycle).print("autoclothing: Invalid inventory item ID: %d\n", ownedItem); continue; } @@ -728,34 +770,28 @@ static void list_unit_counts(color_ostream& out, map& unitList) static bool isAvailableItem(df::item* item) { - if (item->flags.bits.in_job) - return false; - if (item->flags.bits.hostile) - return false; - if (item->flags.bits.in_building) - return false; - if (item->flags.bits.in_building) - return false; - if (item->flags.bits.encased) - return false; - if (item->flags.bits.foreign) - return false; - if (item->flags.bits.trader) - return false; - if (item->flags.bits.owned) - return false; - if (item->flags.bits.artifact) - return false; - if (item->flags.bits.forbid) - return false; - if (item->flags.bits.dump) - return false; - if (item->flags.bits.on_fire) - return false; - if (item->flags.bits.melt) - return false; - if (item->flags.bits.hidden) + static struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; +#define F(x) flags.bits.x = true; + F(in_job); F(hostile); F(in_building); F(encased); + F(foreign); F(trader); F(owned); F(forbid); + F(dump); F(on_fire); F(melt); F(hidden); + + F(garbage_collect); F(rotten); F(construction); + F(in_chest); F(removed); F(spider_web); + + // F(artifact); -- TODO: should this be included? +#undef F + whole = flags.whole; + } + } badFlags; + + if ((item->flags.whole & badFlags.whole) != 0) return false; + if (item->getWear() > 1) return false; if (!item->isClothing()) @@ -782,7 +818,7 @@ static void generate_report(color_ostream& out) auto item = Items::findItemByID(itemId); if (!item) { - WARN(cycle,out).print("Invalid inventory item ID: %d\n", itemId); + DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: %d\n", itemId); continue; } if (item->getWear() >= 1) @@ -915,3 +951,23 @@ static void generate_report(color_ostream& out) } } + +///////////////////////////////////////////////////// +// Lua API +// TODO: implement Lua hooks to manipulate the persistent order configuration +// + +static void autoclothing_doCycle(color_ostream& out) { + DEBUG(report, out).print("entering autoclothing_doCycle\n"); + do_autoclothing(); +} + + +DFHACK_PLUGIN_LUA_FUNCTIONS{ + DFHACK_LUA_FUNCTION(autoclothing_doCycle), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS{ + DFHACK_LUA_END +}; diff --git a/plugins/automelt.cpp b/plugins/automelt.cpp index 0cd9e8131..dc0bc0f79 100644 --- a/plugins/automelt.cpp +++ b/plugins/automelt.cpp @@ -172,20 +172,6 @@ DFhackCExport command_result plugin_load_data(color_ostream &out) return CR_OK; } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - if (event == DFHack::SC_WORLD_UNLOADED) - { - if (is_enabled) - { - DEBUG(status, out).print("world unloaded; disabling %s\n", plugin_name); - is_enabled = false; - } - } - - return CR_OK; -} - DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!Core::getInstance().isWorldLoaded()) @@ -293,7 +279,7 @@ static inline bool can_melt(df::item *item) df::item_type t = item->getType(); - if (t == df::enums::item_type::BOX || t == df::enums::item_type::BAR) + if (t == df::enums::item_type::BAR) return false; for (auto &g : item->general_refs) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index b09c4d497..39847935d 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -229,6 +229,9 @@ static const char * get_tile_dig(const df::coord &pos, const tile_context &) { case tiletype_shape::BOULDER: case tiletype_shape::PEBBLES: case tiletype_shape::BROOK_TOP: + case tiletype_shape::SAPLING: + case tiletype_shape::SHRUB: + case tiletype_shape::TWIG: return "d"; case tiletype_shape::STAIR_UP: return "u"; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index b472c7390..ce424f9c6 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -9,8 +9,10 @@ #include "modules/World.h" +#include "df/construction_type.h" #include "df/item.h" #include "df/job_item.h" +#include "df/organic_mat_category.h" #include "df/world.h" using std::map; @@ -71,7 +73,7 @@ static Tasks tasks; void PlannedBuilding::remove(color_ostream &out) { DEBUG(status,out).print("removing persistent data for building %d\n", id); World::DeletePersistentData(bld_config); - if (planned_buildings.count(id) > 0) + if (planned_buildings.count(id)) planned_buildings.erase(id); } @@ -130,7 +132,7 @@ static const vector & get_job_items(color_ostream &out, Bu }, [&](lua_State *L) { df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: 0x%p\n", std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); if (!jitem) failed = true; @@ -148,7 +150,11 @@ static const df::dfhack_material_category stone_cat(df::dfhack_material_category static const df::dfhack_material_category wood_cat(df::dfhack_material_category::mask_wood); static const df::dfhack_material_category metal_cat(df::dfhack_material_category::mask_metal); static const df::dfhack_material_category glass_cat(df::dfhack_material_category::mask_glass); +static const df::dfhack_material_category gem_cat(df::dfhack_material_category::mask_gem); static const df::dfhack_material_category clay_cat(df::dfhack_material_category::mask_clay); +static const df::dfhack_material_category cloth_cat(df::dfhack_material_category::mask_cloth); +static const df::dfhack_material_category silk_cat(df::dfhack_material_category::mask_silk); +static const df::dfhack_material_category yarn_cat(df::dfhack_material_category::mask_yarn); static void cache_matched(int16_t type, int32_t index) { MaterialInfo mi; @@ -165,14 +171,35 @@ static void cache_matched(int16_t type, int32_t index) { } else if (mi.matches(glass_cat)) { DEBUG(status).print("cached glass material: %s (%d, %d)\n", mi.toString().c_str(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "glass")); + } else if (mi.matches(gem_cat)) { + DEBUG(status).print("cached gem material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + mat_cache.emplace(mi.toString(), std::make_pair(mi, "gem")); } else if (mi.matches(clay_cat)) { DEBUG(status).print("cached clay material: %s (%d, %d)\n", mi.toString().c_str(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "clay")); + } else if (mi.matches(cloth_cat)) { + DEBUG(status).print("cached cloth material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + mat_cache.emplace(mi.toString(), std::make_pair(mi, "cloth")); + } else if (mi.matches(silk_cat)) { + DEBUG(status).print("cached silk material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + mat_cache.emplace(mi.toString(), std::make_pair(mi, "silk")); + } else if (mi.matches(yarn_cat)) { + DEBUG(status).print("cached yarn material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + mat_cache.emplace(mi.toString(), std::make_pair(mi, "yarn")); } else TRACE(status).print("not matched: %s\n", mi.toString().c_str()); } +static void load_organic_material_cache(df::organic_mat_category cat) { + auto& mat_tab = world->raws.mat_table; + auto& cat_vec = mat_tab.organic_types[cat]; + auto& cat_indices = mat_tab.organic_indexes[cat]; + for (size_t i = 0; i < cat_vec.size(); i++) { + cache_matched(cat_vec[i], cat_indices[i]); + } +} + static void load_material_cache() { df::world_raws &raws = world->raws; for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) @@ -182,17 +209,10 @@ static void load_material_cache() { for (size_t i = 0; i < raws.inorganics.size(); i++) cache_matched(0, i); - for (size_t i = 0; i < raws.plants.all.size(); i++) { - df::plant_raw *p = raws.plants.all[i]; - if (p->material.size() <= 1) - continue; - for (size_t j = 0; j < p->material.size(); j++) { - if (p->material[j]->id == "WOOD") { - cache_matched(DFHack::MaterialInfo::PLANT_BASE+j, i); - break; - } - } - } + load_organic_material_cache(df::organic_mat_category::Wood); + load_organic_material_cache(df::organic_mat_category::PlantFiber); + load_organic_material_cache(df::organic_mat_category::Silk); + load_organic_material_cache(df::organic_mat_category::Yarn); } static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { @@ -211,9 +231,9 @@ static DefaultItemFilters & get_item_filters(color_ostream &out, const BuildingT static command_result do_command(color_ostream &out, vector ¶meters); void buildingplan_cycle(color_ostream &out, Tasks &tasks, - unordered_map &planned_buildings); + unordered_map &planned_buildings, bool unsuspend_on_finalize); -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bool unsuspend_on_finalize); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(status,out).print("initializing %s\n", plugin_name); @@ -282,6 +302,28 @@ static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "reload_pens"); } +static int16_t get_subtype(df::building *bld) { + if (!bld) + return -1; + + int16_t subtype = bld->getSubtype(); + if (bld->getType() == df::building_type::Construction && + subtype >= df::construction_type::UpStair && + subtype <= df::construction_type::UpDownStair) + subtype = df::construction_type::UpDownStair; + return subtype; +} + +static bool is_suspendmanager_enabled(color_ostream &out) { + bool suspendmanager_enabled = false; + call_buildingplan_lua(&out, "is_suspendmanager_enabled", 0, 1, + Lua::DEFAULT_LUA_LAMBDA, + [&](lua_State *L){ + suspendmanager_enabled = lua_toboolean(L, -1); + }); + return suspendmanager_enabled; +} + DFhackCExport command_result plugin_load_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentData(CONFIG_KEY); @@ -307,21 +349,22 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); + bool unsuspend_on_finalize = !is_suspendmanager_enabled(out); for (size_t idx = 0; idx < num_building_configs; ++idx) { PlannedBuilding pb(out, building_configs[idx]); df::building *bld = df::building::find(pb.id); if (!bld) { - INFO(status,out).print("building %d no longer exists; skipping\n", pb.id); + DEBUG(status,out).print("building %d no longer exists; skipping\n", pb.id); pb.remove(out); continue; } - BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + BuildingTypeKey key(bld->getType(), get_subtype(bld), bld->getCustomType()); if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); pb.remove(out); continue; } - registerPlannedBuilding(out, pb); + registerPlannedBuilding(out, pb, unsuspend_on_finalize); } return CR_OK; @@ -334,7 +377,8 @@ static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; cycle_requested = false; - buildingplan_cycle(out, tasks, planned_buildings); + bool unsuspend_on_finalize = !is_suspendmanager_enabled(out); + buildingplan_cycle(out, tasks, planned_buildings, unsuspend_on_finalize); call_buildingplan_lua(&out, "signal_reset"); } @@ -424,7 +468,7 @@ static string getBucket(const df::job_item & ji, const PlannedBuilding & pb, int } // get a list of item vectors that we should search for matches -vector getVectorIds(color_ostream &out, const df::job_item *job_item) { +vector getVectorIds(color_ostream &out, const df::job_item *job_item, bool ignore_filters) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -440,13 +484,13 @@ vector getVectorIds(color_ostream &out, const df::job_it // which vectors to search if (job_item->flags2.bits.building_material) { - if (get_config_bool(config, CONFIG_BLOCKS)) + if (ignore_filters || get_config_bool(config, CONFIG_BLOCKS)) ret.push_back(df::job_item_vector_id::BLOCKS); - if (get_config_bool(config, CONFIG_BOULDERS)) + if (ignore_filters || get_config_bool(config, CONFIG_BOULDERS)) ret.push_back(df::job_item_vector_id::BOULDER); - if (get_config_bool(config, CONFIG_LOGS)) + if (ignore_filters || get_config_bool(config, CONFIG_LOGS)) ret.push_back(df::job_item_vector_id::WOOD); - if (get_config_bool(config, CONFIG_BARS)) + if (ignore_filters || get_config_bool(config, CONFIG_BARS)) ret.push_back(df::job_item_vector_id::BAR); } @@ -456,7 +500,7 @@ vector getVectorIds(color_ostream &out, const df::job_it return ret; } -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bool unsuspend_on_finalize) { df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); if (!bld) return false; @@ -466,10 +510,15 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { return false; } + // suspend jobs + for (auto job : bld->jobs) + job->flags.bits.suspend = true; + auto job_items = bld->jobs[0]->job_items; if (isJobReady(out, job_items)) { // all items are already attached - finalizeBuilding(out, bld); + finalizeBuilding(out, bld, unsuspend_on_finalize); + pb.remove(out); return true; } @@ -495,10 +544,6 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { } } - // 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); @@ -604,13 +649,19 @@ static bool isPlannedBuilding(color_ostream &out, df::building *bld) { 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())) + if (!bld || planned_buildings.count(bld->id)) return false; - BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + + int16_t subtype = get_subtype(bld); + + if (!isPlannableBuilding(out, bld->getType(), subtype, bld->getCustomType())) + return false; + + BuildingTypeKey key(bld->getType(), subtype, bld->getCustomType()); PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); - return registerPlannedBuilding(out, pb); + + bool unsuspend_on_finalize = !is_suspendmanager_enabled(out); + return registerPlannedBuilding(out, pb, unsuspend_on_finalize); } static void doCycle(color_ostream &out) { @@ -624,7 +675,7 @@ static void scheduleCycle(color_ostream &out) { } static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, - int32_t custom, int index, vector *item_ids = NULL, + int32_t custom, int index, bool ignore_filters, vector *item_ids = NULL, map *counts = NULL) { DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", @@ -639,19 +690,21 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ auto &specials = item_filters.getSpecials(); auto &jitem = job_items[index]; - auto vector_ids = getVectorIds(out, jitem); + auto vector_ids = getVectorIds(out, jitem, ignore_filters); int count = 0; for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { ItemFilter filter = filters[index]; - if (counts) { + set special = specials; + if (ignore_filters || counts) { // don't filter by material; we want counts for all materials filter.setMaterialMask(0); filter.setMaterials(set()); + special.clear(); } - if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, filter, specials)) { + if (itemPassesScreen(out, item) && matchesFilters(item, jitem, heat, filter, special)) { if (item_ids) item_ids->emplace_back(item->id); if (counts) { @@ -680,7 +733,7 @@ static int getAvailableItems(lua_State *L) { "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); vector item_ids; - scanAvailableItems(*out, type, subtype, custom, index, &item_ids); + scanAvailableItems(*out, type, subtype, custom, index, true, &item_ids); Lua::PushVector(L, item_ids); return 1; } @@ -703,11 +756,13 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); - return scanAvailableItems(out, type, subtype, custom, index); + return scanAvailableItems(out, type, subtype, custom, index, false); } static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { TRACE(status,out).print("entering hasFilter\n"); + if (!Core::getInstance().isWorldLoaded()) + return false; BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); if (index < 0 || filters.getItemFilters().size() <= (size_t)index) @@ -754,8 +809,16 @@ static int setMaterialMaskFilter(lua_State *L) { mask |= metal_cat.whole; else if (cat == "glass") mask |= glass_cat.whole; + else if (cat == "gem") + mask |= gem_cat.whole; else if (cat == "clay") mask |= clay_cat.whole; + else if (cat == "cloth") + mask |= cloth_cat.whole; + else if (cat == "silk") + mask |= silk_cat.whole; + else if (cat == "yarn") + mask |= yarn_cat.whole; } DEBUG(status,*out).print( "setting material mask filter for building_type=%d subtype=%d custom=%d index=%d to %x\n", @@ -800,7 +863,11 @@ static int getMaterialMaskFilter(lua_State *L) { ret.emplace("wood", !bits || bits & wood_cat.whole); ret.emplace("metal", !bits || bits & metal_cat.whole); ret.emplace("glass", !bits || bits & glass_cat.whole); + ret.emplace("gem", !bits || bits & gem_cat.whole); ret.emplace("clay", !bits || bits & clay_cat.whole); + ret.emplace("cloth", !bits || bits & cloth_cat.whole); + ret.emplace("silk", !bits || bits & silk_cat.whole); + ret.emplace("yarn", !bits || bits & yarn_cat.whole); Lua::Push(L, ret); return 1; } @@ -845,8 +912,16 @@ static int setMaterialFilter(lua_State *L) { mask.whole |= metal_cat.whole; else if (mat.matches(glass_cat)) mask.whole |= glass_cat.whole; + else if (mat.matches(gem_cat)) + mask.whole |= gem_cat.whole; else if (mat.matches(clay_cat)) mask.whole |= clay_cat.whole; + else if (mat.matches(cloth_cat)) + mask.whole |= cloth_cat.whole; + else if (mat.matches(silk_cat)) + mask.whole |= silk_cat.whole; + else if (mat.matches(yarn_cat)) + mask.whole |= yarn_cat.whole; } filter.setMaterialMask(mask.whole); get_item_filters(*out, key).setItemFilter(*out, filter, index); @@ -871,7 +946,7 @@ static int getMaterialFilter(lua_State *L) { return 0; const auto &mat_filter = filters[index].getMaterials(); map counts; - scanAvailableItems(*out, type, subtype, custom, index, NULL, &counts); + scanAvailableItems(*out, type, subtype, custom, index, false, NULL, &counts); HeatSafety heat = get_heat_safety_filter(key); df::job_item jitem_cur_heat = getJobItemWithHeatSafety( get_job_items(*out, key)[index], heat); @@ -905,8 +980,10 @@ static int getMaterialFilter(lua_State *L) { return 1; } -static void setChooseItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, bool choose) { - DEBUG(status,out).print("entering setChooseItems\n"); +static void setChooseItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int choose) { + DEBUG(status,out).print( + "entering setChooseItems building_type=%d subtype=%d custom=%d choose=%d\n", + type, subtype, custom, choose); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); filters.setChooseItems(choose); diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 495602b0b..9bfd38731 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -42,14 +42,20 @@ enum HeatSafety { HEAT_SAFETY_MAGMA = 2, }; +enum ItemSelectionChoice { + ITEM_SELECTION_CHOICE_FILTER = 0, + ITEM_SELECTION_CHOICE_MANUAL = 1, + ITEM_SELECTION_CHOICE_AUTOMATERIAL = 2, +}; + int get_config_val(DFHack::PersistentDataItem &c, int index); bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); -std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item); -bool itemPassesScreen(df::item * item); +std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item, bool ignore_filters); +bool itemPassesScreen(DFHack::color_ostream& out, df::item* item); df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat); bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter, const std::set &special); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); -void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); +void finalizeBuilding(DFHack::color_ostream &out, df::building *bld, bool unsuspend_on_finalize); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 803f1f130..45cafe474 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -43,10 +43,27 @@ struct BadFlags { } }; -bool itemPassesScreen(df::item * item) { +// This is tricky. we want to choose an item that can be brought to the job site, but that's not +// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or +// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. +static bool isAccessible(color_ostream& out, df::item* item) { + df::coord item_pos = Items::getPosition(item); + df::map_block* block = Maps::getTileBlock(item_pos); + bool is_walkable = false; + if (block) { + uint16_t walkability_group = index_tile(block->walkable, item_pos); + is_walkable = walkability_group != 0; + TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); + } + return is_walkable; +} + +bool itemPassesScreen(color_ostream& out, df::item* item) { static const BadFlags bad_flags; return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile(); + && !item->isAssignedToStockpile() + && isAccessible(out, item); } df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat) { @@ -111,7 +128,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // 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. -void finalizeBuilding(color_ostream &out, df::building *bld) { +void finalizeBuilding(color_ostream &out, df::building *bld, bool unsuspend_on_finalize) { DEBUG(cycle,out).print("finalizing building %d\n", bld->id); auto job = bld->jobs[0]; @@ -143,8 +160,10 @@ void finalizeBuilding(color_ostream &out, df::building *bld) { } // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); + if (unsuspend_on_finalize) { + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); + } } static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, @@ -163,25 +182,10 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, return NULL; } -// This is tricky. we want to choose an item that can be brought to the job site, but that's not -// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or -// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. -static bool isAccessibleFrom(color_ostream &out, df::item *item, df::job *job) { - df::coord item_pos = Items::getPosition(item); - df::map_block *block = Maps::getTileBlock(item_pos); - bool is_walkable = false; - if (block) { - uint16_t walkability_group = index_tile(block->walkable, item_pos); - is_walkable = walkability_group != 0; - TRACE(cycle,out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", - item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "" : "not "); - } - return is_walkable; -} - static void doVector(color_ostream &out, df::job_item_vector_id vector_id, map &buckets, - unordered_map &planned_buildings) { + unordered_map &planned_buildings, + bool unsuspend_on_finalize) { 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", @@ -192,7 +196,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, item_it != item_vector.rend(); ++item_it) { auto item = *item_it; - if (!itemPassesScreen(item)) + if (!itemPassesScreen(out, item)) continue; for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { TRACE(cycle,out).print("scanning bucket: %s/%s\n", @@ -215,8 +219,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto filter_idx = task.second; const int rev_filter_idx = num_filters - (filter_idx+1); auto &pb = planned_buildings.at(id); - if (isAccessibleFrom(out, item, job) - && matchesFilters(item, jitems[filter_idx], pb.heat_safety, + if (matchesFilters(item, jitems[filter_idx], pb.heat_safety, pb.item_filters[rev_filter_idx], pb.specials) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) @@ -239,7 +242,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, --jitems[filter_idx]->quantity; task_queue.pop_front(); if (isJobReady(out, jitems)) { - finalizeBuilding(out, bld); + finalizeBuilding(out, bld, unsuspend_on_finalize); planned_buildings.at(id).remove(out); } if (task_queue.empty()) { @@ -274,7 +277,7 @@ struct VectorsToScanLast { }; void buildingplan_cycle(color_ostream &out, Tasks &tasks, - unordered_map &planned_buildings) { + unordered_map &planned_buildings, bool unsuspend_on_finalize) { static const VectorsToScanLast vectors_to_scan_last; DEBUG(cycle,out).print( @@ -292,7 +295,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, } auto & buckets = it->second; - doVector(out, vector_id, buckets, planned_buildings); + doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); 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(), @@ -306,7 +309,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, if (tasks.count(vector_id) == 0) continue; auto & buckets = tasks[vector_id]; - doVector(out, vector_id, buckets, planned_buildings); + doVector(out, vector_id, buckets, planned_buildings, unsuspend_on_finalize); 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(), diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index fc7dd9f56..069459841 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -39,14 +39,14 @@ static string serialize(const std::vector &item_filters, const std:: } DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) - : key(key), choose_items(false) { + : key(key), choose_items(ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER) { DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n", std::get<0>(key), std::get<1>(key), std::get<2>(key)); filter_config = World::AddPersistentData(FILTER_CONFIG_KEY); set_config_val(filter_config, FILTER_CONFIG_TYPE, std::get<0>(key)); set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key)); set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key)); - set_config_bool(filter_config, FILTER_CONFIG_CHOOSE_ITEMS, choose_items); + set_config_val(filter_config, FILTER_CONFIG_CHOOSE_ITEMS, choose_items); item_filters.resize(jitems.size()); for (size_t idx = 0; idx < jitems.size(); ++idx) { item_filters[idx].setMaxQuality(get_max_quality(jitems[idx]), true); @@ -56,10 +56,15 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &filter_config, const std::vector &jitems) : key(getKey(filter_config)), filter_config(filter_config) { - choose_items = get_config_bool(filter_config, FILTER_CONFIG_CHOOSE_ITEMS); + choose_items = get_config_val(filter_config, FILTER_CONFIG_CHOOSE_ITEMS); + if (choose_items < ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER || + choose_items > ItemSelectionChoice::ITEM_SELECTION_CHOICE_AUTOMATERIAL) + choose_items = ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER; auto &serialized = filter_config.val(); DEBUG(status,out).print("deserializing default item filters for key %d,%d,%d: %s\n", std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + if (!jitems.size()) + return; std::vector elems; split_string(&elems, serialized, "|"); std::vector filters = deserialize_item_filters(out, elems[0]); @@ -79,9 +84,9 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f } } -void DefaultItemFilters::setChooseItems(bool choose) { +void DefaultItemFilters::setChooseItems(int choose) { choose_items = choose; - set_config_bool(filter_config, FILTER_CONFIG_CHOOSE_ITEMS, choose); + set_config_val(filter_config, FILTER_CONFIG_CHOOSE_ITEMS, choose); } void DefaultItemFilters::setSpecial(const std::string &special, bool val) { diff --git a/plugins/buildingplan/defaultitemfilters.h b/plugins/buildingplan/defaultitemfilters.h index d7ed12a7b..7d285ce4c 100644 --- a/plugins/buildingplan/defaultitemfilters.h +++ b/plugins/buildingplan/defaultitemfilters.h @@ -14,17 +14,17 @@ public: DefaultItemFilters(DFHack::color_ostream &out, BuildingTypeKey key, const std::vector &jitems); DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector &jitems); - void setChooseItems(bool choose); + void setChooseItems(int choose); void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index); void setSpecial(const std::string &special, bool val); - bool getChooseItems() const { return choose_items; } + int getChooseItems() const { return choose_items; } const std::vector & getItemFilters() const { return item_filters; } const std::set & getSpecials() const { return specials; } private: DFHack::PersistentDataItem filter_config; - bool choose_items; + int choose_items; std::vector item_filters; std::set specials; }; diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index 8e66c3ed7..dac5f98d6 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -35,7 +35,7 @@ bool ItemFilter::isEmpty() const { && materials.empty(); } -static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat_mask) { +static bool deserializeMaterialMask(const string& ser, df::dfhack_material_category& mat_mask) { if (ser.empty()) return true; @@ -46,7 +46,7 @@ static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat return true; } -static bool deserializeMaterials(string ser, set &materials) { +static bool deserializeMaterials(const string& ser, set &materials) { if (ser.empty()) return true; @@ -63,7 +63,7 @@ static bool deserializeMaterials(string ser, set &material return true; } -ItemFilter::ItemFilter(color_ostream &out, string serialized) : ItemFilter() { +ItemFilter::ItemFilter(color_ostream &out, const string& serialized) : ItemFilter() { vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() < 5) { @@ -87,11 +87,9 @@ string ItemFilter::serialize() const { std::ostringstream ser; ser << bitfield_to_string(mat_mask, ",") << "/"; vector matstrs; - if (!materials.empty()) { - for (auto &mat : materials) - matstrs.emplace_back(mat.getToken()); - ser << join_strings(",", matstrs); - } + for (auto &mat : materials) + matstrs.emplace_back(mat.getToken()); + ser << join_strings(",", matstrs); ser << "/" << static_cast(min_quality); ser << "/" << static_cast(max_quality); ser << "/" << static_cast(decorated_only); diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 5ae59dd4a..c74150434 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -8,7 +8,7 @@ class ItemFilter { public: ItemFilter(); - ItemFilter(DFHack::color_ostream &out, std::string serialized); + ItemFilter(DFHack::color_ostream &out, const std::string& serialized); void clear(); bool isEmpty() const; diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index 2be2cf26f..a20d7b29a 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -29,7 +29,7 @@ static vector> get_vector_ids(color_ostream &out, auto &jitems = bld->jobs[0]->job_items; int num_job_items = (int)jitems.size(); for (int jitem_idx = num_job_items - 1; jitem_idx >= 0; --jitem_idx) { - ret.emplace_back(getVectorIds(out, jitems[jitem_idx])); + ret.emplace_back(getVectorIds(out, jitems[jitem_idx], false)); } return ret; } diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index be431722b..b2af2dbca 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -113,14 +113,10 @@ public: case job_type::CarveUpDownStaircase: td.bits.dig = tile_dig_designation::UpDownStair; break; - case job_type::DetailWall: - case job_type::DetailFloor: { - df::tiletype tt = map.tiletypeAt(job->pos); - if (tileSpecial(tt) != df::tiletype_special::SMOOTH) { - td.bits.smooth = 1; - } + case job_type::SmoothWall: + case job_type::SmoothFloor: + td.bits.smooth = 1; break; - } case job_type::CarveTrack: to.bits.carve_track_north = (job->item_category.whole >> 18) & 1; to.bits.carve_track_south = (job->item_category.whole >> 19) & 1; diff --git a/plugins/faststart.cpp b/plugins/faststart.cpp new file mode 100644 index 000000000..de014801c --- /dev/null +++ b/plugins/faststart.cpp @@ -0,0 +1,69 @@ +// Fast Startup tweak + +#include "Core.h" +#include +#include +#include +#include +#include + +#include "df/viewscreen_initial_prepst.h" +#include + +using namespace DFHack; +using namespace df::enums; +using std::vector; + +// Uncomment this to make the Loading screen as fast as possible +// This has the side effect of removing the dwarf face animation +// (and briefly making the game become unresponsive) + +//#define REALLY_FAST + +DFHACK_PLUGIN("faststart"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +struct prep_hook : df::viewscreen_initial_prepst +{ + typedef df::viewscreen_initial_prepst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, logic, ()) + { +#ifdef REALLY_FAST + while (breakdown_level != interface_breakdown_types::STOPSCREEN) + { + render_count++; + INTERPOSE_NEXT(logic)(); + } +#else + render_count = 4; + INTERPOSE_NEXT(logic)(); +#endif + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(prep_hook, logic); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (enable != is_enabled) + { + if (!INTERPOSE_HOOK(prep_hook, logic).apply(enable)) + return CR_FAILURE; + + is_enabled = enable; + } + + return CR_OK; +} + +DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) +{ + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + INTERPOSE_HOOK(prep_hook, logic).remove(); + return CR_OK; +} diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 565316fa6..87f1c0e76 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -1,13 +1,6 @@ -// (un)designate matching plants for gathering/cutting -#include - -#include "Core.h" -#include "Console.h" -#include "Export.h" +#include "Debug.h" #include "PluginManager.h" -#include "DataDefs.h" #include "TileTypes.h" -#include "MiscUtils.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -32,6 +25,11 @@ using std::set; using namespace DFHack; using namespace df::enums; +namespace DFHack +{ +DBG_DECLARE(getplants, log, DebugCategory::LINFO); +} + DFHACK_PLUGIN("getplants"); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); @@ -75,49 +73,38 @@ enum class selectability { // is one of the issues in bug 6940 on the bug tracker (the others cases are detected and // result in the plants not being usable for farming or even collectable at all). -//selectability selectablePlant(color_ostream &out, const df::plant_raw *plant, bool farming) -selectability selectablePlant(const df::plant_raw *plant, bool farming) -{ +selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bool farming) { const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type[plant_material_def::basic_mat], plant->material_defs.idx[plant_material_def::basic_mat]); bool outOfSeason = false; selectability result = selectability::Nonselectable; - if (plant->flags.is_set(plant_raw_flags::TREE)) - { -// out.print("%s is a selectable tree\n", plant->id.c_str()); - if (farming) - { + if (plant->flags.is_set(plant_raw_flags::TREE)) { + DEBUG(log, out).print("%s is a selectable tree\n", plant->id.c_str()); + if (farming) { return selectability::Nonselectable; } - else - { + else { return selectability::Selectable; } } - else if (plant->flags.is_set(plant_raw_flags::GRASS)) - { -// out.print("%s is a non selectable Grass\n", plant->id.c_str()); + else if (plant->flags.is_set(plant_raw_flags::GRASS)) { + DEBUG(log, out).print("%s is a non selectable Grass\n", plant->id.c_str()); return selectability::Grass; } - if (farming && plant->material_defs.type[plant_material_def::seed] == -1) - { + if (farming && plant->material_defs.type[plant_material_def::seed] == -1) { return selectability::Nonselectable; } if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || - basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) - { -// out.print("%s is edible\n", plant->id.c_str()); - if (farming) - { - if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW)) - { + basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) { + DEBUG(log, out).print("%s is edible\n", plant->id.c_str()); + if (farming) { + if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW)) { result = selectability::Selectable; } } - else - { + else { return selectability::Selectable; } } @@ -126,54 +113,43 @@ selectability selectablePlant(const df::plant_raw *plant, bool farming) plant->flags.is_set(plant_raw_flags::MILL) || plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) || plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || - plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) - { -// out.print("%s is thread/mill/extract\n", plant->id.c_str()); - if (farming) - { + plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) { + DEBUG(log, out).print("%s is thread/mill/extract\n", plant->id.c_str()); + if (farming) { result = selectability::Selectable; } - else - { + else { return selectability::Selectable; } } if (basic_mat.material->reaction_product.id.size() > 0 || - basic_mat.material->reaction_class.size() > 0) - { -// out.print("%s has a reaction\n", plant->id.c_str()); - if (farming) - { + basic_mat.material->reaction_class.size() > 0) { + DEBUG(log, out).print("%s has a reaction\n", plant->id.c_str()); + if (farming) { result = selectability::Selectable; } - else - { + else { return selectability::Selectable; } } - for (size_t i = 0; i < plant->growths.size(); i++) - { + for (size_t i = 0; i < plant->growths.size(); i++) { if (plant->growths[i]->item_type == df::item_type::SEEDS || // Only trees have seed growths in vanilla, but raws can be modded... - plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) - { + plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) { const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant->growths[i]->mat_type, plant->growths[i]->mat_index); if ((plant->growths[i]->item_type == df::item_type::SEEDS && - (growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) || - growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) || + (growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) || + growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) || (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH && - growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now... + growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now... { bool seedSource = plant->growths[i]->item_type == df::item_type::SEEDS; - if (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) - { - for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) - { + if (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) { + for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) { if (growth_mat.material->reaction_product.material.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && - growth_mat.material->reaction_product.material.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) - { + growth_mat.material->reaction_product.material.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { seedSource = true; break; } @@ -182,52 +158,46 @@ selectability selectablePlant(const df::plant_raw *plant, bool farming) if (*cur_year_tick >= plant->growths[i]->timing_1 && (plant->growths[i]->timing_2 == -1 || - *cur_year_tick <= plant->growths[i]->timing_2)) - { -// out.print("%s has an edible seed or a stockpile growth\n", plant->id.c_str()); - if (!farming || seedSource) - { + *cur_year_tick <= plant->growths[i]->timing_2)) { + DEBUG(log, out).print("%s has an edible seed or a stockpile growth\n", plant->id.c_str()); + if (!farming || seedSource) { return selectability::Selectable; } } - else - { - if (!farming || seedSource) - { + else { + if (!farming || seedSource) { outOfSeason = true; } } } } -/* else if (plant->growths[i]->behavior.bits.has_seed) // This code designates beans, etc. when DF doesn't, but plant gatherers still fail to collect anything, so it's useless: bug #0006940. - { - const DFHack::MaterialInfo seed_mat = DFHack::MaterialInfo(plant->material_defs.type[plant_material_def::seed], plant->material_defs.idx[plant_material_def::seed]); - - if (seed_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || - seed_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) - { - if (*cur_year_tick >= plant->growths[i]->timing_1 && - (plant->growths[i]->timing_2 == -1 || - *cur_year_tick <= plant->growths[i]->timing_2)) + /* else if (plant->growths[i]->behavior.bits.has_seed) // This code designates beans, etc. when DF doesn't, but plant gatherers still fail to collect anything, so it's useless: bug #0006940. { - return selectability::Selectable; - } - else - { - outOfSeason = true; - } - } - } */ + const DFHack::MaterialInfo seed_mat = DFHack::MaterialInfo(plant->material_defs.type[plant_material_def::seed], plant->material_defs.idx[plant_material_def::seed]); + + if (seed_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || + seed_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) + { + if (*cur_year_tick >= plant->growths[i]->timing_1 && + (plant->growths[i]->timing_2 == -1 || + *cur_year_tick <= plant->growths[i]->timing_2)) + { + return selectability::Selectable; + } + else + { + outOfSeason = true; + } + } + } */ } - if (outOfSeason) - { -// out.print("%s has an out of season growth\n", plant->id.c_str()); + if (outOfSeason) { + DEBUG(log, out).print("%s has an out of season growth\n", plant->id.c_str()); return selectability::OutOfSeason; } - else - { -// out.printerr("%s cannot be gathered\n", plant->id.c_str()); + else { + DEBUG(log, out).print("%s cannot be gathered\n", plant->id.c_str()); return result; } } @@ -241,17 +211,17 @@ bool ripe(int32_t x, int32_t y, int32_t start, int32_t end) { } // Looks in the picked growths vector to see if a matching growth has been marked as picked. -bool picked(const df::plant *plant, int32_t growth_subtype) { - df::world_data *world_data = world->world_data; - df::world_site *site = df::world_site::find(plotinfo->site_id); +bool picked(const df::plant* plant, int32_t growth_subtype) { + df::world_data* world_data = world->world_data; + df::world_site* site = df::world_site::find(plotinfo->site_id); int32_t pos_x = site->global_min_x + plant->pos.x / 48; int32_t pos_y = site->global_min_y + plant->pos.y / 48; size_t id = pos_x + pos_y * 16 * world_data->world_width; - df::world_object_data *object_data = df::world_object_data::find(id); + df::world_object_data* object_data = df::world_object_data::find(id); if (!object_data) { return false; } - df::map_block_column *column = world->map.map_block_columns[(plant->pos.x / 16) * world->map.x_count_block + (plant->pos.y / 16)]; + df::map_block_column* column = world->map.map_block_columns[(plant->pos.x / 16) * world->map.x_count_block + (plant->pos.y / 16)]; for (size_t i = 0; i < object_data->picked_growths.x.size(); i++) { if (object_data->picked_growths.x[i] == plant->pos.x && @@ -266,13 +236,20 @@ bool picked(const df::plant *plant, int32_t growth_subtype) { return false; } -bool designate(const df::plant *plant, bool farming) { - df::plant_raw *plant_raw = world->raws.plants.all[plant->material]; +bool designate(color_ostream& out, const df::plant* plant, bool farming) { + TRACE(log, out).print("Attempting to designate %s at (%i, %i, %i)\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z); + + if (!farming) { + bool istree = (tileMaterial(Maps::getTileBlock(plant->pos)->tiletype[plant->pos.x % 16][plant->pos.y % 16]) == tiletype_material::TREE); + if (istree) + return Designations::markPlant(plant); + } + + df::plant_raw* plant_raw = world->raws.plants.all[plant->material]; const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant_raw->material_defs.type[plant_material_def::basic_mat], plant_raw->material_defs.idx[plant_material_def::basic_mat]); if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || - basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) - { + basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) { return Designations::markPlant(plant); } @@ -280,63 +257,60 @@ bool designate(const df::plant *plant, bool farming) { plant_raw->flags.is_set(plant_raw_flags::MILL) || plant_raw->flags.is_set(plant_raw_flags::EXTRACT_VIAL) || plant_raw->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || - plant_raw->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) - { + plant_raw->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) { if (!farming) { return Designations::markPlant(plant); } } if (basic_mat.material->reaction_product.id.size() > 0 || - basic_mat.material->reaction_class.size() > 0) - { + basic_mat.material->reaction_class.size() > 0) { if (!farming) { return Designations::markPlant(plant); } } - for (size_t i = 0; i < plant_raw->growths.size(); i++) - { - if (plant_raw->growths[i]->item_type == df::item_type::SEEDS || // Only trees have seed growths in vanilla, but raws can be modded... - plant_raw->growths[i]->item_type == df::item_type::PLANT_GROWTH) - { - const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant_raw->growths[i]->mat_type, plant_raw->growths[i]->mat_index); - if ((plant_raw->growths[i]->item_type == df::item_type::SEEDS && - (growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) || - growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) || - (plant_raw->growths[i]->item_type == df::item_type::PLANT_GROWTH && - growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now... - { - bool seedSource = plant_raw->growths[i]->item_type == df::item_type::SEEDS; + for (size_t i = 0; i < plant_raw->growths.size(); i++) { + TRACE(log, out).print("growth item type=%d\n", plant_raw->growths[i]->item_type); + // Only trees have seed growths in vanilla, but raws can be modded... + if (plant_raw->growths[i]->item_type != df::item_type::SEEDS && + plant_raw->growths[i]->item_type != df::item_type::PLANT_GROWTH) + continue; - if (plant_raw->growths[i]->item_type == df::item_type::PLANT_GROWTH) - { - for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) - { - if (growth_mat.material->reaction_product.material.mat_type[k] == plant_raw->material_defs.type[plant_material_def::seed] && - growth_mat.material->reaction_product.material.mat_index[k] == plant_raw->material_defs.idx[plant_material_def::seed]) - { - seedSource = true; - break; - } - } - } + const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant_raw->growths[i]->mat_type, plant_raw->growths[i]->mat_index); + TRACE(log, out).print("edible_cooked=%d edible_raw=%d leaf_mat=%d\n", + growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED), + growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW), + growth_mat.material->flags.is_set(material_flags::LEAF_MAT)); + if (!(plant_raw->growths[i]->item_type == df::item_type::SEEDS && + (growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) || + growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) && + !(plant_raw->growths[i]->item_type == df::item_type::PLANT_GROWTH && + growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now... + continue; - bool istree = (tileMaterial(Maps::getTileBlock(plant->pos)->tiletype[plant->pos.x % 16][plant->pos.y % 16]) == tiletype_material::TREE); - bool isripe = ripe(plant->pos.x, plant->pos.y, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2); - if ((!farming || seedSource) && (istree || isripe) && !picked(plant, i)) - { - return Designations::markPlant(plant); + bool seedSource = plant_raw->growths[i]->item_type == df::item_type::SEEDS; + + if (plant_raw->growths[i]->item_type == df::item_type::PLANT_GROWTH) { + for (size_t k = 0; growth_mat.material->reaction_product.material.mat_type.size(); k++) { + if (growth_mat.material->reaction_product.material.mat_type[k] == plant_raw->material_defs.type[plant_material_def::seed] && + growth_mat.material->reaction_product.material.mat_index[k] == plant_raw->material_defs.idx[plant_material_def::seed]) { + seedSource = true; + break; } } } + + if ((!farming || seedSource) && + ripe(plant->pos.x, plant->pos.y, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && + !picked(plant, i)) + return Designations::markPlant(plant); } return false; } -command_result df_getplants (color_ostream &out, vector & parameters) -{ +command_result df_getplants(color_ostream& out, vector & parameters) { string plantMatStr = ""; std::vector plantSelections; std::vector collectionCount; @@ -348,16 +322,14 @@ command_result df_getplants (color_ostream &out, vector & parameters) plantSelections.resize(world->raws.plants.all.size()); collectionCount.resize(world->raws.plants.all.size()); - for (size_t i = 0; i < plantSelections.size(); i++) - { + for (size_t i = 0; i < plantSelections.size(); i++) { plantSelections[i] = selectability::Unselected; collectionCount[i] = 0; } bool anyPlantsSelected = false; - for (size_t i = 0; i < parameters.size(); i++) - { + for (size_t i = 0; i < parameters.size(); i++) { if (parameters[i] == "help" || parameters[i] == "?") return CR_WRONG_USAGE; else if (parameters[i] == "-t") @@ -374,23 +346,18 @@ command_result df_getplants (color_ostream &out, vector & parameters) verbose = true; else if (parameters[i] == "-f") farming = true; - else if (parameters[i] == "-n") - { - if (parameters.size() > i + 1) - { + else if (parameters[i] == "-n") { + if (parameters.size() > i + 1) { maxCount = atoi(parameters[i + 1].c_str()); - if (maxCount >= 1) - { + if (maxCount >= 1) { i++; // We've consumed the next parameter, so we need to progress the iterator. } - else - { + else { out.printerr("-n requires a positive integer parameter!\n"); return CR_WRONG_USAGE; } } - else - { + else { out.printerr("-n requires a positive integer parameter!\n"); return CR_WRONG_USAGE; } @@ -398,55 +365,43 @@ command_result df_getplants (color_ostream &out, vector & parameters) else plantNames.insert(toUpper(parameters[i])); } - if (treesonly && shrubsonly) - { + if (treesonly && shrubsonly) { out.printerr("Cannot specify both -t and -s at the same time!\n"); return CR_WRONG_USAGE; } - if (treesonly && farming) - { + if (treesonly && farming) { out.printerr("Cannot specify both -t and -f at the same time!\n"); return CR_WRONG_USAGE; } - if (all && exclude) - { + if (all && exclude) { out.printerr("Cannot specify both -a and -x at the same time!\n"); return CR_WRONG_USAGE; } - if (all && plantNames.size()) - { + if (all && plantNames.size()) { out.printerr("Cannot specify -a along with plant IDs!\n"); return CR_WRONG_USAGE; } CoreSuspender suspend; - for (size_t i = 0; i < world->raws.plants.all.size(); i++) - { - df::plant_raw *plant = world->raws.plants.all[i]; - if (all) - { -// plantSelections[i] = selectablePlant(out, plant, farming); - plantSelections[i] = selectablePlant(plant, farming); + for (size_t i = 0; i < world->raws.plants.all.size(); i++) { + df::plant_raw* plant = world->raws.plants.all[i]; + if (all) { + plantSelections[i] = selectablePlant(out, plant, farming); } - else if (plantNames.find(plant->id) != plantNames.end()) - { + else if (plantNames.find(plant->id) != plantNames.end()) { plantNames.erase(plant->id); -// plantSelections[i] = selectablePlant(out, plant, farming); - plantSelections[i] = selectablePlant(plant, farming); - switch (plantSelections[i]) - { + plantSelections[i] = selectablePlant(out, plant, farming); + switch (plantSelections[i]) { case selectability::Grass: out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); break; case selectability::Nonselectable: - if (farming) - { + if (farming) { out.printerr("%s does not have any parts that can be gathered for seeds for farming\n", plant->id.c_str()); } - else - { + else { out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str()); } break; @@ -463,8 +418,7 @@ command_result df_getplants (color_ostream &out, vector & parameters) } } } - if (plantNames.size() > 0) - { + if (plantNames.size() > 0) { out.printerr("Invalid plant ID(s):"); for (set::const_iterator it = plantNames.begin(); it != plantNames.end(); it++) out.printerr(" %s", it->c_str()); @@ -472,33 +426,26 @@ command_result df_getplants (color_ostream &out, vector & parameters) return CR_FAILURE; } - for (size_t i = 0; i < plantSelections.size(); i++) - { + for (size_t i = 0; i < plantSelections.size(); i++) { if (plantSelections[i] == selectability::OutOfSeason || - plantSelections[i] == selectability::Selectable) - { + plantSelections[i] == selectability::Selectable) { anyPlantsSelected = true; break; } } - if (!anyPlantsSelected) - { + if (!anyPlantsSelected) { out.print("Valid plant IDs:\n"); - for (size_t i = 0; i < world->raws.plants.all.size(); i++) - { - df::plant_raw *plant = world->raws.plants.all[i]; -// switch (selectablePlant(out, plant, farming)) - switch (selectablePlant(plant, farming)) - { + for (size_t i = 0; i < world->raws.plants.all.size(); i++) { + df::plant_raw* plant = world->raws.plants.all[i]; + switch (selectablePlant(out, plant, farming)) { case selectability::Grass: case selectability::Nonselectable: continue; case selectability::OutOfSeason: { - if (!treesonly) - { + if (!treesonly) { out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str()); } break; @@ -523,22 +470,21 @@ command_result df_getplants (color_ostream &out, vector & parameters) } count = 0; - for (size_t i = 0; i < world->plants.all.size(); i++) - { - const df::plant *plant = world->plants.all[i]; - df::map_block *cur = Maps::getTileBlock(plant->pos); + for (size_t i = 0; i < world->plants.all.size(); i++) { + const df::plant* plant = world->plants.all[i]; + df::map_block* cur = Maps::getTileBlock(plant->pos); + + TRACE(log, out).print("Examining %s at (%i, %i, %i) [index=%d]\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); int x = plant->pos.x % 16; int y = plant->pos.y % 16; if (plantSelections[plant->material] == selectability::OutOfSeason || - plantSelections[plant->material] == selectability::Selectable) - { + plantSelections[plant->material] == selectability::Selectable) { if (exclude || plantSelections[plant->material] == selectability::OutOfSeason) continue; } - else - { + else { if (!exclude) continue; } @@ -553,37 +499,29 @@ command_result df_getplants (color_ostream &out, vector & parameters) continue; if (collectionCount[plant->material] >= maxCount) continue; - if (deselect && Designations::unmarkPlant(plant)) - { + if (deselect && Designations::unmarkPlant(plant)) { collectionCount[plant->material]++; ++count; } - if (!deselect && designate(plant, farming)) - { -// out.print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + if (!deselect && designate(out, plant, farming)) { + DEBUG(log, out).print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); collectionCount[plant->material]++; ++count; } } - if (count) - { - if (verbose) - { - for (size_t i = 0; i < plantSelections.size(); i++) - { - if (collectionCount[i] > 0) - out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str()); - } - out.print("\n"); + if (count && verbose) { + for (size_t i = 0; i < plantSelections.size(); i++) { + if (collectionCount[i] > 0) + out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str()); } + out.print("\n"); } out.print("Updated %d plant designations.\n", (int)count); return CR_OK; } -DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) -{ +DFhackCExport command_result plugin_init(color_ostream& out, vector & commands) { commands.push_back(PluginCommand( "getplants", "Designate trees for chopping and shrubs for gathering.", @@ -591,7 +529,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector find(":"); // colons at location 0 are for commands like ":lua" if (colon_pos == string::npos || colon_pos == 0) { - add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu); + add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); } else { vector tokens; @@ -111,7 +131,7 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { string focus = tokens[0].substr(1); if(Gui::matchFocusString(focus)) { auto cmdline = trim(tokens[1]); - add_binding_if_valid(sym, cmdline, screen, filtermenu); + add_binding_if_valid(out, sym, cmdline, screen, filtermenu); } } } @@ -124,7 +144,10 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { } static int getHotkeys(lua_State *L) { - find_active_keybindings(Gui::getCurViewscreen(true), true); + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + find_active_keybindings(*out, Gui::getCurViewscreen(true), true); Lua::PushVector(L, sorted_keys); Lua::Push(L, current_bindings); return 2; @@ -140,7 +163,7 @@ static void list(color_ostream &out) { DEBUG(log).print("listing active hotkeys\n"); bool was_valid = valid; if (!valid) - find_active_keybindings(Gui::getCurViewscreen(true), false); + find_active_keybindings(out, Gui::getCurViewscreen(true), false); out.print("Valid keybindings for the current focus:\n %s\n", join_strings("\n", Gui::getCurFocus(true)).c_str()); @@ -176,6 +199,8 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND ); } + CoreSuspender guard; + if (parameters[0] == "list") { list(out); return CR_OK; @@ -185,8 +210,6 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; - CoreSuspender guard; - int index = string_to_int(parameters[1], -1); if (index < 0) return CR_WRONG_USAGE; diff --git a/plugins/lua/autolabor.lua b/plugins/lua/autolabor.lua index 6ef4d7279..50f39b225 100644 --- a/plugins/lua/autolabor.lua +++ b/plugins/lua/autolabor.lua @@ -10,7 +10,7 @@ AutolaborOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/LABOR', frame={w=29, h=5}, - frame_style=gui.MEDIUM_FRAME, + frame_style=gui.THIN_FRAME, frame_background=gui.CLEAR_PEN, } @@ -18,9 +18,20 @@ function AutolaborOverlay:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text_pen=COLOR_RED, + text_pen=COLOR_LIGHTRED, + text='DFHack autolabor is active!', + visible=isEnabled, + }, + widgets.Label{ + frame={t=0, l=0}, + text_pen=COLOR_LIGHTRED, + text='Dwarf Therapist is active!', + visible=function() return not isEnabled() end, + }, + widgets.Label{ + frame={t=1, l=0}, + text_pen=COLOR_WHITE, text={ - 'DFHack autolabor is active!', NEWLINE, 'Any changes made on this', NEWLINE, 'screen will have no effect.' }, @@ -29,7 +40,7 @@ function AutolaborOverlay:init() end function AutolaborOverlay:render(dc) - if not isEnabled() then return false end + if df.global.game_extra.external_flag ~= 1 then return end AutolaborOverlay.super.render(self, dc) end diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 077470409..d64317eb0 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -51,6 +51,11 @@ function parse_commandline(...) return true end +function is_suspendmanager_enabled() + local ok, sm = pcall(reqscript, 'suspendmanager') + return ok and sm.isEnabled() +end + function get_num_filters(btype, subtype, custom) local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) return filters and #filters or 0 @@ -98,6 +103,16 @@ function get_desc(filter) desc = 'Mechanism' elseif desc == 'Wood' then desc = 'Log' + elseif desc == 'Any weapon' then + desc = 'Weapon' + elseif desc == 'Any spike' then + desc = 'Spike' + elseif desc == 'Ballistapart' then + desc = 'Ballista part' + elseif desc == 'Catapultpart' then + desc = 'Catapult part' + elseif desc == 'Smallgem' then + desc = 'Small, cut gem' end return desc diff --git a/plugins/lua/buildingplan/filterselection.lua b/plugins/lua/buildingplan/filterselection.lua index 84b5c46ea..e25b58eec 100644 --- a/plugins/lua/buildingplan/filterselection.lua +++ b/plugins/lua/buildingplan/filterselection.lua @@ -12,143 +12,6 @@ local function get_cur_filters() uibs.building_subtype, uibs.custom_type) end --------------------------------- --- Slider --- - -Slider = defclass(Slider, widgets.Widget) -Slider.ATTRS{ - num_stops=DEFAULT_NIL, - get_left_idx_fn=DEFAULT_NIL, - get_right_idx_fn=DEFAULT_NIL, - on_left_change=DEFAULT_NIL, - on_right_change=DEFAULT_NIL, -} - -function Slider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function Slider:init() - if self.num_stops < 2 then error('too few Slider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function slider_get_width_per_idx(self) - return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function Slider:onInput(keys) - if not keys._MOUSE_L_DOWN then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = slider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(right_idx-1) + 4 - if x < left_pos then - self.on_left_change(self.get_left_idx_fn() - 1) - elseif x < left_pos+3 then - self.is_dragging_target = 'left' - self.is_dragging_idx = x - left_pos - elseif x < right_pos then - self.is_dragging_target = 'both' - self.is_dragging_idx = x - left_pos - elseif x < right_pos+3 then - self.is_dragging_target = 'right' - self.is_dragging_idx = x - right_pos - else - self.on_right_change(self.get_right_idx_fn() + 1) - end - return true -end - -local function slider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = self.is_dragging_target == 'right' and -2 or 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - local new_left_idx, new_right_idx - if self.is_dragging_target == 'right' then - new_right_idx = new_idx - else - new_left_idx = new_idx - if self.is_dragging_target == 'both' then - new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() - if new_right_idx > self.num_stops then - return - end - end - end - if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then - self.on_left_change(new_left_idx) - end - if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then - self.on_right_change(new_right_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function Slider:onRenderBody(dc, rect) - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = slider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx > stop_idx or right_idx < stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif right_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if right_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tabs - dc:seek(width_per_idx*(left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - slider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - -------------------------------- -- QualityAndMaterialsPage -- @@ -328,7 +191,7 @@ function QualityAndMaterialsPage:init() enabled=enable_item_quality, on_change=function(val) self:set_max_quality(val+1) end, }, - Slider{ + widgets.RangeSlider{ frame={l=0, t=6}, num_stops=7, get_left_idx_fn=function() @@ -452,7 +315,11 @@ function QualityAndMaterialsPage:refresh() make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats), make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats), make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats), + make_cat_choice('Gem', 'gem', 'CUSTOM_SHIFT_E', cats), make_cat_choice('Clay', 'clay', 'CUSTOM_SHIFT_C', cats), + make_cat_choice('Cloth', 'cloth', 'CUSTOM_SHIFT_L', cats), + make_cat_choice('Silk', 'silk', 'CUSTOM_SHIFT_K', cats), + make_cat_choice('Yarn', 'yarn', 'CUSTOM_SHIFT_Y', cats), } self.subviews.materials_categories:setChoices(category_choices) diff --git a/plugins/lua/buildingplan/itemselection.lua b/plugins/lua/buildingplan/itemselection.lua index 8134b9455..84e866502 100644 --- a/plugins/lua/buildingplan/itemselection.lua +++ b/plugins/lua/buildingplan/itemselection.lua @@ -15,6 +15,12 @@ local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} -- most recent entries are at the *end* of the list local recently_used = {} +function get_automaterial_selection(building_type) + local tracker = recently_used[building_type] + if not tracker or not tracker.list then return end + return tracker.list[#tracker.list] +end + local function sort_by_type(a, b) local ad, bd = a.data, b.data return ad.item_type < bd.item_type or @@ -49,11 +55,12 @@ end ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', - frame={w=56, h=20, l=4, t=8}, + frame={w=56, h=24, l=4, t=7}, resizable=true, index=DEFAULT_NIL, desc=DEFAULT_NIL, quantity=DEFAULT_NIL, + autoselect=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -63,97 +70,155 @@ function ItemSelection:init() self.selected_set = {} local plural = self.quantity == 1 and '' or 's' + local choices = self:get_choices(sort_by_recency) + + if self.autoselect then + self:do_autoselect(choices) + if self.num_selected >= self.quantity then + self:submit(choices) + return + end + end + self:addviews{ - widgets.Label{ - frame={t=0, l=0, r=10}, - text={ - self.desc, - plural, - NEWLINE, - ('Select up to %d item%s ('):format(self.quantity, plural), - {text=function() return self.num_selected end}, - ' selected)', + widgets.Panel{ + view_id='header', + frame={t=0, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0, r=16}, + text={ + self.desc, plural, NEWLINE, + ('Select up to %d item%s ('):format(self.quantity, plural), + {text=function() return self.num_selected end}, + ' selected)', + }, + }, + widgets.Label{ + frame={r=0, w=15, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' Use filter ', NEWLINE, + ' for remaining ', NEWLINE, + ' items ', + }, + on_click=self:callback('submit'), + visible=function() return self.num_selected < self.quantity end, + }, + widgets.Label{ + frame={r=0, w=15, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' ', NEWLINE, + ' Continue ', NEWLINE, + ' ', + }, + on_click=self:callback('submit'), + visible=function() return self.num_selected >= self.quantity end, + }, }, }, - widgets.Label{ - frame={r=0, w=11, t=0, h=3}, - text_pen=BUILD_TEXT_PEN, - text_hpen=BUILD_TEXT_HPEN, - text={ - ' ', NEWLINE, - ' Confirm ', NEWLINE, - ' ', + } + + self:addviews{ + widgets.Panel{ + view_id='body', + frame={t=self.subviews.header.frame.h, b=4}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=1, t=2}, + key='CUSTOM_SHIFT_R', + label='Sort by:', + options={ + {label='Recently used', value=sort_by_recency}, + {label='Name', value=sort_by_name}, + {label='Amount', value=sort_by_quantity}, + }, + on_change=self:callback('on_sort'), + }, + widgets.Panel{ + frame={l=0, t=3, r=0, b=0}, + frame_style=gui.INTERIOR_FRAME, + subviews={ + widgets.FilteredList{ + view_id='flist', + frame={t=0, b=0}, + case_sensitive=false, + choices=choices, + icon_width=2, + on_submit=self:callback('toggle_group'), + }, + }, + }, }, - on_click=self:callback('submit'), }, - widgets.FilteredList{ - view_id='flist', - frame={t=3, l=0, r=0, b=4}, - case_sensitive=false, - choices=self:get_choices(sort_by_recency), - icon_width=2, - on_submit=self:callback('toggle_group'), - edit_on_char=function(ch) return ch:match('[%l -]') end, - }, - widgets.CycleHotkeyLabel{ - frame={l=0, b=2}, - key='CUSTOM_SHIFT_R', - label='Sort by:', - options={ - {label='Recently used', value=sort_by_recency}, - {label='Name', value=sort_by_name}, - {label='Amount', value=sort_by_quantity}, + widgets.Panel{ + view_id='footer', + frame={l=1, r=1, b=0, h=3}, + subviews={ + widgets.HotkeyLabel{ + frame={l=0, h=1, t=0}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep='----: ', -- these hypens function as "padding" to be overwritten by the next Label + label='Use one', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, w=5, t=0}, + text_pen=COLOR_LIGHTGREEN, + text='Right', -- this overrides the "6----" characters from the previous HotkeyLabel + }, + widgets.HotkeyLabel{ + frame={l=1, h=1, t=1}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep='---: ', -- these hypens function as "padding" to be overwritten by the next Label + label='Use one fewer', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=7, w=4, t=1}, + text_pen=COLOR_LIGHTGREEN, + text='Left', -- this overrides the "4---" characters from the previous HotkeyLabel + }, + widgets.HotkeyLabel{ + frame={l=6, t=2, h=2}, + key='SELECT', + label='Use all/none', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={r=5, t=0}, + key='LEAVESCREEN', + label='Go back', + auto_width=true, + on_activate=self:callback('on_cancel'), + }, + widgets.HotkeyLabel{ + frame={r=4, t=2}, + key='CUSTOM_SHIFT_C', + label='Continue', + auto_width=true, + on_activate=self:callback('submit'), + }, }, - on_change=self:callback('on_sort'), - }, - widgets.HotkeyLabel{ - frame={l=0, b=1}, - key='SELECT', - label='Use all/none', - auto_width=true, - on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, - }, - widgets.HotkeyLabel{ - frame={l=22, b=1}, - key='CUSTOM_SHIFT_C', - label='Confirm', - auto_width=true, - on_activate=self:callback('submit'), - }, - widgets.HotkeyLabel{ - frame={l=38, b=1}, - key='LEAVESCREEN', - label='Go back', - auto_width=true, - on_activate=self:callback('on_cancel'), - }, - widgets.HotkeyLabel{ - frame={l=0, b=0}, - key='KEYBOARD_CURSOR_RIGHT_FAST', - key_sep=' : ', - label='Use one', - auto_width=true, - on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, - }, - widgets.Label{ - frame={l=6, b=0, w=5}, - text_pen=COLOR_LIGHTGREEN, - text='Right', - }, - widgets.HotkeyLabel{ - frame={l=23, b=0}, - key='KEYBOARD_CURSOR_LEFT_FAST', - key_sep=' : ', - label='Use one fewer', - auto_width=true, - on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, - }, - widgets.Label{ - frame={l=29, b=0, w=4}, - text_pen=COLOR_LIGHTGREEN, - text='Left', }, } + + self.subviews.flist.list.frame.t = 0 + self.subviews.flist.edit.visible = false + self.subviews.flist.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.flist:callback('onFilterChange') end -- resort and restore selection @@ -206,7 +271,7 @@ function ItemSelection:get_choices(sort_fn) for desc,choice in pairs(buckets) do local data = choice.data choice.text = { - {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {width=10, text=function() return ('%d/%d'):format(data.selected, data.quantity) end}, {gap=2, text=desc}, } table.insert(choices, choice) @@ -215,6 +280,13 @@ function ItemSelection:get_choices(sort_fn) return choices end +function ItemSelection:do_autoselect(choices) + if #choices == 0 then return end + local desired = get_automaterial_selection(uibs.building_type) + if choices[1].search_key ~= desired then return end + self:toggle_group(1, choices[1]) +end + function ItemSelection:increment_group(idx, choice) local data = choice.data if self.quantity <= self.num_selected then return false end @@ -282,13 +354,13 @@ local function track_recently_used(choices) end end -function ItemSelection:submit() +function ItemSelection:submit(choices) local selected_items = {} for item_id in pairs(self.selected_set) do table.insert(selected_items, item_id) end if #selected_items > 0 then - track_recently_used(self.subviews.flist:getChoices()) + track_recently_used(choices or self.subviews.flist:getChoices()) end self.on_submit(selected_items) end @@ -328,6 +400,7 @@ ItemSelectionScreen.ATTRS { index=DEFAULT_NIL, desc=DEFAULT_NIL, quantity=DEFAULT_NIL, + autoselect=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -338,6 +411,7 @@ function ItemSelectionScreen:init() index=self.index, desc=self.desc, quantity=self.quantity, + autoselect=self.autoselect, on_submit=self.on_submit, on_cancel=self.on_cancel, } diff --git a/plugins/lua/buildingplan/pens.lua b/plugins/lua/buildingplan/pens.lua index 973bb7bc6..e69a4c210 100644 --- a/plugins/lua/buildingplan/pens.lua +++ b/plugins/lua/buildingplan/pens.lua @@ -2,9 +2,10 @@ local _ENV = mkmodule('plugins.buildingplan.pens') GOOD_TILE_PEN, BAD_TILE_PEN = nil, nil VERT_TOP_PEN, VERT_MID_PEN, VERT_BOT_PEN = nil, nil, nil +HORI_LEFT_PEN, HORI_MID_PEN, HORI_RIGHT_PEN = nil, nil, nil BUTTON_START_PEN, BUTTON_END_PEN = nil, nil SELECTED_ITEM_PEN = nil -MINIMIZED_LEFT_PEN, MINIMIZED_RIGHT_PEN = nil, nil +MINI_TEXT_PEN, MINI_TEXT_HPEN, MINI_BUTT_PEN, MINI_BUTT_HPEN = nil, nil, nil, nil local to_pen = dfhack.pen.parse @@ -19,17 +20,23 @@ function reload_pens() local tb_texpos = dfhack.textures.getThinBordersTexposStart() VERT_TOP_PEN = to_pen{tile=tp(tb_texpos, 10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} - VERT_MID_PEN = to_pen{tile=tp(tb_texpos, 4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} - VERT_BOT_PEN = to_pen{tile=tp(tb_texpos, 11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + VERT_MID_PEN = to_pen{tile=tp(tb_texpos, 4), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + VERT_BOT_PEN = to_pen{tile=tp(tb_texpos, 11), ch=193, fg=COLOR_GREY, bg=COLOR_BLACK} + + local mb_texpos = dfhack.textures.getMediumBordersTexposStart() + HORI_LEFT_PEN = to_pen{tile=tp(mb_texpos, 12), ch=195, fg=COLOR_GREY, bg=COLOR_BLACK} + HORI_MID_PEN = to_pen{tile=tp(mb_texpos, 5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} + HORI_RIGHT_PEN = to_pen{tile=tp(mb_texpos, 13), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} local cp_texpos = dfhack.textures.getControlPanelTexposStart() BUTTON_START_PEN = to_pen{tile=tp(cp_texpos, 13), ch='[', fg=COLOR_YELLOW} BUTTON_END_PEN = to_pen{tile=tp(cp_texpos, 15), ch=']', fg=COLOR_YELLOW} SELECTED_ITEM_PEN = to_pen{tile=tp(cp_texpos, 9), ch=string.char(251), fg=COLOR_YELLOW} - local wb_texpos = dfhack.textures.getWindowBordersTexposStart() - MINIMIZED_LEFT_PEN = to_pen{tile=tp(wb_texpos, 0), ch=199, fg=COLOR_WHITE} - MINIMIZED_RIGHT_PEN = to_pen{tile=tp(wb_texpos, 2), ch=182, fg=COLOR_WHITE} + MINI_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREY} + MINI_TEXT_HPEN = to_pen{fg=COLOR_BLACK, bg=COLOR_WHITE} + MINI_BUTT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_LIGHTRED} + MINI_BUTT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_RED} end reload_pens() diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index a60b2ebcd..803e9ae99 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -4,12 +4,15 @@ local itemselection = require('plugins.buildingplan.itemselection') local filterselection = require('plugins.buildingplan.filterselection') local gui = require('gui') local guidm = require('gui.dwarfmode') +local json = require('json') local overlay = require('plugins.overlay') local pens = require('plugins.buildingplan.pens') local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') +config = config or json.open('dfhack-config/buildingplan.json') + local uibs = df.global.buildreq reset_counts_flag = false @@ -23,13 +26,69 @@ local function is_choosing_area() return uibs.selection_pos.x >= 0 end -local function get_cur_area_dims(placement_data) - if not placement_data and not is_choosing_area() then return 1, 1, 1 end - local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos - local pos = placement_data and placement_data.p2 or uibs.pos - return math.abs(selection_pos.x - pos.x) + 1, - math.abs(selection_pos.y - pos.y) + 1, - math.abs(selection_pos.z - pos.z) + 1 +-- TODO: reuse data in quickfort database +local function get_selection_size_limits() + local btype = uibs.building_type + if btype == df.building_type.Bridge + or btype == df.building_type.FarmPlot + or btype == df.building_type.RoadPaved + or btype == df.building_type.RoadDirt then + return {w=31, h=31} + elseif btype == df.building_type.AxleHorizontal then + return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} + elseif btype == df.building_type.Rollers then + return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} + end +end + +local function get_selected_bounds(selection_pos, pos) + selection_pos = selection_pos or uibs.selection_pos + if not is_choosing_area() then return end + + pos = pos or uibs.pos + + local bounds = { + x1=math.min(selection_pos.x, pos.x), + x2=math.max(selection_pos.x, pos.x), + y1=math.min(selection_pos.y, pos.y), + y2=math.max(selection_pos.y, pos.y), + z1=math.min(selection_pos.z, pos.z), + z2=math.max(selection_pos.z, pos.z), + } + + -- clamp to map edges + bounds = { + x1=math.max(0, bounds.x1), + x2=math.min(df.global.world.map.x_count-1, bounds.x2), + y1=math.max(0, bounds.y1), + y2=math.min(df.global.world.map.y_count-1, bounds.y2), + z1=math.max(0, bounds.z1), + z2=math.min(df.global.world.map.z_count-1, bounds.z2), + } + + local limits = get_selection_size_limits() + if limits then + -- clamp to building type area limit + bounds = { + x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), + x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), + y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), + y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), + z1=bounds.z1, + z2=bounds.z2, + } + end + + return bounds +end + +local function get_cur_area_dims(bounds) + if not bounds and not is_choosing_area() then return 1, 1, 1 end + bounds = bounds or get_selected_bounds() + if not bounds then return 1, 1, 1 end + return bounds.x2 - bounds.x1 + 1, + bounds.y2 - bounds.y1 + 1, + bounds.z2 - bounds.z1 + 1 end local function is_pressure_plate() @@ -53,7 +112,8 @@ end -- adjusted from CycleHotkeyLabel on the planner panel local weapon_quantity = 1 -local function get_quantity(filter, hollow, placement_data) +-- TODO: this should account for erroring constructions +local function get_quantity(filter, hollow, bounds) if is_pressure_plate() then local flags = uibs.plate_info.flags return (flags.units and 1 or 0) + (flags.water and 1 or 0) + @@ -62,7 +122,7 @@ local function get_quantity(filter, hollow, placement_data) return weapon_quantity end local quantity = filter.quantity or 1 - local dimx, dimy, dimz = get_cur_area_dims(placement_data) + local dimx, dimy, dimz = get_cur_area_dims(bounds) if quantity < 1 then return (((dimx * dimy) // 4) + 1) * dimz end @@ -85,8 +145,15 @@ local function is_construction() return uibs.building_type == df.building_type.Construction end +local function is_tutorial_open() + local help = df.global.game.main_interface.help + return help.open and + help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS +end + local function is_plannable() - return get_cur_filters() and + return not is_tutorial_open() and + get_cur_filters() and not (is_construction() and uibs.building_subtype == df.construction_type.TrackNSEW) end @@ -152,40 +219,47 @@ ItemLine.ATTRS{ } function ItemLine:init() - self.frame.h = 1 + self.frame.h = 2 self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ + view_id='item_symbol', frame={t=0, l=0}, - text='*', + text=string.char(16), -- this is the "â–º" character + text_pen=COLOR_YELLOW, auto_width=true, visible=self.is_selected_fn, }, widgets.Label{ - frame={t=0, l=25}, + view_id='item_desc', + frame={t=0, l=2}, text={ - {tile=pens.BUTTON_START_PEN}, - {gap=6, tile=pens.BUTTON_END_PEN}, + {text=self:callback('get_item_line_text'), + pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, }, - auto_width=true, - on_click=function() self.on_filter(self.idx) end, }, widgets.Label{ - frame={t=0, l=33}, + view_id='item_filter', + frame={t=0, l=28}, text={ - {tile=pens.BUTTON_START_PEN}, - {gap=1, tile=pens.BUTTON_END_PEN}, + {text=self:callback('get_filter_text'), + pen=function() return gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, }, auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=42}, + text='[clear]', + text_pen=COLOR_LIGHTRED, + auto_width=true, + visible=self:callback('has_filter'), on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ - frame={t=0, l=2}, + frame={t=1, l=2}, text={ - {width=21, text=self:callback('get_item_line_text')}, - {gap=3, text='filter', pen=COLOR_GREEN}, - {gap=2, text='x', pen=self:callback('get_x_pen')}, - {gap=3, text=function() return self.note end, + {gap=2, text=function() return self.note end, pen=function() return self.note_pen end}, }, }, @@ -204,11 +278,6 @@ function ItemLine:onInput(keys) return ItemLine.super.onInput(self, keys) end -function ItemLine:get_x_pen() - return require('plugins.buildingplan').hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) and - COLOR_GREEN or COLOR_GREY -end - function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] @@ -221,15 +290,26 @@ function ItemLine:get_item_line_text() uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) if self.available >= quantity then self.note_pen = COLOR_GREEN - self.note = 'Available now' + self.note = ' Available now' else - self.note_pen = COLOR_YELLOW - self.note = 'Will link later' + self.note_pen = COLOR_BROWN + self.note = ' Will link later' end + self.note = string.char(192) .. self.note -- character 192 is "â””" return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end +function ItemLine:has_filter() + return require('plugins.buildingplan').hasFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) +end + +function ItemLine:get_filter_text() + -- TODO: make this show the filter's materials instead of "edit filters" + return self:has_filter() and '[edit filters]' or '[any material]' +end + function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] @@ -255,40 +335,41 @@ PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=56, h=20}, + frame={w=56, h=22}, } function PlannerOverlay:init() self.selected = 1 - self.minimized = false + self.state = ensure_key(config.data, 'planner') local main_panel = widgets.Panel{ view_id='main', - frame={t=0, l=0, r=0, h=14}, - frame_style=gui.MEDIUM_FRAME, + frame={t=1, l=0, r=0, h=14}, + frame_style=gui.INTERIOR_MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, - visible=function() return not self.minimized end, + visible=self:callback('is_not_minimized'), } local minimized_panel = widgets.Panel{ - frame={t=0, r=0, w=4, h=1}, + frame={t=0, r=1, w=17, h=1}, subviews={ widgets.Label{ - frame={t=0, l=0, w=1, h=1}, - text={{tile=pens.MINIMIZED_LEFT_PEN}}, - visible=function() return self.minimized end, - }, - widgets.Label{ - frame={t=0, l=1, w=2, h=1}, - text=string.char(31)..string.char(30), - text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_GREY}, - text_hpen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE}, - on_click=function() self.minimized = not self.minimized end, + frame={t=0, r=0, h=1}, + text={ + {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, + {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, + }, + visible=self:callback('is_minimized'), + on_click=self:callback('toggle_minimized'), }, widgets.Label{ - frame={t=0, r=0, w=1, h=1}, - text={{tile=pens.MINIMIZED_RIGHT_PEN}}, - visible=function() return self.minimized end, + frame={t=0, r=0, h=1}, + text={ + {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, + {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, + }, + visible=self:callback('is_not_minimized'), + on_click=self:callback('toggle_minimized'), }, }, } @@ -332,18 +413,18 @@ function PlannerOverlay:init() on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', - frame={t=3, l=4}, + frame={b=4, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, options={ {label='No', value=false}, - {label='Yes', value=true}, + {label='Yes', value=true, pen=COLOR_GREEN}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={t=4, l=4}, + frame={b=5, l=23, w=30}, key='CUSTOM_R', label='Top Stair Type: ', visible=is_stairs, @@ -355,7 +436,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={t=5, l=4}, + frame={b=4, l=23, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', visible=is_stairs, @@ -365,19 +446,30 @@ function PlannerOverlay:init() {label='Up', value=df.construction_type.UpStair}, }, }, - widgets.CycleHotkeyLabel { + widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider view_id='weapons', - frame={t=5, l=4}, + frame={b=4, l=1, w=28}, key='CUSTOM_T', key_back='CUSTOM_SHIFT_T', - label='Num weapons:', + label='Number of weapons:', visible=is_weapon_or_spike_trap, - options={1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + options={ + {label='(1)', value=1, pen=COLOR_YELLOW}, + {label='(2)', value=2, pen=COLOR_YELLOW}, + {label='(3)', value=3, pen=COLOR_YELLOW}, + {label='(4)', value=4, pen=COLOR_YELLOW}, + {label='(5)', value=5, pen=COLOR_YELLOW}, + {label='(6)', value=6, pen=COLOR_YELLOW}, + {label='(7)', value=7, pen=COLOR_YELLOW}, + {label='(8)', value=8, pen=COLOR_YELLOW}, + {label='(9)', value=9, pen=COLOR_YELLOW}, + {label='(10)', value=10, pen=COLOR_YELLOW}, + }, on_change=function(val) weapon_quantity = val end, }, widgets.ToggleHotkeyLabel { view_id='engraved', - frame={t=5, l=4}, + frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, @@ -386,7 +478,8 @@ function PlannerOverlay:init() end, }, widgets.Label{ - frame={b=3, l=17}, + frame={b=2, l=23}, + text_pen=COLOR_DARKGREY, text={ 'Selected area: ', {text=function() @@ -402,32 +495,18 @@ function PlannerOverlay:init() visible=function() return #get_cur_filters() > 0 end, subviews={ widgets.HotkeyLabel{ - frame={b=1, l=0}, - key='STRING_A042', - auto_width=true, - enabled=function() return #get_cur_filters() > 1 end, - on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, - }, - widgets.HotkeyLabel{ - frame={b=1, l=1}, - key='STRING_A047', - label='Prev/next item', - auto_width=true, - enabled=function() return #get_cur_filters() > 1 end, - on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, - }, - widgets.HotkeyLabel{ - frame={b=1, l=21}, + frame={b=1, l=1, w=22}, key='CUSTOM_F', - label='Set filter', - auto_width=true, + label=function() + return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + and 'Edit filter' or 'Set filter' + end, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ - frame={b=1, l=37}, + frame={b=0, l=1, w=22}, key='CUSTOM_X', label='Clear filter', - auto_width=true, on_activate=function() self:clear_filter(self.selected) end, enabled=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) @@ -435,26 +514,29 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='choose', - frame={b=0, l=0, w=25}, - key='CUSTOM_I', - label='Choose from items:', - options={{label='Yes', value=true}, - {label='No', value=false}}, - initial_option=false, - enabled=function() - for idx = 1,4 do - if (self.subviews['item'..idx].available or 0) > 0 then - return true - end - end - end, + frame={b=0, l=23}, + key='CUSTOM_Z', + label='Choose items:', + label_below=true, + options={ + {label='With filters', value=0}, + { + label=function() + local automaterial = itemselection.get_automaterial_selection(uibs.building_type) + return ('Last used (%s)'):format(automaterial or 'pick manually') + end, + value=2, + }, + {label='Manually', value=1}, + }, + initial_option=0, on_change=function(choose) buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) end, }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=29, w=25}, + frame={b=2, l=23, w=25}, key='CUSTOM_G', label='Building safety:', options={ @@ -472,36 +554,107 @@ function PlannerOverlay:init() }, } + local divider_widget = widgets.Panel{ + view_id='divider', + frame={t=10, l=0, r=0, h=1}, + on_render=self:callback('draw_divider_h'), + visible=self:callback('is_not_minimized'), + } + local error_panel = widgets.ResizingPanel{ view_id='errors', - frame={t=14, l=0, r=0}, - frame_style=gui.MEDIUM_FRAME, + frame={t=15, l=0, r=0}, + frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, - visible=function() return not self.minimized end, + visible=self:callback('is_not_minimized'), } error_panel:addviews{ widgets.WrappedLabel{ - frame={t=0, l=0, r=0}, + frame={t=0, l=1, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, visible=function() return #uibs.errors > 0 end, }, widgets.Label{ - frame={t=0, l=0, r=0}, + frame={t=0, l=1, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, }, } + local prev_next_selector = widgets.Panel{ + frame={h=1}, + auto_width=true, + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=1, w=9}, + key='CUSTOM_SHIFT_Q', + key_sep='\0', + label=': Prev/', + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={t=0, l=2, w=1}, + key='CUSTOM_Q', + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + widgets.Label{ + frame={t=0,l=10}, + text='next item', + on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + }, + visible=function() return #get_cur_filters() > 1 end, + } + + local black_bar = widgets.Panel{ + frame={t=0, l=1, w=37, h=1}, + frame_inset=0, + frame_background=gui.CLEAR_PEN, + visible=self:callback('is_not_minimized'), + subviews={ + prev_next_selector, + }, + } + self:addviews{ - main_panel, + black_bar, minimized_panel, + main_panel, + divider_widget, error_panel, } end +function PlannerOverlay:is_minimized() + return self.state.minimized +end + +function PlannerOverlay:is_not_minimized() + return not self.state.minimized +end + +function PlannerOverlay:toggle_minimized() + self.state.minimized = not self.state.minimized + config:write() +end + +function PlannerOverlay:draw_divider_h(dc) + local x2 = dc.width -1 + for x=0,x2 do + dc:seek(x, 0) + if x == 0 then + dc:char(nil, pens.HORI_LEFT_PEN) + elseif x == x2 then + dc:char(nil, pens.HORI_RIGHT_PEN) + else + dc:char(nil, pens.HORI_MID_PEN) + end + end +end + function PlannerOverlay:reset() self.subviews.item1:reset() self.subviews.item2:reset() @@ -519,19 +672,18 @@ function PlannerOverlay:clear_filter(idx) end local function get_placement_data() - local pos = uibs.pos local direction = uibs.direction - local width, height, depth = get_cur_area_dims() + local bounds = get_selected_bounds() + local width, height, depth = get_cur_area_dims(bounds) local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) -- get the upper-left corner of the building/area at min z-level - local has_selection = is_choosing_area() - local start_pos = xyz2pos( - has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, - has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, - has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z - ) + local start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or + xyz2pos( + uibs.pos.x - adjusted_width//2, + uibs.pos.y - adjusted_height//2, + uibs.pos.z) if uibs.building_type == df.building_type.ScrewPump then if direction == df.screw_pump_direction.FromSouth then start_pos.y = start_pos.y + 1 @@ -546,11 +698,11 @@ local function get_placement_data() and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, pos.z) + max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end return { - p1=xyz2pos(min_x, min_y, min_z), - p2=xyz2pos(max_x, max_y, max_z), + x1=min_x, y1=min_y, z1=min_z, + x2=max_x, y2=max_y, z2=max_z, width=adjusted_width, height=adjusted_height } @@ -564,10 +716,11 @@ function PlannerOverlay:save_placement() self.saved_pos = copyall(uibs.pos) uibs.selection_pos:clear() else - self.saved_selection_pos = copyall(self.saved_placement.p1) - self.saved_pos = copyall(self.saved_placement.p2) - self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1 - self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1 + local sp = self.saved_placement + self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) + self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) + self.saved_pos.x = self.saved_pos.x + sp.width - 1 + self.saved_pos.y = self.saved_pos.y + sp.height - 1 end end @@ -593,20 +746,19 @@ function PlannerOverlay:onInput(keys) return true end self.selected = 1 - self.minimized = false self.subviews.hollow:setOption(false) self:reset() reset_counts_flag = true return false end if keys.CUSTOM_ALT_M then - self.minimized = not self.minimized + self:toggle_minimized() return true end if PlannerOverlay.super.onInput(self, keys) then return true end - if self.minimized then return false end + if self:is_minimized() then return false end if keys._MOUSE_L_DOWN then if is_over_options_panel() then return false end local detect_rect = copyall(self.frame_rect) @@ -622,42 +774,50 @@ function PlannerOverlay:onInput(keys) if is_choosing_area() or cur_building_has_no_area() then local filters = get_cur_filters() local num_filters = #filters - local choose = self.subviews.choose - if choose.enabled() and choose:getOptionValue() then + local choose = self.subviews.choose:getOptionValue() + if choose > 0 then + local bounds = get_selected_bounds() self:save_placement() + local autoselect = choose == 2 local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} - if (self.subviews['item'..idx].available or 0) > 0 then - local filter = filters[idx] - active_screens[idx] = itemselection.ItemSelectionScreen{ - index=idx, - desc=require('plugins.buildingplan').get_desc(filter), - quantity=get_quantity(filter, is_hollow, - self.saved_placement), - on_submit=function(items) - chosen_items[idx] = items + local filter = filters[idx] + local selection_screen = itemselection.ItemSelectionScreen{ + index=idx, + desc=require('plugins.buildingplan').get_desc(filter), + quantity=get_quantity(filter, is_hollow, bounds), + autoselect=autoselect, + on_submit=function(items) + chosen_items[idx] = items + if active_screens[idx] then active_screens[idx]:dismiss() active_screens[idx] = nil - pending = pending - 1 - if pending == 0 then - df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT - self:place_building(self:restore_placement(), chosen_items) - end - end, - on_cancel=function() - for i,scr in pairs(active_screens) do - scr:dismiss() - end + else + active_screens[idx] = true + end + pending = pending - 1 + if pending == 0 then df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT - self:restore_placement() - end, - }:show() + self:place_building(self:restore_placement(), chosen_items) + end + end, + on_cancel=function() + for i,scr in pairs(active_screens) do + scr:dismiss() + end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:restore_placement() + end, + } + if active_screens[idx] then + -- we've already returned via autoselect + active_screens[idx] = nil else - pending = pending - 1 + active_screens[idx] = selection_screen:show() end end else @@ -694,16 +854,10 @@ function PlannerOverlay:onRenderFrame(dc, rect) uibs.building_type, uibs.building_subtype, uibs.custom_type)) end - local selection_pos = self.saved_selection_pos or uibs.selection_pos - if not selection_pos or selection_pos.x < 0 then return end + if self:is_minimized() then return end - local pos = self.saved_pos or uibs.pos - local bounds = { - x1 = math.max(0, math.min(selection_pos.x, pos.x)), - x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)), - y1 = math.max(0, math.min(selection_pos.y, pos.y)), - y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)), - } + local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) + if not bounds then return end local hollow = self.subviews.hollow:getOptionValue() local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN @@ -726,25 +880,25 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) +function PlannerOverlay:get_stairs_subtype(pos, bounds) local subtype = uibs.building_subtype - if pos.z == corner1.z then + if pos.z == bounds.z1 then local opt = self.subviews.stairs_bottom_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape - if shape ~= df.tiletype_shape.STAIR_DOWN then + if shape ~= df.tiletype_shape.STAIR_DOWN and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.UpStair end else subtype = opt end - elseif pos.z == corner2.z then + elseif pos.z == bounds.z2 then local opt = self.subviews.stairs_top_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape - if shape ~= df.tiletype_shape.STAIR_UP then + if shape ~= df.tiletype_shape.STAIR_UP and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.DownStair end else @@ -755,7 +909,7 @@ function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) end function PlannerOverlay:place_building(placement_data, chosen_items) - local p1, p2 = placement_data.p1, placement_data.p2 + local pd = placement_data local blds = {} local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype @@ -765,17 +919,17 @@ function PlannerOverlay:place_building(placement_data, chosen_items) elseif is_weapon_trap() then filters[2].quantity = get_quantity(filters[2]) end - for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do - if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then + for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do + if hollow and x ~= pd.x1 and x ~= pd.x2 and y ~= pd.y1 and y ~= pd.y2 then goto continue end local pos = xyz2pos(x, y, z) if is_stairs() then - subtype = self:get_stairs_subtype(pos, p1, p2) + subtype = self:get_stairs_subtype(pos, pd) end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, - width=placement_data.width, height=placement_data.height, + width=pd.width, height=pd.height, direction=uibs.direction, filters=filters} if err then -- it's ok if some buildings fail to build diff --git a/plugins/lua/confirm.lua b/plugins/lua/confirm.lua index 13964645b..1ddf190b4 100644 --- a/plugins/lua/confirm.lua +++ b/plugins/lua/confirm.lua @@ -12,10 +12,11 @@ setmetatable(keys, { -- Mouse keys will be sent as a string instead of interface_key local MOUSE_LEFT = "MOUSE_LEFT" local MOUSE_RIGHT = "MOUSE_RIGHT" + --[[ The screen where a confirmation has been triggered Note that this is *not* necessarily the topmost viewscreen, so do not use gui.getCurViewscreen() or related functions. ]] -screen = nil +--screen = nil function if_nil(obj, default) if obj == nil then @@ -118,7 +119,9 @@ zone_remove.message = "Are you sure you want to remove this zone?" burrow_remove = defconf('burrow-remove') function burrow_remove.intercept_key(key) - return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 171 + return key == MOUSE_LEFT and + (df.global.game.main_interface.current_hover == 171 or + df.global.game.main_interface.current_hover == 168) end burrow_remove.title = "Remove burrow" burrow_remove.message = "Are you sure you want to remove this burrow?" diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 0eb09d244..fca0f1f27 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,6 +5,19 @@ local helpdb = require('helpdb') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') +local function get_command(cmdline) + local first_word = cmdline:trim():split(' +')[1] + if first_word:startswith(':') then first_word = first_word:sub(2) end + return first_word +end + +function should_hide_armok(cmdline) + local command = get_command(cmdline) + return dfhack.getHideArmokTools() and + helpdb.is_entry(command) and + helpdb.get_entry_tags(command).armok +end + -- ----------------- -- -- HotspotMenuWidget -- -- ----------------- -- @@ -26,7 +39,7 @@ HotspotMenuWidget.ATTRS{ -- 'new_region', -- conflicts with vanilla panel layouts 'savegame', 'setupdwarfgame', - 'title', + 'title/Default', 'update_region', 'world' }, @@ -106,8 +119,8 @@ local function get_bindings_to_hotkeys(hotkeys, bindings) return bindings_to_hotkeys end --- number of non-text tiles: icon, space, space between cmd and hk, scrollbar -local LIST_BUFFER = 2 + 1 + 1 +-- number of non-text tiles: icon, space between cmd and hk, scrollbar+margin +local LIST_BUFFER = 2 + 1 + 3 local function get_choices(hotkeys, bindings, is_inverted) local choices, max_width, seen = {}, 0, {} @@ -143,7 +156,7 @@ local function get_choices(hotkeys, bindings, is_inverted) -- adjust width of command fields so the hotkey tokens are right justified for _,choice in ipairs(choices) do local command_token = choice.text[1] - command_token.width = max_width - choice.hk_width - 3 + command_token.width = max_width - choice.hk_width - (LIST_BUFFER - 1) end return choices, max_width @@ -232,10 +245,9 @@ end 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 - self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and - helpdb.get_entry_short_help(first_word) or 'Command not found' + local command = get_command(choice.command) + self.subviews.help.text_to_wrap = helpdb.is_entry(command) and + helpdb.get_entry_short_help(command) or 'Command not found' self.subviews.help_panel:updateLayout() end diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 72bb6185e..50197fce4 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -19,15 +19,37 @@ local function do_clear() function() dfhack.run_command('orders', 'clear') end) end +local function get_import_choices() + return dfhack.run_command_silent('orders', 'list'):split('\n') +end + local function do_import() - local output = dfhack.run_command_silent('orders', 'list') - dialogs.ListBox{ - frame_title='Import Manager Orders', + local dlg + local function get_dlg() return dlg end + dlg = dialogs.ListBox{ + frame_title='Import/Delete Manager Orders', with_filter=true, - choices=output:split('\n'), - on_select=function(idx, choice) + choices=get_import_choices(), + on_select=function(_, choice) dfhack.run_command('orders', 'import', choice.text) end, + dismiss_on_select2=false, + on_select2=function(_, choice) + if choice.text:startswith('library/') then return end + local fname = 'dfhack-config/orders/'..choice.text..'.json' + if not dfhack.filesystem.isfile(fname) then return end + dialogs.showYesNoPrompt('Delete orders file?', + 'Are you sure you want to delete "' .. fname .. '"?', nil, + function() + print('deleting ' .. fname) + os.remove(fname) + local list = get_dlg().subviews.list + local filter = list:getFilter() + list:setChoices(get_import_choices(), list:getSelected()) + list:setFilter(filter) + end) + end, + select2_hint='Delete file', }:show() end @@ -46,37 +68,87 @@ OrdersOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS', frame={w=30, h=4}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, } function OrdersOverlay:init() - self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, - label='import', - key='CUSTOM_CTRL_I', - on_activate=do_import, - }, - widgets.HotkeyLabel{ - frame={t=1, l=0}, - label='export', - key='CUSTOM_CTRL_E', - on_activate=do_export, - }, - widgets.HotkeyLabel{ - frame={t=0, l=15}, - label='sort', - key='CUSTOM_CTRL_O', - on_activate=do_sort, + self.minimized = false + + local main_panel = widgets.Panel{ + frame={t=0, l=0, r=0, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + visible=function() return not self.minimized end, + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='import', + key='CUSTOM_CTRL_I', + auto_width=true, + on_activate=do_import, + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='export', + key='CUSTOM_CTRL_E', + auto_width=true, + on_activate=do_export, + }, + widgets.HotkeyLabel{ + frame={t=0, l=15}, + label='sort', + key='CUSTOM_CTRL_O', + auto_width=true, + on_activate=do_sort, + }, + widgets.HotkeyLabel{ + frame={t=1, l=15}, + label='clear', + key='CUSTOM_CTRL_C', + auto_width=true, + on_activate=do_clear, + }, }, - widgets.HotkeyLabel{ - frame={t=1, l=15}, - label='clear', - key='CUSTOM_CTRL_C', - on_activate=do_clear, + } + + local minimized_panel = widgets.Panel{ + frame={t=0, r=0, w=3, h=1}, + subviews={ + widgets.Label{ + frame={t=0, l=0, w=1, h=1}, + text='[', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, + widgets.Label{ + frame={t=0, l=1, w=1, h=1}, + text={{text=function() return self.minimized and string.char(31) or string.char(30) end}}, + text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_GREY}, + text_hpen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE}, + on_click=function() self.minimized = not self.minimized end, + }, + widgets.Label{ + frame={t=0, r=0, w=1, h=1}, + text=']', + text_pen=COLOR_RED, + visible=function() return self.minimized end, + }, }, } + + self:addviews{ + main_panel, + minimized_panel, + } +end + +function OrdersOverlay:onInput(keys) + if keys.CUSTOM_ALT_M then + self.minimized = not self.minimized + return true + end + if OrdersOverlay.super.onInput(self, keys) then + return true + end end OVERLAY_WIDGETS = { diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 3d476bf0d..b1960989f 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -8,6 +8,7 @@ local widgets = require('gui.widgets') local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' +local GLOBAL_KEY = 'OVERLAY' local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2 @@ -311,6 +312,14 @@ function reload() reposition_widgets() end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc ~= SC_WORLD_LOADED then + return + end + -- pick up widgets from active mods + reload() +end + local function dump_widget_config(name, widget) local pos = overlay_config[name].pos print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y)) @@ -501,7 +510,7 @@ end local function _render_viewscreen_widgets(vs_name, dc) local vs_widgets = active_viewscreen_widgets[vs_name] - if not vs_widgets then return false end + if not vs_widgets then return end dc = dc or gui.Painter.new() for _,db_entry in pairs(vs_widgets) do local w = db_entry.widget @@ -509,11 +518,18 @@ local function _render_viewscreen_widgets(vs_name, dc) detect_frame_change(w, function() w:render(dc) end) end end + return dc end +local force_refresh + function render_viewscreen_widgets(vs_name) local dc = _render_viewscreen_widgets(vs_name, nil) _render_viewscreen_widgets('all', dc) + if force_refresh then + force_refresh = nil + df.global.gps.force_full_display_count = 1 + end end -- called when the DF window is resized @@ -522,6 +538,7 @@ function reposition_widgets() for _,db_entry in pairs(widget_db) do db_entry.widget:updateLayout(sr) end + force_refresh = true end -- ------------------------------------------------- -- @@ -553,4 +570,42 @@ function OverlayWidget:init() self.frame.h = self.frame.h or 1 end +-- ------------------- -- +-- TitleVersionOverlay -- +-- ------------------- -- + +TitleVersionOverlay = defclass(TitleVersionOverlay, OverlayWidget) +TitleVersionOverlay.ATTRS{ + default_pos={x=7, y=2}, + default_enabled=true, + viewscreens='title/Default', + frame={w=35, h=3}, +} + +function TitleVersionOverlay:init() + local text = {} + table.insert(text, 'DFHack ' .. dfhack.getDFHackVersion() .. + (dfhack.isRelease() and '' or (' (git: %s)'):format(dfhack.getGitCommit(true)))) + if #dfhack.getDFHackBuildID() > 0 then + table.insert(text, NEWLINE) + table.insert(text, 'Build ID: ' .. dfhack.getDFHackBuildID()) + end + if dfhack.isPrerelease() then + table.insert(text, NEWLINE) + table.insert(text, {text='Pre-release build', pen=COLOR_LIGHTRED}) + end + + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text=text, + text_pen=COLOR_WHITE, + }, + } +end + +OVERLAY_WIDGETS = { + title_version = TitleVersionOverlay, +} + return _ENV diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index ca8c28cd4..9e17cbe2b 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -1,244 +1,187 @@ local _ENV = mkmodule('plugins.stockpiles') ---[[ +local argparse = require('argparse') - Native functions: +local STOCKPILES_DIR = "dfhack-config/stockpiles"; +local STOCKPILES_LIBRARY_DIR = "hack/data/stockpiles"; - * stockpiles_list_settings(dir_path), list files in directory - * stockpiles_load(file), with full path - * stockpiles_save(file), with full path - * isEnabled() - ---]] --- -function safe_require(module) - local status, module = pcall(require, module) - return status and module or nil +local function get_sp_name(name, num) + if #name > 0 then return name end + return ('Stockpile %d'):format(num) end - -local gui = require 'gui' -local widgets = require('gui.widgets') -local dlg = require('gui.dialogs') -local script = require 'gui.script' -local persist = safe_require('persist-table') - - -function ListFilterDialog(args) - args.text = args.prompt or 'Type or select an option' - args.text_pen = COLOR_WHITE - args.with_filter = true - args.icon_width = 2 - - local choices = {} - - if not args.hide_none then - table.insert(choices, { - icon = '?', text = args.none_caption or 'none', - index = -1, name = -1 - }) +local STATUS_FMT = '%6s %s' +local function print_status() + local sps = df.global.world.buildings.other.STOCKPILE + print(('Current stockpiles: %d'):format(#sps)) + if #sps > 0 then + print() + print(STATUS_FMT:format('ID', 'Name')) + print(STATUS_FMT:format('------', '----------')) end - - local filter = args.item_filter - - for i,v in ipairs(args.items) do - if not filter or filter(v,-1) then - local name = v - local icon - table.insert(choices, { - icon = icon, search_key = string.lower(name), text = name, index = i - }) - end + for _,sp in ipairs(sps) do + print(STATUS_FMT:format(sp.id, get_sp_name(sp.name, sp.stockpile_number))) end +end - args.choices = choices - - if args.on_select then - local cb = args.on_select - args.on_select = function(idx, obj) - return cb(obj.index, args.items[obj.index]) +local function list_dir(path, prefix, filters) + local paths = dfhack.filesystem.listdir_recursive(path, 0, false) + if not paths then + dfhack.printerr(('Cannot find stockpile settings directory: "%s"'):format(path)) + return + end + local normalized_filters = {} + for _,filter in ipairs(filters or {}) do + table.insert(normalized_filters, filter:lower()) + end + for _,v in ipairs(paths) do + local normalized_path = prefix .. v.path:lower() + if v.isdir or not normalized_path:endswith('.dfstock') then goto continue end + normalized_path = normalized_path:sub(1, -9) + if #normalized_filters > 0 then + local matched = false + for _,filter in ipairs(normalized_filters) do + if normalized_path:find(filter, 1, true) then + matched = true + break + end + end + if not matched then goto continue end end + print(('%s%s'):format(prefix, v.path:sub(1, -9))) + ::continue:: end - - return dlg.ListBox(args) end -function showFilterPrompt(title, list, text,item_filter,hide_none) - ListFilterDialog{ - frame_title=title, - items=list, - prompt=text, - item_filter=item_filter, - hide_none=hide_none, - on_select=script.mkresume(true), - on_cancel=script.mkresume(false), - on_close=script.qresume(nil) - }:show() - - return script.wait() +local function list_settings_files(filters) + list_dir(STOCKPILES_DIR, '', filters) + list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters) end -function init() - if persist == nil then return end - if dfhack.isMapLoaded() then - if persist.GlobalTable.stockpiles == nil then - persist.GlobalTable.stockpiles = {} - persist.GlobalTable.stockpiles['settings_path'] = './stocksettings' - end +local function assert_safe_name(name) + if not name or #name == 0 then + qerror('name missing or empty') + end + if name:find('[^%w._]') then + qerror('name can only contain numbers, letters, periods, and underscores') end end -function tablify(iterableObject) - t={} - for k,v in ipairs(iterableObject) do - t[k] = v~=nil and v or 'nil' - end - return t +local function get_sp_id(opts) + if opts.id then return opts.id end + local sp = dfhack.gui.getSelectedStockpile() + if sp then return sp.id end + return nil end -local filename_invalid_regex = '[^A-Za-z0-9 ._-]' +local included_elements = { + containers=1, + general=2, + categories=4, + types=8, +} -function valid_filename(filename) - return not filename:match(filename_invalid_regex) -end +local function export_stockpile(name, opts) + assert_safe_name(name) + name = STOCKPILES_DIR .. '/' .. name -function sanitize_filename(filename) - local ret = '' - for i = 1, #filename do - local ch = filename:sub(i, i) - if valid_filename(ch) then - ret = ret .. ch - else - ret = ret .. '-' + local includedElements = 0 + for _,inc in ipairs(opts.includes) do + if included_elements[inc] then + includedElements = includedElements | included_elements[inc] end end - return ret -end -FilenameInputBox = defclass(FilenameInputBox, dlg.InputBox) -function FilenameInputBox:onInput(keys) - if not valid_filename(string.char(keys._STRING or 0)) and not keys.STRING_A000 then - keys._STRING = nil + if includedElements == 0 then + for _,v in pairs(included_elements) do + includedElements = includedElements | v + end end - FilenameInputBox.super.onInput(self, keys) -end -function showFilenameInputPrompt(title, text, tcolor, input, min_width) - FilenameInputBox{ - frame_title = title, - text = text, - text_pen = tcolor, - input = input, - frame_width = min_width, - on_input = script.mkresume(true), - on_cancel = script.mkresume(false), - on_close = script.qresume(nil) - }:show() - - return script.wait() + stockpiles_export(name, get_sp_id(opts), includedElements) end -function load_settings() - init() - local path = get_path() - local ok, list = pcall(stockpiles_list_settings, path) - if not ok then - show_message_box("Stockpile Settings", "The stockpile settings folder doesn't exist.", true) - return +local function import_stockpile(name, opts) + local is_library = false + if name:startswith('library/') then + name = name:sub(9) + is_library = true end - if #list == 0 then - show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true) - return + assert_safe_name(name) + if not is_library and dfhack.filesystem.exists(STOCKPILES_DIR .. '/' .. name .. '.dfstock') then + name = STOCKPILES_DIR .. '/' .. name + else + name = STOCKPILES_LIBRARY_DIR .. '/' .. name end + stockpiles_import(name, get_sp_id(opts), opts.mode, table.concat(opts.filters, ',')) +end - local choice_list = {} - for i,v in ipairs(list) do - choice_list[i] = string.gsub(v, "/", "/ ") - choice_list[i] = string.gsub(choice_list[i], "-", " - ") - choice_list[i] = string.gsub(choice_list[i], "_", " ") - end +local valid_includes = {general=true, categories=true, types=true} - script.start(function() - local ok2,index,name=showFilterPrompt('Stockpile Settings', choice_list, 'Choose a stockpile', function(item) return true end, true) - if ok2 then - local filename = list[index]; - stockpiles_load(path..'/'..filename) +local function parse_include(arg) + local includes = argparse.stringList(arg, 'include') + for _,v in ipairs(includes) do + if not valid_includes[v] then + qerror(('invalid included element: "%s"'):format(v)) end - end) + end + return includes end -function save_settings(stockpile) - init() - script.start(function() - local suggested = stockpile.name - if #suggested == 0 then - suggested = 'Stock1' - end - suggested = sanitize_filename(suggested) - local path = get_path() - local sok,filename = showFilenameInputPrompt('Stockpile Settings', 'Enter filename', COLOR_WHITE, suggested) - if sok then - if filename == nil or filename == '' or not valid_filename(filename) then - script.showMessage('Stockpile Settings', 'Invalid File Name', COLOR_RED) - else - if not dfhack.filesystem.exists(path) then - dfhack.filesystem.mkdir(path) - end - stockpiles_save(path..'/'..filename) - end - end - end) -end +local valid_modes = {set=true, enable=true, disable=true} -function manage_settings(sp) - init() - if not guard() then return false end - script.start(function() - local list = {'Load', 'Save'} - local tok,i = script.showListPrompt('Stockpile Settings','Load or Save Settings?',COLOR_WHITE,tablify(list)) - if tok then - if i == 1 then - load_settings() - else - save_settings(sp) - end - end - end) +local function parse_mode(arg) + if not valid_modes[arg] then + qerror(('invalid mode: "%s"'):format(arg)) + end + return arg end -function show_message_box(title, msg, iserror) - local color = COLOR_WHITE - if iserror then - color = COLOR_RED +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return end - script.start(function() - script.showMessage(title, msg, color) - end) + + opts.includes = {} + opts.mode = 'set' + opts.filters = {} + + return argparse.processArgsGetopt(args, { + {'f', 'filter', hasArg=true, + handler=function(arg) opts.filters = argparse.stringList(arg) end}, + {'h', 'help', handler=function() opts.help = true end}, + {'i', 'include', hasArg=true, + handler=function(arg) opts.includes = parse_include(arg) end}, + {'m', 'mode', hasArg=true, + handler=function(arg) opts.mode = parse_mode(arg) end}, + {'s', 'stockpile', hasArg=true, + handler=function(arg) opts.id = argparse.nonnegativeInt(arg, 'stockpile') end}, + }) end -function guard() - if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Stockpile') then - qerror("This script requires a stockpile selected in the 'q' mode") +function parse_commandline(args) + local opts = {} + local positionals = process_args(opts, args) + + if opts.help or not positionals then return false end - return true -end -function set_path(path) - init() - if persist == nil then - qerror("This version of DFHack doesn't support setting the stockpile settings path. Sorry.") - return + local command = table.remove(positionals, 1) + if not command or command == 'status' then + print_status() + elseif command == 'list' then + list_settings_files(positionals) + elseif command == 'export' then + export_stockpile(positionals[1], opts) + elseif command == 'import' then + import_stockpile(positionals[1], opts) + else + return false end - persist.GlobalTable.stockpiles['settings_path'] = path -end -function get_path() - init() - if persist == nil then - return "stocksettings" - end - return persist.GlobalTable.stockpiles['settings_path'] + return true end return _ENV diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua index e9a88bfe1..46b3526d9 100644 --- a/plugins/lua/tailor.lua +++ b/plugins/lua/tailor.lua @@ -32,6 +32,11 @@ function setMaterials(names) idxs.adamantine or -1) end +function setDebugMode(opt) + local fl = (opt[1] == "true" or opt[1] == "on") + tailor_setDebugFlag(fl) +end + function parse_commandline(...) local args, opts = {...}, {} local positionals = process_args(opts, args) @@ -47,6 +52,8 @@ function parse_commandline(...) tailor_doCycle() elseif command == 'materials' then setMaterials(positionals) + elseif command == 'debugging' then + setDebugMode(positionals) else return false end diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index e75c967dc..d660c98b5 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -168,9 +168,11 @@ static void printMatdata(color_ostream &con, const matdata &data, bool only_z = con << std::setw(9) << int(data.count); if(data.lower_z != data.upper_z) - con <<" Z:" << std::setw(4) << data.lower_z << ".." << data.upper_z << std::endl; + con <<" Elev:" << std::setw(4) << (data.lower_z) << ".." << (data.upper_z) + << std::endl; else - con <<" Z:" << std::setw(4) << data.lower_z << std::endl; + con <<" Elev:" << std::setw(4) << (data.lower_z) + << std::endl; } static int getValue(const df::inorganic_raw &info) @@ -565,6 +567,10 @@ static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen, const prospect_options &options) { + out.printerr("prospector at embark is not currently available.\n"); + return CR_FAILURE; + +/* if (!world || !world->world_data) { out.printerr("World data is not available.\n"); @@ -621,6 +627,7 @@ static command_result embark_prospector(color_ostream &out, out << std::endl << "Warning: the above data is only a very rough estimate." << std::endl; return CR_OK; +*/ } static command_result map_prospector(color_ostream &con, @@ -671,7 +678,8 @@ static command_result map_prospector(color_ostream &con, b->GetGlobalFeature(&blockFeatureGlobal); b->GetLocalFeature(&blockFeatureLocal); - int global_z = world->map.region_z + z; + // the '- 100' is because DF v50 and later have a 100 offset in reported elevation + int global_z = world->map.region_z + z - 100; // Iterate over all the tiles in the block for(uint32_t y = 0; y < 16; y++) diff --git a/plugins/remotefortressreader/CMakeLists.txt b/plugins/remotefortressreader/CMakeLists.txt index 55525bc21..262d163f1 100644 --- a/plugins/remotefortressreader/CMakeLists.txt +++ b/plugins/remotefortressreader/CMakeLists.txt @@ -24,23 +24,9 @@ set(PROJECT_PROTO ui_sidebar_mode ) -set(PLUGIN_PROTOS) -foreach(pbuf ${PROJECT_PROTO}) - list(APPEND PLUGIN_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/../proto/${pbuf}.proto) -endforeach() - -string(REPLACE ".proto" ".pb.cc" PLUGIN_PROTO_SRCS "${PLUGIN_PROTOS}") -string(REPLACE ".proto" ".pb.h" PLUGIN_PROTO_HDRS "${PLUGIN_PROTOS}") -set_source_files_properties(${PLUGIN_PROTO_SRCS} ${PLUGIN_PROTO_HDRS} PROPERTIES GENERATED TRUE) - -set_source_files_properties( ${PROJECT_HDRS} ${PLUGIN_PROTO_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) - -# mash them together (headers are marked as headers and nothing will try to compile them) -list(APPEND PROJECT_SRCS ${PROJECT_HDRS} ${PLUGIN_PROTOS} ${PLUGIN_PROTO_SRCS} ${PLUGIN_PROTO_HDRS}) - if(UNIX AND NOT APPLE) set(PROJECT_LIBS ${PROJECT_LIBS} SDL) endif() # this makes sure all the stuff is put in proper places and linked to dfhack -dfhack_plugin(RemoteFortressReader ${PROJECT_SRCS} LINK_LIBRARIES protobuf-lite ${PROJECT_LIBS} COMPILE_FLAGS_MSVC "/FI\"Export.h\"" COMPILE_FLAGS_GCC "-include Export.h -Wno-misleading-indentation" ) +dfhack_plugin(RemoteFortressReader ${PROJECT_SRCS} LINK_LIBRARIES ${PROJECT_LIBS} PROTOBUFS ${PROJECT_PROTO}) diff --git a/plugins/remotefortressreader/proto/.gitignore b/plugins/remotefortressreader/proto/.gitignore new file mode 100644 index 000000000..befabf79d --- /dev/null +++ b/plugins/remotefortressreader/proto/.gitignore @@ -0,0 +1,3 @@ +*.pb.cc +*.pb.cc.rule +*.pb.h diff --git a/plugins/proto/AdventureControl.proto b/plugins/remotefortressreader/proto/AdventureControl.proto similarity index 100% rename from plugins/proto/AdventureControl.proto rename to plugins/remotefortressreader/proto/AdventureControl.proto diff --git a/plugins/proto/DwarfControl.proto b/plugins/remotefortressreader/proto/DwarfControl.proto similarity index 100% rename from plugins/proto/DwarfControl.proto rename to plugins/remotefortressreader/proto/DwarfControl.proto diff --git a/plugins/proto/ItemdefInstrument.proto b/plugins/remotefortressreader/proto/ItemdefInstrument.proto similarity index 100% rename from plugins/proto/ItemdefInstrument.proto rename to plugins/remotefortressreader/proto/ItemdefInstrument.proto diff --git a/plugins/proto/RemoteFortressReader.proto b/plugins/remotefortressreader/proto/RemoteFortressReader.proto similarity index 100% rename from plugins/proto/RemoteFortressReader.proto rename to plugins/remotefortressreader/proto/RemoteFortressReader.proto diff --git a/plugins/proto/ui_sidebar_mode.proto b/plugins/remotefortressreader/proto/ui_sidebar_mode.proto similarity index 100% rename from plugins/proto/ui_sidebar_mode.proto rename to plugins/remotefortressreader/proto/ui_sidebar_mode.proto diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index f33044877..061cf2698 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -76,7 +76,9 @@ #include "df/ocean_wave.h" #include "df/physical_attribute_type.h" #include "df/plant.h" +#include "df/plant_tree_tile.h" #include "df/plant_raw_flags.h" +#include "df/plant_root_tile.h" #include "df/projectile.h" #include "df/proj_itemst.h" #include "df/proj_unitst.h" @@ -971,17 +973,18 @@ void CopyBlock(df::map_block * DfBlock, RemoteFortressReader::MapBlock * NetBloc || yyy >= 16 ) continue; - df::plant_tree_tile tile; if (-localPos.z < 0) { - tile = tree_info->roots[-1 + localPos.z][xx + (yy*tree_info->dim_x)]; + df::plant_root_tile tile = tree_info->roots[-1 + localPos.z][xx + (yy * tree_info->dim_x)]; + if (!tile.whole || tile.bits.blocked) + continue; } else { - tile = tree_info->body[-localPos.z][xx + (yy*tree_info->dim_x)]; + df::plant_tree_tile tile = tree_info->body[-localPos.z][xx + (yy * tree_info->dim_x)]; + if (!tile.whole || tile.bits.blocked) + continue; } - if (!tile.whole || tile.bits.blocked) - continue; if (tree_info->body_height <= 1) trunk_percent[xxx][yyy] = 0; else diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 2367eddb5..a9e2210c9 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -21,7 +21,7 @@ namespace DFHack { */ void OrganicMatLookup::food_mat_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { - DEBUG(log).print("food_lookup: food_idx(%zd) ", food_idx); + DEBUG(log).print("food_lookup: food_idx(%zd)\n", food_idx); df::world_raws& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; @@ -31,11 +31,11 @@ void OrganicMatLookup::food_mat_by_idx(organic_mat_category::organic_mat_categor mat_category == organic_mat_category::Eggs) { food_mat.creature = raws.creatures.all[type]; food_mat.caste = food_mat.creature->caste[main_idx]; - DEBUG(log).print("special creature type(%d) caste(%d)", type, main_idx); + DEBUG(log).print("special creature type(%d) caste(%d)\n", type, main_idx); } else { food_mat.material.decode(type, main_idx); - DEBUG(log).print("type(%d) index(%d) token(%s)", type, main_idx, food_mat.material.getToken().c_str()); + DEBUG(log).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); } } std::string OrganicMatLookup::food_token_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx) { @@ -74,7 +74,7 @@ void OrganicMatLookup::food_build_map() { int16_t OrganicMatLookup::food_idx_by_token(organic_mat_category::organic_mat_category mat_category, const std::string& token) { df::world_raws& raws = world->raws; df::special_mat_table table = raws.mat_table; - DEBUG(log).print("food_idx_by_token: "); + DEBUG(log).print("food_idx_by_token:\n"); if (mat_category == organic_mat_category::Fish || mat_category == organic_mat_category::UnpreparedFish || mat_category == organic_mat_category::Eggs) { diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 65feb2711..529e402bd 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -10,6 +10,7 @@ // df #include "df/building_stockpilest.h" +#include "df/creature_raw.h" #include "df/inorganic_raw.h" #include "df/item_quality.h" #include @@ -44,23 +45,121 @@ namespace DFHack { DBG_EXTERN(stockpiles, log); } -StockpileSerializer::StockpileSerializer(df::building_stockpilest* stockpile) - : mPile(stockpile) { +static struct OtherMatsFurniture { + const std::map mats; + + OtherMatsFurniture() : mats(getMats()) {} + + std::map getMats() { + std::map ret; + ret.emplace(0, "WOOD"); + ret.emplace(1, "PLANT_CLOTH"); + ret.emplace(2, "BONE"); + ret.emplace(3, "TOOTH"); + ret.emplace(4, "HORN"); + ret.emplace(5, "PEARL"); + ret.emplace(6, "SHELL"); + ret.emplace(7, "LEATHER"); + ret.emplace(8, "SILK"); + ret.emplace(9, "AMBER"); + ret.emplace(10, "CORAL"); + ret.emplace(11, "GREEN_GLASS"); + ret.emplace(12, "CLEAR_GLASS"); + ret.emplace(13, "CRYSTAL_GLASS"); + ret.emplace(14, "YARN"); + return ret; + } +} mOtherMatsFurniture; + +static struct OtherMatsBars { + const std::map mats; + + OtherMatsBars() : mats(getMats()) {} + + std::map getMats() { + std::map ret; + ret.emplace(0, "COAL"); + ret.emplace(1, "POTASH"); + ret.emplace(2, "ASH"); + ret.emplace(3, "PEARLASH"); + ret.emplace(4, "SOAP"); + return ret; + } +} mOtherMatsBars; + +static struct OtherMatsBlocks { + const std::map mats; + + OtherMatsBlocks() : mats(getMats()) {} + + std::map getMats() { + std::map ret; + ret.emplace(0, "GREEN_GLASS"); + ret.emplace(1, "CLEAR_GLASS"); + ret.emplace(2, "CRYSTAL_GLASS"); + ret.emplace(3, "WOOD"); + return ret; + } +} mOtherMatsBlocks; + +static struct OtherMatsFinishedGoods { + const std::map mats; + + OtherMatsFinishedGoods() : mats(getMats()) {} + + std::map getMats() { + std::map ret; + ret.emplace(0, "WOOD"); + ret.emplace(1, "PLANT_CLOTH"); + ret.emplace(2, "BONE"); + ret.emplace(3, "TOOTH"); + ret.emplace(4, "HORN"); + ret.emplace(5, "PEARL"); + ret.emplace(6, "SHELL"); + ret.emplace(7, "LEATHER"); + ret.emplace(8, "SILK"); + ret.emplace(9, "AMBER"); + ret.emplace(10, "CORAL"); + ret.emplace(11, "GREEN_GLASS"); + ret.emplace(12, "CLEAR_GLASS"); + ret.emplace(13, "CRYSTAL_GLASS"); + ret.emplace(14, "YARN"); + ret.emplace(15, "WAX"); + return ret; + } +} mOtherMatsFinishedGoods; + +static struct OtherMatsWeaponsArmor { + const std::map mats; + + OtherMatsWeaponsArmor() : mats(getMats()) {} + + std::map getMats() { + std::map ret; + ret.emplace(0, "WOOD"); + ret.emplace(1, "PLANT_CLOTH"); + ret.emplace(2, "BONE"); + ret.emplace(3, "SHELL"); + ret.emplace(4, "LEATHER"); + ret.emplace(5, "SILK"); + ret.emplace(6, "GREEN_GLASS"); + ret.emplace(7, "CLEAR_GLASS"); + ret.emplace(8, "CRYSTAL_GLASS"); + ret.emplace(9, "YARN"); + return ret; + } +} mOtherMatsWeaponsArmor; - // build other_mats indices - furniture_setup_other_mats(); - bars_blocks_setup_other_mats(); - finished_goods_setup_other_mats(); - weapons_armor_setup_other_mats(); -} +StockpileSerializer::StockpileSerializer(df::building_stockpilest* stockpile) + : mPile(stockpile) { } StockpileSerializer::~StockpileSerializer() { } -bool StockpileSerializer::serialize_to_ostream(std::ostream* output) { +bool StockpileSerializer::serialize_to_ostream(std::ostream* output, uint32_t includedElements) { if (output->fail()) return false; mBuffer.Clear(); - write(); + write(includedElements); { io::OstreamOutputStream zero_copy_output(output); if (!mBuffer.SerializeToZeroCopyStream(&zero_copy_output)) @@ -69,544 +168,1435 @@ bool StockpileSerializer::serialize_to_ostream(std::ostream* output) { return output->good(); } -bool StockpileSerializer::serialize_to_file(const std::string& file) { +bool StockpileSerializer::serialize_to_file(const string& file, uint32_t includedElements) { std::fstream output(file, std::ios::out | std::ios::binary | std::ios::trunc); if (output.fail()) { - WARN(log).print("ERROR: failed to open file for writing: '%s'\n", file.c_str()); + WARN(log).print("ERROR: failed to open file for writing: '%s'\n", + file.c_str()); return false; } - return serialize_to_ostream(&output); + return serialize_to_ostream(&output, includedElements); } -bool StockpileSerializer::parse_from_istream(std::istream* input) { +bool StockpileSerializer::parse_from_istream(std::istream* input, DeserializeMode mode, const vector& filters) { if (input->fail()) return false; mBuffer.Clear(); io::IstreamInputStream zero_copy_input(input); - const bool res = mBuffer.ParseFromZeroCopyStream(&zero_copy_input) && input->eof(); + const bool res = mBuffer.ParseFromZeroCopyStream(&zero_copy_input) + && input->eof(); if (res) - read(); + read(mode, filters); return res; } -bool StockpileSerializer::unserialize_from_file(const std::string& file) { +bool StockpileSerializer::unserialize_from_file(const string& file, DeserializeMode mode, const vector& filters) { std::fstream input(file, std::ios::in | std::ios::binary); if (input.fail()) { - WARN(log).print("failed to open file for reading: '%s'\n", file.c_str()); + WARN(log).print("failed to open file for reading: '%s'\n", + file.c_str()); return false; } - return parse_from_istream(&input); -} - -void StockpileSerializer::write() { - DEBUG(log).print("GROUP SET %s\n", bitfield_to_string(mPile->settings.flags).c_str()); - write_general(); - if (mPile->settings.flags.bits.animals) - write_animals(); - if (mPile->settings.flags.bits.food) - write_food(); - if (mPile->settings.flags.bits.furniture) - write_furniture(); - if (mPile->settings.flags.bits.refuse) - write_refuse(); - if (mPile->settings.flags.bits.stone) - write_stone(); - if (mPile->settings.flags.bits.ammo) - write_ammo(); - if (mPile->settings.flags.bits.coins) - write_coins(); - if (mPile->settings.flags.bits.bars_blocks) - write_bars_blocks(); - if (mPile->settings.flags.bits.gems) - write_gems(); - if (mPile->settings.flags.bits.finished_goods) - write_finished_goods(); - if (mPile->settings.flags.bits.leather) - write_leather(); - if (mPile->settings.flags.bits.cloth) - write_cloth(); - if (mPile->settings.flags.bits.wood) - write_wood(); - if (mPile->settings.flags.bits.weapons) - write_weapons(); - if (mPile->settings.flags.bits.armor) - write_armor(); -} - -void StockpileSerializer::read() { - DEBUG(log).print("==READ==\n"); - read_general(); - read_animals(); - read_food(); - read_furniture(); - read_refuse(); - read_stone(); - read_ammo(); - read_coins(); - read_bars_blocks(); - read_gems(); - read_finished_goods(); - read_leather(); - read_cloth(); - read_wood(); - read_weapons(); - read_armor(); -} - -void StockpileSerializer::serialize_list_organic_mat(FuncWriteExport add_value, const std::vector* list, organic_mat_category::organic_mat_category cat) { + return parse_from_istream(&input, mode, filters); +} + +/** + * Find an enum's value based off the string label. + * @param traits the enum's trait struct + * @param token the string value in key_table + * @return the enum's value, -1 if not found + */ +template +static typename df::enum_traits::base_type linear_index(df::enum_traits traits, const string& token) { + auto j = traits.first_item_value; + auto limit = traits.last_item_value; + // sometimes enums start at -1, which is bad news for array indexing + if (j < 0) { + j += abs(traits.first_item_value); + limit += abs(traits.first_item_value); + } + for (; j <= limit; ++j) { + if (token.compare(traits.key_table[j]) == 0) + return j; + } + return -1; +} + +static bool matches_filter(const vector& filters, const string& name) { + for (auto & filter : filters) { + DEBUG(log).print("searching for '%s' in '%s'\n", filter.c_str(), name.c_str()); + if (std::search(name.begin(), name.end(), filter.begin(), filter.end(), + [](unsigned char ch1, unsigned char ch2) { return std::toupper(ch1) == std::toupper(ch2); } + ) != name.end()) + return true; + } + return !filters.size(); +} + +static void set_flag(const char* name, const vector& filters, bool all, char val, bool enabled, bool& elem) { + if ((all || enabled) && matches_filter(filters, name)) { + DEBUG(log).print("setting %s to %d\n", name, val); + elem = val; + } +} + +static void set_filter_elem(const char* subcat, const vector& filters, char val, + const string& name, const string& id, char& elem) { + if (matches_filter(filters, subcat + ((*subcat ? "/" : "") + name))) { + DEBUG(log).print("setting %s (%s) to %d\n", name.c_str(), id.c_str(), val); + elem = val; + } +} + +template +static void set_filter_elem(const char* subcat, const vector& filters, T_val val, + const string& name, T_id id, T_val& elem) { + if (matches_filter(filters, subcat + ((*subcat ? "/" : "") + name))) { + DEBUG(log).print("setting %s (%d) to %d\n", name.c_str(), (int32_t)id, val); + elem = val; + } +} + +/** + * There are many repeated (un)serialization cases throughout the stockpile_settings structure, + * so the most common cases have been generalized into generic functions using lambdas. + * + * The basic process to serialize a stockpile_settings structure is: + * 1. loop through the list + * 2. for every element that is TRUE: + * 3. map the specific stockpile_settings index into a general material, creature, etc index + * 4. verify that type is allowed in the list (e.g., no stone in gems stockpiles) + * 5. add it to the protobuf using FuncWriteExport + * + * The unserialization process is the same in reverse. + */ +static bool serialize_list_itemdef(FuncWriteExport add_value, + vector list, + vector items, + item_type::item_type type) { + bool all = true; + for (size_t i = 0; i < list.size(); ++i) { + if (!list.at(i)) { + all = false; + continue; + } + const df::itemdef* a = items.at(i); + // skip procedurally generated items + if (a->base_flags.is_set(df::itemdef_flags::GENERATED)) + continue; + ItemTypeInfo ii; + if (!ii.decode(type, i)) + continue; + DEBUG(log).print("adding itemdef type %s\n", ii.getToken().c_str()); + add_value(ii.getToken()); + } + return all; +} + +static void unserialize_list_itemdef(const char* subcat, bool all, char val, const vector& filters, + FuncReadImport read_value, int32_t list_size, vector& pile_list, item_type::item_type type) { + int num_elems = Items::getSubtypeCount(type); + pile_list.resize(num_elems, '\0'); + if (all) { + for (auto idx = 0; idx < num_elems; ++idx) { + ItemTypeInfo ii; + ii.decode(type, idx); + set_filter_elem(subcat, filters, val, ii.toString(), idx, pile_list.at(idx)); + } + return; + } + + for (auto i = 0; i < list_size; ++i) { + string id = read_value(i); + ItemTypeInfo ii; + if (!ii.find(id)) + continue; + if (ii.subtype < 0 || size_t(ii.subtype) >= pile_list.size()) { + WARN(log).print("item type index invalid: %d\n", ii.subtype); + continue; + } + set_filter_elem(subcat, filters, val, id, ii.subtype, pile_list.at(ii.subtype)); + } +} + +static bool serialize_list_quality(FuncWriteExport add_value, + const bool(&quality_list)[7]) { + using df::enums::item_quality::item_quality; + using quality_traits = df::enum_traits; + + bool all = true; + for (size_t i = 0; i < 7; ++i) { + if (!quality_list[i]) { + all = false; + continue; + } + const string f_type(quality_traits::key_table[i]); + add_value(f_type); + DEBUG(log).print("adding quality %s\n", f_type.c_str()); + } + return all; +} + +static void quality_clear(bool(&pile_list)[7]) { + std::fill(pile_list, pile_list + 7, false); +} + +static void unserialize_list_quality(const char* subcat, bool all, bool val, const vector& filters, + FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]) { + if (all) { + for (auto idx = 0; idx < 7; ++idx) { + string id = ENUM_KEY_STR(item_quality, (df::item_quality)idx); + set_filter_elem(subcat, filters, val, id, idx, pile_list[idx]); + } + return; + } + + using df::enums::item_quality::item_quality; + df::enum_traits quality_traits; + for (int i = 0; i < list_size; ++i) { + const string quality = read_value(i); + df::enum_traits::base_type idx = linear_index(quality_traits, quality); + if (idx < 0) { + WARN(log).print("invalid quality token: %s\n", quality.c_str()); + continue; + } + set_filter_elem(subcat, filters, val, quality, idx, pile_list[idx]); + } +} + +static string other_mats_index(const std::map other_mats, + int idx) { + auto it = other_mats.find(idx); + if (it == other_mats.end()) + return string(); + return it->second; +} + +static int other_mats_token(const std::map other_mats, + const string& token) { + for (auto it = other_mats.begin(); it != other_mats.end(); ++it) { + if (it->second == token) + return it->first; + } + return -1; +} + +static bool serialize_list_other_mats( + const std::map other_mats, + FuncWriteExport add_value, + vector list) { + bool all = true; + for (size_t i = 0; i < list.size(); ++i) { + if (!list.at(i)) { + all = false; + continue; + } + const string token = other_mats_index(other_mats, i); + if (token.empty()) { + WARN(log).print("invalid other material with index %zd\n", i); + continue; + } + add_value(token); + DEBUG(log).print("other mats %zd is %s\n", i, token.c_str()); + } + return all; +} + +static void unserialize_list_other_mats(const char* subcat, bool all, char val, const vector& filters, + const std::map other_mats, FuncReadImport read_value, int32_t list_size, vector& pile_list) { + size_t num_elems = other_mats.size(); + pile_list.resize(num_elems, '\0'); + + if (all) { + for (auto & entry : other_mats) + set_filter_elem(subcat, filters, val, entry.second, entry.first, pile_list.at(entry.first)); + return; + } + + for (int i = 0; i < list_size; ++i) { + const string token = read_value(i); + size_t idx = other_mats_token(other_mats, token); + if (idx < 0) { + WARN(log).print("invalid other mat with token %s\n", token.c_str()); + continue; + } + if (idx >= num_elems) { + WARN(log).print("other_mats index too large! idx[%zd] max_size[%zd]\n", idx, num_elems); + continue; + } + set_filter_elem(subcat, filters, val, token, idx, pile_list.at(idx)); + } +} + +static bool serialize_list_organic_mat(FuncWriteExport add_value, + const vector* list, + organic_mat_category::organic_mat_category cat) { + bool all = true; if (!list) { DEBUG(log).print("serialize_list_organic_mat: list null\n"); - return; + return all; } for (size_t i = 0; i < list->size(); ++i) { - if (list->at(i)) { - std::string token = OrganicMatLookup::food_token_by_idx(cat, i); - if (token.empty()) { - DEBUG(log).print("food mat invalid :(\n"); - continue; - } - DEBUG(log).print("organic_material %zd is %s\n", i, token.c_str()); - add_value(token); + if (!list->at(i)) { + all = false; + continue; + } + string token = OrganicMatLookup::food_token_by_idx(cat, i); + if (token.empty()) { + DEBUG(log).print("food mat invalid :(\n"); + continue; } + DEBUG(log).print("organic_material %zd is %s\n", i, token.c_str()); + add_value(token); } + return all; } -void StockpileSerializer::unserialize_list_organic_mat(FuncReadImport get_value, size_t list_size, std::vector* pile_list, organic_mat_category::organic_mat_category cat) { - pile_list->clear(); - pile_list->resize(OrganicMatLookup::food_max_size(cat), '\0'); +static void unserialize_list_organic_mat(const char* subcat, bool all, char val, const vector& filters, + FuncReadImport read_value, size_t list_size, vector& pile_list, + organic_mat_category::organic_mat_category cat) { + size_t num_elems = OrganicMatLookup::food_max_size(cat); + pile_list.resize(num_elems, '\0'); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + string token = OrganicMatLookup::food_token_by_idx(cat, idx); + set_filter_elem(subcat, filters, val, token, idx, pile_list.at(idx)); + } + return; + } + for (size_t i = 0; i < list_size; ++i) { - std::string token = get_value(i); + const string token = read_value(i); int16_t idx = OrganicMatLookup::food_idx_by_token(cat, token); - DEBUG(log).print(" organic_material %d is %s\n", idx, token.c_str()); - if (size_t(idx) >= pile_list->size()) { - WARN(log).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, pile_list->size()); + if (idx < 0 || size_t(idx) >= num_elems) { + WARN(log).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); continue; } - pile_list->at(idx) = 1; + set_filter_elem(subcat, filters, val, token, idx, pile_list.at(idx)); } } -void StockpileSerializer::serialize_list_item_type(FuncItemAllowed is_allowed, FuncWriteExport add_value, const std::vector& list) { +static bool serialize_list_item_type(FuncItemAllowed is_allowed, + FuncWriteExport add_value, const vector& list) { using df::enums::item_type::item_type; using type_traits = df::enum_traits; + + bool all = true; size_t num_item_types = list.size(); - DEBUG(log).print("item_type size = %zd size limit = %d typecasted: %zd\n", num_item_types, type_traits::last_item_value, (size_t)type_traits::last_item_value); + DEBUG(log).print("item_type size = %zd size limit = %d typecasted: %zd\n", + num_item_types, type_traits::last_item_value, + (size_t)type_traits::last_item_value); for (size_t i = 0; i <= (size_t)type_traits::last_item_value; ++i) { - if (i < num_item_types && !list.at(i)) + if (i < num_item_types && !list.at(i)) { + all = false; continue; + } const item_type type = (item_type)((df::enum_traits::base_type)i); - std::string r_type(type_traits::key_table[i + 1]); + string r_type(type_traits::key_table[i + 1]); if (!is_allowed(type)) continue; add_value(r_type); DEBUG(log).print("item_type key_table[%zd] type[%d] is %s\n", i + 1, (int16_t)type, r_type.c_str()); } + return all; } -void StockpileSerializer::unserialize_list_item_type(FuncItemAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector* pile_list) { - pile_list->clear(); - pile_list->resize(112, '\0'); // TODO remove hardcoded list size value - for (size_t i = 0; i < pile_list->size(); ++i) { - pile_list->at(i) = is_allowed((item_type::item_type)i) ? 0 : 1; +static void unserialize_list_item_type(const char* subcat, bool all, char val, const vector& filters, + FuncItemAllowed is_allowed, FuncReadImport read_value, int32_t list_size, vector& pile_list) { + // TODO can we remove the hardcoded list size? + size_t num_elems = 112; + pile_list.resize(num_elems, '\0'); + for (size_t i = 0; i < num_elems; ++i) { + if (!is_allowed((df::item_type)i)) + pile_list.at(i) = 1; } + + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + string id = ENUM_KEY_STR(item_type, (df::item_type)idx); + set_filter_elem(subcat, filters, val, id, idx, pile_list.at(idx)); + } + return; + } + using df::enums::item_type::item_type; df::enum_traits type_traits; - for (int32_t i = 0; i < list_size; ++i) { - const std::string token = read_value(i); + for (int i = 0; i < list_size; ++i) { + const string token = read_value(i); // subtract one because item_type starts at -1 const df::enum_traits::base_type idx = linear_index(type_traits, token) - 1; - const item_type type = (item_type)idx; - if (!is_allowed(type)) + if (!is_allowed((item_type)idx)) continue; - DEBUG(log).print("item_type %d is %s\n", idx, token.c_str()); - if (size_t(idx) >= pile_list->size()) { - WARN(log).print("error item_type index too large! idx[%d] max_size[%zd]\n", idx, pile_list->size()); + if (idx < 0 || size_t(idx) >= num_elems) { + WARN(log).print("error item_type index too large! idx[%d] max_size[%zd]\n", idx, num_elems); continue; } - pile_list->at(idx) = 1; + set_filter_elem(subcat, filters, val, token, idx, pile_list.at(idx)); } } -void StockpileSerializer::serialize_list_material(FuncMaterialAllowed is_allowed, FuncWriteExport add_value, const std::vector& list) { +static bool serialize_list_material(FuncMaterialAllowed is_allowed, + FuncWriteExport add_value, const vector& list) { + bool all = true; MaterialInfo mi; for (size_t i = 0; i < list.size(); ++i) { - if (list.at(i)) { - mi.decode(0, i); - if (!is_allowed(mi)) - continue; - DEBUG(log).print("material %zd is %s\n", i, mi.getToken().c_str()); - add_value(mi.getToken()); + if (!list.at(i)) { + all = false; + continue; } + mi.decode(0, i); + if (!is_allowed(mi)) + continue; + DEBUG(log).print("adding material %s\n", mi.getToken().c_str()); + add_value(mi.getToken()); } + return all; } -void StockpileSerializer::unserialize_list_material(FuncMaterialAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector* pile_list) { - // we initialize all possible (allowed) values to 0, - // then all other not-allowed values to 1 - // why? because that's how the memory is in DF before - // we muck with it. - std::set idx_set; - pile_list->clear(); - pile_list->resize(world->raws.inorganics.size(), 0); - for (size_t i = 0; i < pile_list->size(); ++i) { +static void unserialize_list_material(const char* subcat, bool all, char val, const vector& filters, + FuncMaterialAllowed is_allowed, FuncReadImport read_value, int32_t list_size, + vector& pile_list) { + // we initialize all disallowed values to 1 + // why? because that's how the memory is in DF before we muck with it. + size_t num_elems = world->raws.inorganics.size(); + pile_list.resize(num_elems, 0); + for (size_t i = 0; i < pile_list.size(); ++i) { MaterialInfo mi(0, i); - pile_list->at(i) = is_allowed(mi) ? 0 : 1; - } - for (int i = 0; i < list_size; ++i) { - const std::string token = read_value(i); - MaterialInfo mi; - mi.find(token); if (!is_allowed(mi)) - continue; - DEBUG(log).print("material %d is %s\n", mi.index, token.c_str()); - if (size_t(mi.index) >= pile_list->size()) { - WARN(log).print("material index too large! idx[%d] max_size[%zd]\n", mi.index, pile_list->size()); - continue; - } - pile_list->at(mi.index) = 1; + pile_list.at(i) = 1; } -} -void StockpileSerializer::serialize_list_quality(FuncWriteExport add_value, const bool(&quality_list)[7]) { - using df::enums::item_quality::item_quality; - using quality_traits = df::enum_traits; - for (size_t i = 0; i < 7; ++i) { - if (quality_list[i]) { - const std::string f_type(quality_traits::key_table[i]); - add_value(f_type); - DEBUG(log).print("quality: %zd is %s\n", i, f_type.c_str()); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + MaterialInfo mi; + mi.decode(0, idx); + set_filter_elem(subcat, filters, val, mi.toString(), idx, pile_list.at(idx)); } + return; } -} - -void StockpileSerializer::quality_clear(bool(&pile_list)[7]) { - std::fill(pile_list, pile_list + 7, false); -} -void StockpileSerializer::unserialize_list_quality(FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]) { - quality_clear(pile_list); - if (list_size > 0 && list_size <= 7) { - using df::enums::item_quality::item_quality; - df::enum_traits quality_traits; - for (int i = 0; i < list_size; ++i) { - const std::string quality = read_value(i); - df::enum_traits::base_type idx = linear_index(quality_traits, quality); - if (idx < 0) { - WARN(log).print("invalid quality token: %s\n", quality.c_str()); - continue; - } - DEBUG(log).print("quality: %d is %s\n", idx, quality.c_str()); - pile_list[idx] = true; + for (auto i = 0; i < list_size; ++i) { + string id = read_value(i); + MaterialInfo mi; + if (!mi.find(id) || !is_allowed(mi)) + continue; + if (mi.index < 0 || size_t(mi.index) >= pile_list.size()) { + WARN(log).print("material type index invalid: %d\n", mi.index); + continue; } + set_filter_elem(subcat, filters, val, id, mi.index, pile_list.at(mi.index)); } } -void StockpileSerializer::serialize_list_other_mats(const std::map other_mats, FuncWriteExport add_value, std::vector list) { +static bool serialize_list_creature(FuncWriteExport add_value, const vector& list) { + bool all = true; + for (size_t i = 0; i < list.size(); ++i) { - if (list.at(i)) { - const std::string token = other_mats_index(other_mats, i); - if (token.empty()) { - WARN(log).print("invalid other material with index %zd\n", i); - continue; - } - add_value(token); - DEBUG(log).print("other mats %zd is %s\n", i, token.c_str()); + if (!list.at(i)) { + all = false; + continue; } + df::creature_raw* r = find_creature(i); + if (r->flags.is_set(creature_raw_flags::GENERATED) + || r->creature_id == "EQUIPMENT_WAGON") + continue; + DEBUG(log).print("adding creature %s\n", r->creature_id.c_str()); + add_value(r->creature_id); } + return all; } -void StockpileSerializer::unserialize_list_other_mats(const std::map other_mats, FuncReadImport read_value, int32_t list_size, std::vector* pile_list) { - pile_list->clear(); - pile_list->resize(other_mats.size(), '\0'); - for (int i = 0; i < list_size; ++i) { - const std::string token = read_value(i); - size_t idx = other_mats_token(other_mats, token); - if (idx < 0) { - WARN(log).print("invalid other mat with token %s\n", token.c_str()); - continue; +static void unserialize_list_creature(const char* subcat, bool all, char val, const vector& filters, + FuncReadImport read_value, int32_t list_size, vector& pile_list) { + size_t num_elems = world->raws.creatures.all.size(); + pile_list.resize(num_elems, '\0'); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + auto r = find_creature(idx); + set_filter_elem(subcat, filters, val, r->name[0], r->creature_id, pile_list.at(idx)); } - DEBUG(log).print("other_mats %zd is %s\n", idx, token.c_str()); - if (idx >= pile_list->size()) { - WARN(log).print("other_mats index too large! idx[%zd] max_size[%zd]\n", idx, pile_list->size()); + return; + } + + for (auto i = 0; i < list_size; ++i) { + string id = read_value(i); + int idx = find_creature(id); + if (idx < 0 || size_t(idx) >= num_elems) { + WARN(log).print("animal index invalid: %d\n", idx); continue; } - pile_list->at(idx) = 1; + auto r = find_creature(idx); + set_filter_elem(subcat, filters, val, r->name[0], r->creature_id, pile_list.at(idx)); } } -void StockpileSerializer::serialize_list_itemdef(FuncWriteExport add_value, std::vector list, std::vector items, item_type::item_type type) { - for (size_t i = 0; i < list.size(); ++i) { - if (list.at(i)) { - const df::itemdef* a = items.at(i); - // skip procedurally generated items - if (a->base_flags.is_set(df::itemdef_flags::GENERATED)) - continue; - ItemTypeInfo ii; - if (!ii.decode(type, i)) - continue; - add_value(ii.getToken()); - DEBUG(log).print("itemdef type %zd is %s\n", i, ii.getToken().c_str()); - } +template +static void write_cat(const char *name, bool include_types, uint32_t cat_flags, + enum df::stockpile_group_set::Mask cat_mask, + std::function mutable_cat_fn, + std::function write_cat_fn) { + if (!(cat_flags & cat_mask)) + return; + + T_cat_set* cat_set = mutable_cat_fn(); + + if (!include_types) { + DEBUG(log).print("including all for %s since only category is being recorded\n", name); + cat_set->set_all(true); + return; } -} -void StockpileSerializer::unserialize_list_itemdef(FuncReadImport read_value, int32_t list_size, std::vector* pile_list, item_type::item_type type) { - pile_list->clear(); - pile_list->resize(Items::getSubtypeCount(type), '\0'); - for (int i = 0; i < list_size; ++i) { - std::string token = read_value(i); - ItemTypeInfo ii; - if (!ii.find(token)) - continue; - DEBUG(log).print("itemdef %d is %s\n", ii.subtype, token.c_str()); - if (size_t(ii.subtype) >= pile_list->size()) { - WARN(log).print("itemdef index too large! idx[%d] max_size[%zd]\n", ii.subtype, pile_list->size()); - continue; - } - pile_list->at(ii.subtype) = 1; + if (write_cat_fn(cat_set)) { + // all fields were set. clear them and use the "all" flag instead so "all" can be applied + // to other worlds with other generated types + DEBUG(log).print("including all for %s since all fields were enabled\n", name); + cat_set->Clear(); + cat_set->set_all(true); } } -std::string StockpileSerializer::other_mats_index(const std::map other_mats, int idx) { - auto it = other_mats.find(idx); - if (it == other_mats.end()) - return std::string(); - return it->second; +void StockpileSerializer::write(uint32_t includedElements) { + if (includedElements & INCLUDED_ELEMENTS_CONTAINERS) + write_containers(); + if (includedElements & INCLUDED_ELEMENTS_GENERAL) + write_general(); + + if (!(includedElements & INCLUDED_ELEMENTS_CATEGORIES)) + return; + + DEBUG(log).print("GROUP SET %s\n", + bitfield_to_string(mPile->settings.flags).c_str()); + + bool include_types = 0 != (includedElements & INCLUDED_ELEMENTS_TYPES); + + write_cat("ammo", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_ammo, + std::bind(&StockpileSettings::mutable_ammo, &mBuffer), + std::bind(&StockpileSerializer::write_ammo, this, _1)); + write_cat("animals", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_animals, + std::bind(&StockpileSettings::mutable_animals, &mBuffer), + std::bind(&StockpileSerializer::write_animals, this, _1)); + write_cat("armor", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_armor, + std::bind(&StockpileSettings::mutable_armor, &mBuffer), + std::bind(&StockpileSerializer::write_armor, this, _1)); + write_cat("bars_blocks", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_bars_blocks, + std::bind(&StockpileSettings::mutable_barsblocks, &mBuffer), + std::bind(&StockpileSerializer::write_bars_blocks, this, _1)); + write_cat("cloth", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_cloth, + std::bind(&StockpileSettings::mutable_cloth, &mBuffer), + std::bind(&StockpileSerializer::write_cloth, this, _1)); + write_cat("coin", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_coins, + std::bind(&StockpileSettings::mutable_coin, &mBuffer), + std::bind(&StockpileSerializer::write_coins, this, _1)); + write_cat("finished_goods", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_finished_goods, + std::bind(&StockpileSettings::mutable_finished_goods, &mBuffer), + std::bind(&StockpileSerializer::write_finished_goods, this, _1)); + write_cat("food", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_food, + std::bind(&StockpileSettings::mutable_food, &mBuffer), + std::bind(&StockpileSerializer::write_food, this, _1)); + write_cat("furniture", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_furniture, + std::bind(&StockpileSettings::mutable_furniture, &mBuffer), + std::bind(&StockpileSerializer::write_furniture, this, _1)); + write_cat("gems", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_gems, + std::bind(&StockpileSettings::mutable_gems, &mBuffer), + std::bind(&StockpileSerializer::write_gems, this, _1)); + write_cat("leather", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_leather, + std::bind(&StockpileSettings::mutable_leather, &mBuffer), + std::bind(&StockpileSerializer::write_leather, this, _1)); + write_cat("corpses", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_corpses, + std::bind(&StockpileSettings::mutable_corpses_v50, &mBuffer), + std::bind(&StockpileSerializer::write_corpses, this, _1)); + write_cat("refuse", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_refuse, + std::bind(&StockpileSettings::mutable_refuse, &mBuffer), + std::bind(&StockpileSerializer::write_refuse, this, _1)); + write_cat("sheet", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_sheet, + std::bind(&StockpileSettings::mutable_sheet, &mBuffer), + std::bind(&StockpileSerializer::write_sheet, this, _1)); + write_cat("stone", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_stone, + std::bind(&StockpileSettings::mutable_stone, &mBuffer), + std::bind(&StockpileSerializer::write_stone, this, _1)); + write_cat("weapons", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_weapons, + std::bind(&StockpileSettings::mutable_weapons, &mBuffer), + std::bind(&StockpileSerializer::write_weapons, this, _1)); + write_cat("wood", include_types, + mPile->settings.flags.whole, + mPile->settings.flags.mask_wood, + std::bind(&StockpileSettings::mutable_wood, &mBuffer), + std::bind(&StockpileSerializer::write_wood, this, _1)); +} + +void StockpileSerializer::read(DeserializeMode mode, const vector& filters) { + DEBUG(log).print("==READ==\n"); + read_containers(mode); + read_general(mode); + read_ammo(mode, filters); + read_animals(mode, filters); + read_armor(mode, filters); + read_bars_blocks(mode, filters); + read_cloth(mode, filters); + read_coins(mode, filters); + read_finished_goods(mode, filters); + read_food(mode, filters); + read_furniture(mode, filters); + read_gems(mode, filters); + read_leather(mode, filters); + + // support for old versions before corpses had a set + if (mBuffer.has_corpses()) { + StockpileSettings::CorpsesSet* corpses = mBuffer.mutable_corpses_v50(); + corpses->set_all(true); + } + read_corpses(mode, filters); + + read_refuse(mode, filters); + read_sheet(mode, filters); + read_stone(mode, filters); + read_weapons(mode, filters); + read_wood(mode, filters); +} + +void StockpileSerializer::write_containers() { + DEBUG(log).print("writing container settings\n"); + mBuffer.set_max_bins(mPile->max_bins); + mBuffer.set_max_barrels(mPile->max_barrels); + mBuffer.set_max_wheelbarrows(mPile->max_wheelbarrows); } -int StockpileSerializer::other_mats_token(const std::map other_mats, const std::string& token) { - for (auto it = other_mats.begin(); it != other_mats.end(); ++it) { - if (it->second == token) - return it->first; +template +static void read_elem(const char *name, DeserializeMode mode, + std::function has_elem_fn, + std::function elem_fn, + T_elem &setting) { + if (!has_elem_fn()) + return; + + bool is_set = elem_fn() != 0; + if (mode == DESERIALIZE_MODE_SET || is_set) { + T_elem val = (mode == DESERIALIZE_MODE_DISABLE) ? 0 : elem_fn(); + DEBUG(log).print("setting %s to %d\n", name, val); + setting = val; } - return -1; +} + +template +static void read_category(const char *name, DeserializeMode mode, + std::function has_cat_fn, + std::function cat_fn, + uint32_t & cat_flags, + enum df::stockpile_group_set::Mask cat_mask, + std::function clear_fn, + std::function set_fn) { + if (mode == DESERIALIZE_MODE_SET) { + DEBUG(log).print("clearing %s\n", name); + cat_flags &= ~cat_mask; + clear_fn(); + } + + if (!has_cat_fn()) + return; + + if (mode == DESERIALIZE_MODE_DISABLE && !(cat_flags & cat_mask)) + return; + + if (mode == DESERIALIZE_MODE_SET || mode == DESERIALIZE_MODE_ENABLE) + cat_flags |= cat_mask; + + bool all = cat_fn().all(); + char val = (mode == DESERIALIZE_MODE_DISABLE) ? (char)0 : (char)1; + DEBUG(log).print("setting %s %s elements to %d\n", + all ? "all" : "marked", name, val); + set_fn(all, val); +} + +void StockpileSerializer::read_containers(DeserializeMode mode) { + read_elem("max_bins", mode, + std::bind(&StockpileSettings::has_max_bins, mBuffer), + std::bind(&StockpileSettings::max_bins, mBuffer), + mPile->max_bins); + read_elem("max_barrels", mode, + std::bind(&StockpileSettings::has_max_barrels, mBuffer), + std::bind(&StockpileSettings::max_barrels, mBuffer), + mPile->max_barrels); + read_elem("max_wheelbarrows", mode, + std::bind(&StockpileSettings::has_max_wheelbarrows, mBuffer), + std::bind(&StockpileSettings::max_wheelbarrows, mBuffer), + mPile->max_wheelbarrows); } void StockpileSerializer::write_general() { - mBuffer.set_max_bins(mPile->max_bins); - mBuffer.set_max_wheelbarrows(mPile->max_wheelbarrows); - mBuffer.set_max_barrels(mPile->max_barrels); + DEBUG(log).print("writing general settings\n"); mBuffer.set_use_links_only(mPile->use_links_only); mBuffer.set_allow_inorganic(mPile->settings.allow_inorganic); mBuffer.set_allow_organic(mPile->settings.allow_organic); - mBuffer.set_corpses(mPile->settings.flags.bits.corpses); -} - -void StockpileSerializer::read_general() { - if (mBuffer.has_max_bins()) - mPile->max_bins = mBuffer.max_bins(); - if (mBuffer.has_max_wheelbarrows()) - mPile->max_wheelbarrows = mBuffer.max_wheelbarrows(); - if (mBuffer.has_max_barrels()) - mPile->max_barrels = mBuffer.max_barrels(); - if (mBuffer.has_use_links_only()) - mPile->use_links_only = mBuffer.use_links_only(); - if (mBuffer.has_allow_inorganic()) - mPile->settings.allow_inorganic = mBuffer.allow_inorganic(); - if (mBuffer.has_allow_organic()) - mPile->settings.allow_organic = mBuffer.allow_organic(); - if (mBuffer.has_corpses()) - mPile->settings.flags.bits.corpses = mBuffer.corpses(); -} - -void StockpileSerializer::write_animals() { - StockpileSettings::AnimalsSet* animals = mBuffer.mutable_animals(); - animals->set_empty_cages(mPile->settings.animals.empty_cages); - animals->set_empty_traps(mPile->settings.animals.empty_traps); - for (size_t i = 0; i < mPile->settings.animals.enabled.size(); ++i) { - if (mPile->settings.animals.enabled.at(i) == 1) { - df::creature_raw* r = find_creature(i); - DEBUG(log).print("creature %s %zd\n", r->creature_id.c_str(), i); - animals->add_enabled(r->creature_id); +} + +void StockpileSerializer::read_general(DeserializeMode mode) { + read_elem("use_links_only", mode, + std::bind(&StockpileSettings::has_use_links_only, mBuffer), + std::bind(&StockpileSettings::use_links_only, mBuffer), + mPile->use_links_only); + read_elem("allow_inorganic", mode, + std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), + std::bind(&StockpileSettings::allow_inorganic, mBuffer), + mPile->settings.allow_inorganic); + read_elem("allow_organic", mode, + std::bind(&StockpileSettings::has_allow_organic, mBuffer), + std::bind(&StockpileSettings::allow_organic, mBuffer), + mPile->settings.allow_organic); +} + +static bool ammo_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); +} + +bool StockpileSerializer::write_ammo(StockpileSettings::AmmoSet* ammo) { + bool all = serialize_list_itemdef( + [&](const string& token) { ammo->add_type(token); }, + mPile->settings.ammo.type, + vector(world->raws.itemdefs.ammo.begin(), world->raws.itemdefs.ammo.end()), + item_type::AMMO); + + all = serialize_list_material( + ammo_mat_is_allowed, + [&](const string& token) { ammo->add_mats(token); }, + mPile->settings.ammo.mats) && all; + + if (mPile->settings.ammo.other_mats.size() > 2) { + WARN(log).print("ammo other materials > 2: %zd\n", + mPile->settings.ammo.other_mats.size()); + } + + size_t num_other_mats = std::min(size_t(2), + mPile->settings.ammo.other_mats.size()); + for (size_t i = 0; i < num_other_mats; ++i) { + if (!mPile->settings.ammo.other_mats.at(i)) { + all = false; + continue; } + const string token = i == 0 ? "WOOD" : "BONE"; + ammo->add_other_mats(token); + DEBUG(log).print("other mats %zd is %s\n", i, token.c_str()); } + + all = serialize_list_quality( + [&](const string& token) { ammo->add_quality_core(token); }, + mPile->settings.ammo.quality_core) && all; + + all = serialize_list_quality( + [&](const string& token) { ammo->add_quality_total(token); }, + mPile->settings.ammo.quality_total) && all; + + return all; } -void StockpileSerializer::read_animals() { - if (mBuffer.has_animals()) { - mPile->settings.flags.bits.animals = 1; - DEBUG(log).print("animals:\n"); - mPile->settings.animals.empty_cages = mBuffer.animals().empty_cages(); - mPile->settings.animals.empty_traps = mBuffer.animals().empty_traps(); - - mPile->settings.animals.enabled.clear(); - mPile->settings.animals.enabled.resize(world->raws.creatures.all.size(), '\0'); - DEBUG(log).print("pile has %zd\n", mPile->settings.animals.enabled.size()); - for (auto i = 0; i < mBuffer.animals().enabled_size(); ++i) { - std::string id = mBuffer.animals().enabled(i); - int idx = find_creature(id); - DEBUG(log).print("%s %d\n", id.c_str(), idx); - if (idx < 0 || size_t(idx) >= mPile->settings.animals.enabled.size()) { - WARN(log).print("animal index invalid: %d\n", idx); - continue; +void StockpileSerializer::read_ammo(DeserializeMode mode, const vector& filters) { + auto & pammo = mPile->settings.ammo; + read_category("ammo", mode, + std::bind(&StockpileSettings::has_ammo, mBuffer), + std::bind(&StockpileSettings::ammo, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_ammo, + [&]() { + pammo.type.clear(); + pammo.mats.clear(); + pammo.other_mats.clear(); + quality_clear(pammo.quality_core); + quality_clear(pammo.quality_total); + }, + [&](bool all, char val) { + auto & bammo = mBuffer.ammo(); + + unserialize_list_itemdef("type", all, val, filters, + [&](const size_t& idx) -> const string& { return bammo.type(idx); }, + bammo.type_size(), pammo.type, item_type::AMMO); + + unserialize_list_material("mats", all, val, filters, ammo_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bammo.mats(idx); }, + bammo.mats_size(), pammo.mats); + + pammo.other_mats.resize(2, '\0'); + if (all) { + set_filter_elem("other", filters, val, "WOOD", 0, pammo.other_mats.at(0)); + set_filter_elem("other", filters, val, "BONE", 1, pammo.other_mats.at(1)); + } else { + // TODO can we un-hardcode the values? + for (int i = 0; i < bammo.other_mats_size(); ++i) { + const string id = bammo.other_mats(i); + const int32_t idx = id == "WOOD" ? 0 : id == "BONE" ? 1 : -1; + if (idx == -1) + continue; + set_filter_elem("other", filters, val, id, idx, pammo.other_mats.at(idx)); + } } - mPile->settings.animals.enabled.at(idx) = (char)1; - } - } - else { - mPile->settings.animals.enabled.clear(); - mPile->settings.flags.bits.animals = 0; - mPile->settings.animals.empty_cages = false; - mPile->settings.animals.empty_traps = false; + + unserialize_list_quality("core", all, val, filters, + [&](const size_t& idx) -> const string& { return bammo.quality_core(idx); }, + bammo.quality_core_size(), pammo.quality_core); + + unserialize_list_quality("total", all, val, filters, + [&](const size_t& idx) -> const string& { return bammo.quality_total(idx); }, + bammo.quality_total_size(), pammo.quality_total); + }); +} + +bool StockpileSerializer::write_animals(StockpileSettings::AnimalsSet* animals) { + auto & panimals = mPile->settings.animals; + bool all = panimals.empty_cages && panimals.empty_traps; + + animals->set_empty_cages(panimals.empty_cages); + animals->set_empty_traps(panimals.empty_traps); + + return serialize_list_creature( + [&](const string& token) { animals->add_enabled(token); }, + panimals.enabled) && all; +} + +void StockpileSerializer::read_animals(DeserializeMode mode, const vector& filters) { + auto & panimals = mPile->settings.animals; + read_category("animals", mode, + std::bind(&StockpileSettings::has_animals, mBuffer), + std::bind(&StockpileSettings::animals, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_animals, + [&]() { + panimals.empty_cages = false; + panimals.empty_traps = false; + panimals.enabled.clear(); + }, + [&](bool all, char val) { + auto & banimals = mBuffer.animals(); + + set_flag("cages", filters, all, val, banimals.empty_cages(), panimals.empty_cages); + set_flag("traps", filters, all, val, banimals.empty_traps(), panimals.empty_traps); + + unserialize_list_creature("", all, val, filters, + [&](const size_t& idx) -> const string& { return banimals.enabled(idx); }, + banimals.enabled_size(), panimals.enabled); + }); +} + +static bool armor_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); +} + +bool StockpileSerializer::write_armor(StockpileSettings::ArmorSet* armor) { + + auto & parmor = mPile->settings.armor; + bool all = parmor.unusable && parmor.usable; + + armor->set_unusable(parmor.unusable); + armor->set_usable(parmor.usable); + + // armor type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_body(token); }, + parmor.body, + vector(world->raws.itemdefs.armor.begin(), world->raws.itemdefs.armor.end()), + item_type::ARMOR) && all; + + // helm type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_head(token); }, + parmor.head, + vector(world->raws.itemdefs.helms.begin(), world->raws.itemdefs.helms.end()), + item_type::HELM) && all; + + // shoes type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_feet(token); }, + parmor.feet, + vector(world->raws.itemdefs.shoes.begin(), world->raws.itemdefs.shoes.end()), + item_type::SHOES) && all; + + // gloves type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_hands(token); }, + parmor.hands, + vector(world->raws.itemdefs.gloves.begin(), world->raws.itemdefs.gloves.end()), + item_type::GLOVES) && all; + + // pant type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_legs(token); }, + parmor.legs, + vector(world->raws.itemdefs.pants.begin(), world->raws.itemdefs.pants.end()), + item_type::PANTS) && all; + + // shield type + all = serialize_list_itemdef( + [&](const string& token) { armor->add_shield(token); }, + parmor.shield, + vector(world->raws.itemdefs.shields.begin(), world->raws.itemdefs.shields.end()), + item_type::SHIELD) && all; + + // materials + all = serialize_list_material( + armor_mat_is_allowed, + [&](const string& token) { armor->add_mats(token); }, + parmor.mats) && all; + + // other mats + all = serialize_list_other_mats( + mOtherMatsWeaponsArmor.mats, [&](const string& token) { armor->add_other_mats(token); }, + parmor.other_mats) && all; + + // quality core + all = serialize_list_quality([&](const string& token) { armor->add_quality_core(token); }, + parmor.quality_core) && all; + + // quality total + all = serialize_list_quality([&](const string& token) { armor->add_quality_total(token); }, + parmor.quality_total) && all; + + return all; +} + +void StockpileSerializer::read_armor(DeserializeMode mode, const vector& filters) { + auto & parmor = mPile->settings.armor; + read_category("armor", mode, + std::bind(&StockpileSettings::has_armor, mBuffer), + std::bind(&StockpileSettings::armor, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_armor, + [&]() { + parmor.unusable = false; + parmor.usable = false; + parmor.body.clear(); + parmor.head.clear(); + parmor.feet.clear(); + parmor.hands.clear(); + parmor.legs.clear(); + parmor.shield.clear(); + parmor.other_mats.clear(); + parmor.mats.clear(); + quality_clear(parmor.quality_core); + quality_clear(parmor.quality_total); + }, + [&](bool all, char val) { + auto & barmor = mBuffer.armor(); + + set_flag("nouse", filters, all, val, barmor.unusable(), parmor.unusable); + set_flag("canuse", filters, all, val, barmor.usable(), parmor.usable); + + unserialize_list_itemdef("body", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.body(idx); }, + barmor.body_size(), parmor.body, item_type::ARMOR); + + unserialize_list_itemdef("head", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.head(idx); }, + barmor.head_size(), parmor.head, item_type::HELM); + + unserialize_list_itemdef("feet", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.feet(idx); }, + barmor.feet_size(), parmor.feet, item_type::SHOES); + + unserialize_list_itemdef("hands", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.hands(idx); }, + barmor.hands_size(), parmor.hands, item_type::GLOVES); + + unserialize_list_itemdef("legs", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.legs(idx); }, + barmor.legs_size(), parmor.legs, item_type::PANTS); + + unserialize_list_itemdef("shield", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.shield(idx); }, + barmor.shield_size(), parmor.shield, item_type::SHIELD); + + unserialize_list_material("mats", all, val, filters, + armor_mat_is_allowed, + [&](const size_t& idx) -> const string& { return barmor.mats(idx); }, + barmor.mats_size(), parmor.mats); + + unserialize_list_other_mats("other", all, val, filters, + mOtherMatsWeaponsArmor.mats, [&](const size_t& idx) -> const string& { return barmor.other_mats(idx); }, + barmor.other_mats_size(), parmor.other_mats); + + unserialize_list_quality("core", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.quality_core(idx); }, + barmor.quality_core_size(), parmor.quality_core); + + unserialize_list_quality("total", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.quality_total(idx); }, + barmor.quality_total_size(), parmor.quality_total); + }); +} + +static bool bars_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); +} + +static bool blocks_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); +} + +bool StockpileSerializer::write_bars_blocks(StockpileSettings::BarsBlocksSet* bars_blocks) { + bool all = serialize_list_material( + bars_mat_is_allowed, + [&](const string& token) { bars_blocks->add_bars_mats(token); }, + mPile->settings.bars_blocks.bars_mats); + + all = serialize_list_material( + blocks_mat_is_allowed, + [&](const string& token) { bars_blocks->add_blocks_mats(token); }, + mPile->settings.bars_blocks.blocks_mats) && all; + + all = serialize_list_other_mats( + mOtherMatsBars.mats, [&](const string& token) { bars_blocks->add_bars_other_mats(token); }, + mPile->settings.bars_blocks.bars_other_mats) && all; + + all = serialize_list_other_mats( + mOtherMatsBlocks.mats, [&](const string& token) { bars_blocks->add_blocks_other_mats(token); }, + mPile->settings.bars_blocks.blocks_other_mats) && all; + + return all; +} + +void StockpileSerializer::read_bars_blocks(DeserializeMode mode, const vector& filters) { + auto & pbarsblocks = mPile->settings.bars_blocks; + read_category("bars_blocks", mode, + std::bind(&StockpileSettings::has_barsblocks, mBuffer), + std::bind(&StockpileSettings::barsblocks, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_bars_blocks, + [&]() { + pbarsblocks.bars_mats.clear(); + pbarsblocks.bars_other_mats.clear(); + pbarsblocks.blocks_mats.clear(); + pbarsblocks.blocks_other_mats.clear(); + }, + [&](bool all, char val) { + auto & bbarsblocks = mBuffer.barsblocks(); + + unserialize_list_material("mats/bars", all, val, filters, bars_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bbarsblocks.bars_mats(idx); }, + bbarsblocks.bars_mats_size(), pbarsblocks.bars_mats); + + unserialize_list_other_mats("other/bars", all, val, filters, + mOtherMatsBars.mats, + [&](const size_t& idx) -> const string& { return bbarsblocks.bars_other_mats(idx); }, + bbarsblocks.bars_other_mats_size(), pbarsblocks.bars_other_mats); + + unserialize_list_material("mats/blocks", all, val, filters, + blocks_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bbarsblocks.blocks_mats(idx); }, + bbarsblocks.blocks_mats_size(), pbarsblocks.blocks_mats); + + unserialize_list_other_mats("other/blocks", all, val, filters, + mOtherMatsBlocks.mats, + [&](const size_t& idx) -> const string& { return bbarsblocks.blocks_other_mats(idx); }, + bbarsblocks.blocks_other_mats_size(), pbarsblocks.blocks_other_mats); + }); +} + +bool StockpileSerializer::write_cloth(StockpileSettings::ClothSet* cloth) { + bool all = true; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_thread_silk(token); }, + &mPile->settings.cloth.thread_silk, organic_mat_category::Silk) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_thread_plant(token); }, + &mPile->settings.cloth.thread_plant, organic_mat_category::PlantFiber) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_thread_yarn(token); }, + &mPile->settings.cloth.thread_yarn, organic_mat_category::Yarn) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_thread_metal(token); }, + &mPile->settings.cloth.thread_metal, organic_mat_category::MetalThread) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_cloth_silk(token); }, + &mPile->settings.cloth.cloth_silk, organic_mat_category::Silk) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_cloth_plant(token); }, + &mPile->settings.cloth.cloth_plant, organic_mat_category::PlantFiber) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_cloth_yarn(token); }, + &mPile->settings.cloth.cloth_yarn, organic_mat_category::Yarn) && all; + + all = serialize_list_organic_mat( + [&](const string& token) { cloth->add_cloth_metal(token); }, + &mPile->settings.cloth.cloth_metal, organic_mat_category::MetalThread) && all; + + return all; +} + +void StockpileSerializer::read_cloth(DeserializeMode mode, const vector& filters) { + auto & pcloth = mPile->settings.cloth; + read_category("cloth", mode, + std::bind(&StockpileSettings::has_cloth, mBuffer), + std::bind(&StockpileSettings::cloth, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_cloth, + [&]() { + pcloth.thread_silk.clear(); + pcloth.thread_yarn.clear(); + pcloth.thread_plant.clear(); + pcloth.thread_metal.clear(); + pcloth.cloth_silk.clear(); + pcloth.cloth_plant.clear(); + pcloth.cloth_yarn.clear(); + pcloth.cloth_metal.clear(); + }, + [&](bool all, char val) { + auto & bcloth = mBuffer.cloth(); + + unserialize_list_organic_mat("thread/silk", all, val, filters, + [&](size_t idx) -> string { return bcloth.thread_silk(idx); }, + bcloth.thread_silk_size(), pcloth.thread_silk, organic_mat_category::Silk); + + unserialize_list_organic_mat("thread/plant", all, val, filters, + [&](size_t idx) -> string { return bcloth.thread_plant(idx); }, + bcloth.thread_plant_size(), pcloth.thread_plant, organic_mat_category::PlantFiber); + + unserialize_list_organic_mat("thread/yarn", all, val, filters, + [&](size_t idx) -> string { return bcloth.thread_yarn(idx); }, + bcloth.thread_yarn_size(), pcloth.thread_yarn, organic_mat_category::Yarn); + + unserialize_list_organic_mat("thread/metal", all, val, filters, + [&](size_t idx) -> string { return bcloth.thread_metal(idx); }, + bcloth.thread_metal_size(), pcloth.thread_metal, organic_mat_category::MetalThread); + + unserialize_list_organic_mat("cloth/silk", all, val, filters, + [&](size_t idx) -> string { return bcloth.cloth_silk(idx); }, + bcloth.cloth_silk_size(), pcloth.cloth_silk, organic_mat_category::Silk); + + unserialize_list_organic_mat("cloth/plant", all, val, filters, + [&](size_t idx) -> string { return bcloth.cloth_plant(idx); }, + bcloth.cloth_plant_size(), pcloth.cloth_plant, organic_mat_category::PlantFiber); + + unserialize_list_organic_mat("cloth/yarn", all, val, filters, + [&](size_t idx) -> string { return bcloth.cloth_yarn(idx); }, + bcloth.cloth_yarn_size(), pcloth.cloth_yarn, organic_mat_category::Yarn); + + unserialize_list_organic_mat("cloth/metal", all, val, filters, + [&](size_t idx) -> string { return bcloth.cloth_metal(idx); }, + bcloth.cloth_metal_size(), pcloth.cloth_metal, organic_mat_category::MetalThread); + }); +} + +static bool coins_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid(); +} + +bool StockpileSerializer::write_coins(StockpileSettings::CoinSet* coins) { + return serialize_list_material( + coins_mat_is_allowed, + [&](const string& token) { coins->add_mats(token); }, + mPile->settings.coins.mats); +} + +void StockpileSerializer::read_coins(DeserializeMode mode, const vector& filters) { + auto & pcoins = mPile->settings.coins; + read_category("coin", mode, + std::bind(&StockpileSettings::has_coin, mBuffer), + std::bind(&StockpileSettings::coin, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_coins, + [&]() { + pcoins.mats.clear(); + }, + [&](bool all, char val) { + auto & bcoin = mBuffer.coin(); + + unserialize_list_material("", all, val, filters, coins_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bcoin.mats(idx); }, + bcoin.mats_size(), pcoins.mats); + }); +} + +static bool finished_goods_type_is_allowed(item_type::item_type type) { + switch (type) { + case item_type::CHAIN: + case item_type::FLASK: + case item_type::GOBLET: + case item_type::INSTRUMENT: + case item_type::TOY: + case item_type::ARMOR: + case item_type::SHOES: + case item_type::HELM: + case item_type::GLOVES: + case item_type::FIGURINE: + case item_type::AMULET: + case item_type::SCEPTER: + case item_type::CROWN: + case item_type::RING: + case item_type::EARRING: + case item_type::BRACELET: + case item_type::GEM: + case item_type::TOTEM: + case item_type::PANTS: + case item_type::BACKPACK: + case item_type::QUIVER: + case item_type::SPLINT: + case item_type::CRUTCH: + case item_type::TOOL: + case item_type::BOOK: + return true; + default: + return false; } } -StockpileSerializer::food_pair StockpileSerializer::food_map(organic_mat_category::organic_mat_category cat) { +static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_GEM) || mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); +} + +bool StockpileSerializer::write_finished_goods(StockpileSettings::FinishedGoodsSet* finished_goods) { + bool all = serialize_list_item_type( + finished_goods_type_is_allowed, + [&](const string& token) { finished_goods->add_type(token); }, + mPile->settings.finished_goods.type); + + all = serialize_list_material( + finished_goods_mat_is_allowed, + [&](const string& token) { finished_goods->add_mats(token); }, + mPile->settings.finished_goods.mats) && all; + + all = serialize_list_other_mats( + mOtherMatsFinishedGoods.mats, [&](const string& token) { finished_goods->add_other_mats(token); }, + mPile->settings.finished_goods.other_mats) && all; + + all = serialize_list_quality([&](const string& token) { finished_goods->add_quality_core(token); }, + mPile->settings.finished_goods.quality_core) && all; + + all = serialize_list_quality([&](const string& token) { finished_goods->add_quality_total(token); }, + mPile->settings.finished_goods.quality_total) && all; + + return all; +} + +void StockpileSerializer::read_finished_goods(DeserializeMode mode, const vector& filters) { + auto & pfinished_goods = mPile->settings.finished_goods; + read_category("finished_goods", mode, + std::bind(&StockpileSettings::has_finished_goods, mBuffer), + std::bind(&StockpileSettings::finished_goods, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_finished_goods, + [&]() { + pfinished_goods.type.clear(); + pfinished_goods.other_mats.clear(); + pfinished_goods.mats.clear(); + quality_clear(pfinished_goods.quality_core); + quality_clear(pfinished_goods.quality_total); + }, + [&](bool all, char val) { + auto & bfinished_goods = mBuffer.finished_goods(); + + unserialize_list_item_type("type", all, val, filters, finished_goods_type_is_allowed, + [&](const size_t& idx) -> const string& { return bfinished_goods.type(idx); }, + bfinished_goods.type_size(), pfinished_goods.type); + + unserialize_list_material("mats", all, val, filters, finished_goods_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bfinished_goods.mats(idx); }, + bfinished_goods.mats_size(), pfinished_goods.mats); + + unserialize_list_other_mats("other", all, val, filters, mOtherMatsFinishedGoods.mats, + [&](const size_t& idx) -> const string& { return bfinished_goods.other_mats(idx); }, + bfinished_goods.other_mats_size(), pfinished_goods.other_mats); + + unserialize_list_quality("core", all, val, filters, + [&](const size_t& idx) -> const string& { return bfinished_goods.quality_core(idx); }, + bfinished_goods.quality_core_size(), pfinished_goods.quality_core); + + unserialize_list_quality("total", all, val, filters, + [&](const size_t& idx) -> const string& { return bfinished_goods.quality_total(idx); }, + bfinished_goods.quality_total_size(), pfinished_goods.quality_total); + }); +} + +food_pair StockpileSerializer::food_map(organic_mat_category::organic_mat_category cat) { using df::enums::organic_mat_category::organic_mat_category; - using namespace std::placeholders; + switch (cat) { case organic_mat_category::Meat: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_meat(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().meat(idx); }; - return food_pair(setter, &mPile->settings.food.meat, getter, mBuffer.food().meat_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().meat(idx); }; + return food_pair("meat", setter, &mPile->settings.food.meat, getter, mBuffer.food().meat_size()); } case organic_mat_category::Fish: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_fish(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().fish(idx); }; - return food_pair(setter, &mPile->settings.food.fish, getter, mBuffer.food().fish_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().fish(idx); }; + return food_pair("fish/prepared", setter, &mPile->settings.food.fish, getter, mBuffer.food().fish_size()); } case organic_mat_category::UnpreparedFish: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_unprepared_fish(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().unprepared_fish(idx); }; - return food_pair(setter, &mPile->settings.food.unprepared_fish, getter, mBuffer.food().unprepared_fish_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().unprepared_fish(idx); }; + return food_pair("fish/unprepared", setter, &mPile->settings.food.unprepared_fish, getter, mBuffer.food().unprepared_fish_size()); } case organic_mat_category::Eggs: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_egg(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().egg(idx); }; - return food_pair(setter, &mPile->settings.food.egg, getter, mBuffer.food().egg_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().egg(idx); }; + return food_pair("egg", setter, &mPile->settings.food.egg, getter, mBuffer.food().egg_size()); } case organic_mat_category::Plants: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_plants(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().plants(idx); }; - return food_pair(setter, &mPile->settings.food.plants, getter, mBuffer.food().plants_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().plants(idx); }; + return food_pair("plants", setter, &mPile->settings.food.plants, getter, mBuffer.food().plants_size()); } case organic_mat_category::PlantDrink: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_drink_plant(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().drink_plant(idx); }; - return food_pair(setter, &mPile->settings.food.drink_plant, getter, mBuffer.food().drink_plant_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().drink_plant(idx); }; + return food_pair("drink/plant", setter, &mPile->settings.food.drink_plant, getter, mBuffer.food().drink_plant_size()); } case organic_mat_category::CreatureDrink: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_drink_animal(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().drink_animal(idx); }; - return food_pair(setter, &mPile->settings.food.drink_animal, getter, mBuffer.food().drink_animal_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().drink_animal(idx); }; + return food_pair("drink/animal", setter, &mPile->settings.food.drink_animal, getter, mBuffer.food().drink_animal_size()); } case organic_mat_category::PlantCheese: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_cheese_plant(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().cheese_plant(idx); }; - return food_pair(setter, &mPile->settings.food.cheese_plant, getter, mBuffer.food().cheese_plant_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().cheese_plant(idx); }; + return food_pair("cheese/plant", setter, &mPile->settings.food.cheese_plant, getter, mBuffer.food().cheese_plant_size()); } case organic_mat_category::CreatureCheese: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_cheese_animal(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().cheese_animal(idx); }; - return food_pair(setter, &mPile->settings.food.cheese_animal, getter, mBuffer.food().cheese_animal_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().cheese_animal(idx); }; + return food_pair("cheese/animal", setter, &mPile->settings.food.cheese_animal, getter, mBuffer.food().cheese_animal_size()); } case organic_mat_category::Seed: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_seeds(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().seeds(idx); }; - return food_pair(setter, &mPile->settings.food.seeds, getter, mBuffer.food().seeds_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().seeds(idx); }; + return food_pair("seeds", setter, &mPile->settings.food.seeds, getter, mBuffer.food().seeds_size()); } case organic_mat_category::Leaf: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_leaves(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().leaves(idx); }; - return food_pair(setter, &mPile->settings.food.leaves, getter, mBuffer.food().leaves_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().leaves(idx); }; + return food_pair("leaves", setter, &mPile->settings.food.leaves, getter, mBuffer.food().leaves_size()); } case organic_mat_category::PlantPowder: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_powder_plant(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().powder_plant(idx); }; - return food_pair(setter, &mPile->settings.food.powder_plant, getter, mBuffer.food().powder_plant_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().powder_plant(idx); }; + return food_pair("powder/plant", setter, &mPile->settings.food.powder_plant, getter, mBuffer.food().powder_plant_size()); } case organic_mat_category::CreaturePowder: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_powder_creature(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().powder_creature(idx); }; - return food_pair(setter, &mPile->settings.food.powder_creature, getter, mBuffer.food().powder_creature_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().powder_creature(idx); }; + return food_pair("powder/animal", setter, &mPile->settings.food.powder_creature, getter, mBuffer.food().powder_creature_size()); } case organic_mat_category::Glob: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_glob(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().glob(idx); }; - return food_pair(setter, &mPile->settings.food.glob, getter, mBuffer.food().glob_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().glob(idx); }; + return food_pair("glob", setter, &mPile->settings.food.glob, getter, mBuffer.food().glob_size()); } case organic_mat_category::PlantLiquid: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_liquid_plant(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().liquid_plant(idx); }; - return food_pair(setter, &mPile->settings.food.liquid_plant, getter, mBuffer.food().liquid_plant_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().liquid_plant(idx); }; + return food_pair("liquid/plant", setter, &mPile->settings.food.liquid_plant, getter, mBuffer.food().liquid_plant_size()); } case organic_mat_category::CreatureLiquid: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_liquid_animal(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().liquid_animal(idx); }; - return food_pair(setter, &mPile->settings.food.liquid_animal, getter, mBuffer.food().liquid_animal_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().liquid_animal(idx); }; + return food_pair("liquid/animal", setter, &mPile->settings.food.liquid_animal, getter, mBuffer.food().liquid_animal_size()); } case organic_mat_category::MiscLiquid: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_liquid_misc(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().liquid_misc(idx); }; - return food_pair(setter, &mPile->settings.food.liquid_misc, getter, mBuffer.food().liquid_misc_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().liquid_misc(idx); }; + return food_pair("liquid/misc", setter, &mPile->settings.food.liquid_misc, getter, mBuffer.food().liquid_misc_size()); } case organic_mat_category::Paste: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_glob_paste(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().glob_paste(idx); }; - return food_pair(setter, &mPile->settings.food.glob_paste, getter, mBuffer.food().glob_paste_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().glob_paste(idx); }; + return food_pair("paste", setter, &mPile->settings.food.glob_paste, getter, mBuffer.food().glob_paste_size()); } case organic_mat_category::Pressed: { - FuncWriteExport setter = [=](const std::string& id) { + FuncWriteExport setter = [&](const string& id) { mBuffer.mutable_food()->add_glob_pressed(id); }; - FuncReadImport getter = [=](size_t idx) -> std::string { return mBuffer.food().glob_pressed(idx); }; - return food_pair(setter, &mPile->settings.food.glob_pressed, getter, mBuffer.food().glob_pressed_size()); + FuncReadImport getter = [&](size_t idx) -> string { return mBuffer.food().glob_pressed(idx); }; + return food_pair("pressed", setter, &mPile->settings.food.glob_pressed, getter, mBuffer.food().glob_pressed_size()); } case organic_mat_category::Leather: case organic_mat_category::Silk: @@ -633,10 +1623,11 @@ StockpileSerializer::food_pair StockpileSerializer::food_map(organic_mat_categor return food_pair(); } -void StockpileSerializer::write_food() { - StockpileSettings::FoodSet* food = mBuffer.mutable_food(); - DEBUG(log).print("food:\n"); - food->set_prepared_meals(mPile->settings.food.prepared_meals); +bool StockpileSerializer::write_food(StockpileSettings::FoodSet* food) { + auto & pfood = mPile->settings.food; + bool all = pfood.prepared_meals; + + food->set_prepared_meals(pfood.prepared_meals); using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; @@ -644,1138 +1635,627 @@ void StockpileSerializer::write_food() { food_pair p = food_map((organic_mat_category)mat_category); if (!p.valid) continue; - DEBUG(log).print("food: %s\n", traits::key_table[mat_category]); - serialize_list_organic_mat(p.set_value, p.stockpile_values, (organic_mat_category)mat_category); + all = serialize_list_organic_mat(p.set_value, p.stockpile_values, + (organic_mat_category)mat_category) && all; } + + return all; } -void StockpileSerializer::read_food() { +void StockpileSerializer::read_food(DeserializeMode mode, const vector& filters) { using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; - if (mBuffer.has_food()) { - mPile->settings.flags.bits.food = 1; - const StockpileSettings::FoodSet food = mBuffer.food(); - DEBUG(log).print("food:\n"); - - if (food.has_prepared_meals()) - mPile->settings.food.prepared_meals = food.prepared_meals(); - else - mPile->settings.food.prepared_meals = true; - - DEBUG(log).print("prepared_meals: %d\n", mPile->settings.food.prepared_meals); - - for (int32_t mat_category = traits::first_item_value; mat_category < traits::last_item_value; ++mat_category) { - food_pair p = food_map((organic_mat_category)mat_category); - if (!p.valid) - continue; - unserialize_list_organic_mat(p.get_value, p.serialized_count, p.stockpile_values, (organic_mat_category)mat_category); - } - } - else { - for (int32_t mat_category = traits::first_item_value; mat_category < traits::last_item_value; ++mat_category) { - food_pair p = food_map((organic_mat_category)mat_category); - if (!p.valid) - continue; - p.stockpile_values->clear(); - } - mPile->settings.flags.bits.food = 0; - mPile->settings.food.prepared_meals = false; - } -} - -void StockpileSerializer::furniture_setup_other_mats() { - mOtherMatsFurniture.insert(std::make_pair(0, "WOOD")); - mOtherMatsFurniture.insert(std::make_pair(1, "PLANT_CLOTH")); - mOtherMatsFurniture.insert(std::make_pair(2, "BONE")); - mOtherMatsFurniture.insert(std::make_pair(3, "TOOTH")); - mOtherMatsFurniture.insert(std::make_pair(4, "HORN")); - mOtherMatsFurniture.insert(std::make_pair(5, "PEARL")); - mOtherMatsFurniture.insert(std::make_pair(6, "SHELL")); - mOtherMatsFurniture.insert(std::make_pair(7, "LEATHER")); - mOtherMatsFurniture.insert(std::make_pair(8, "SILK")); - mOtherMatsFurniture.insert(std::make_pair(9, "AMBER")); - mOtherMatsFurniture.insert(std::make_pair(10, "CORAL")); - mOtherMatsFurniture.insert(std::make_pair(11, "GREEN_GLASS")); - mOtherMatsFurniture.insert(std::make_pair(12, "CLEAR_GLASS")); - mOtherMatsFurniture.insert(std::make_pair(13, "CRYSTAL_GLASS")); - mOtherMatsFurniture.insert(std::make_pair(14, "YARN")); -} - -void StockpileSerializer::write_furniture() { - StockpileSettings::FurnitureSet* furniture = mBuffer.mutable_furniture(); - - // FURNITURE type - using df::enums::furniture_type::furniture_type; - using type_traits = df::enum_traits; - for (size_t i = 0; i < mPile->settings.furniture.type.size(); ++i) { - if (mPile->settings.furniture.type.at(i)) { - std::string f_type(type_traits::key_table[i]); - furniture->add_type(f_type); - DEBUG(log).print("furniture_type %zd is %s\n", i, f_type.c_str()); - } - } - // metal, stone/clay materials - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::furniture_mat_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { furniture->add_mats(token); }, - mPile->settings.furniture.mats); - - // other mats - serialize_list_other_mats( - mOtherMatsFurniture, [=](const std::string& token) { furniture->add_other_mats(token); }, - mPile->settings.furniture.other_mats); - - serialize_list_quality([=](const std::string& token) { furniture->add_quality_core(token); }, - mPile->settings.furniture.quality_core); - serialize_list_quality([=](const std::string& token) { furniture->add_quality_total(token); }, - mPile->settings.furniture.quality_total); -} - -bool StockpileSerializer::furniture_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); -} -void StockpileSerializer::read_furniture() { - if (mBuffer.has_furniture()) { - mPile->settings.flags.bits.furniture = 1; - const StockpileSettings::FurnitureSet furniture = mBuffer.furniture(); - DEBUG(log).print("furniture:\n"); - - // type - using df::enums::furniture_type::furniture_type; - df::enum_traits type_traits; - mPile->settings.furniture.type.clear(); - mPile->settings.furniture.type.resize(type_traits.last_item_value + 1, '\0'); - if (furniture.type_size() > 0) { - for (int i = 0; i < furniture.type_size(); ++i) { - const std::string type = furniture.type(i); - df::enum_traits::base_type idx = linear_index(type_traits, type); - DEBUG(log).print("type %d is %s\n", idx, type.c_str()); - if (idx < 0 || size_t(idx) >= mPile->settings.furniture.type.size()) { - WARN(log).print("furniture type index invalid %s, idx=%d\n", type.c_str(), idx); + auto & pfood = mPile->settings.food; + read_category("food", mode, + std::bind(&StockpileSettings::has_food, mBuffer), + std::bind(&StockpileSettings::food, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_food, + [&]() { + pfood.prepared_meals = false; + for (int32_t mat_category = traits::first_item_value; mat_category < traits::last_item_value; ++mat_category) { + food_pair p = food_map((organic_mat_category)mat_category); + if (!p.valid) continue; - } - mPile->settings.furniture.type.at(idx) = 1; + p.stockpile_values->clear(); } - } - - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::furniture_mat_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return furniture.mats(idx); }, - furniture.mats_size(), &mPile->settings.furniture.mats); - - // other materials - unserialize_list_other_mats( - mOtherMatsFurniture, [=](const size_t& idx) -> const std::string& { return furniture.other_mats(idx); }, - furniture.other_mats_size(), &mPile->settings.furniture.other_mats); - - // core quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return furniture.quality_core(idx); }, - furniture.quality_core_size(), mPile->settings.furniture.quality_core); - - // total quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return furniture.quality_total(idx); }, - furniture.quality_total_size(), mPile->settings.furniture.quality_total); - } - else { - mPile->settings.flags.bits.furniture = 0; - mPile->settings.furniture.type.clear(); - mPile->settings.furniture.other_mats.clear(); - mPile->settings.furniture.mats.clear(); - quality_clear(mPile->settings.furniture.quality_core); - quality_clear(mPile->settings.furniture.quality_total); - } -} - -bool StockpileSerializer::refuse_creature_is_allowed(const df::creature_raw* raw) { - if (!raw) - return false; - // wagon and generated creatures not allowed, except angels - const bool is_wagon = raw->creature_id == "EQUIPMENT_WAGON"; - const bool is_generated = raw->flags.is_set(creature_raw_flags::GENERATED); - const bool is_angel = is_generated && raw->creature_id.find("DIVINE_") != std::string::npos; - return !is_wagon && !(is_generated && !is_angel); -} - -void StockpileSerializer::refuse_write_helper(std::function add_value, const vector& list) { - for (size_t i = 0; i < list.size(); ++i) { - if (list.at(i) == 1) { - df::creature_raw* r = find_creature(i); - // skip forgotten beasts, titans, demons, and night creatures - if (!refuse_creature_is_allowed(r)) - continue; - DEBUG(log).print("creature %s %zd\n", r->creature_id.c_str(), i); - add_value(r->creature_id); - } - } -} - -bool StockpileSerializer::refuse_type_is_allowed(item_type::item_type type) { - if (type == item_type::NONE || type == item_type::BAR || type == item_type::SMALLGEM || type == item_type::BLOCKS || type == item_type::ROUGH || type == item_type::BOULDER || type == item_type::CORPSE || type == item_type::CORPSEPIECE || type == item_type::ROCK || type == item_type::ORTHOPEDIC_CAST) - return false; - return true; -} - -void StockpileSerializer::write_refuse() { - DEBUG(log).print("refuse:\n"); - StockpileSettings::RefuseSet* refuse = mBuffer.mutable_refuse(); - refuse->set_fresh_raw_hide(mPile->settings.refuse.fresh_raw_hide); - refuse->set_rotten_raw_hide(mPile->settings.refuse.rotten_raw_hide); - - // type - DEBUG(log).print("getting types\n"); - FuncItemAllowed filter = std::bind(&StockpileSerializer::refuse_type_is_allowed, this, _1); - serialize_list_item_type( - filter, [=](const std::string& token) { - DEBUG(log).print("adding type: %s\n", token.c_str()); - refuse->add_type(token); }, - mPile->settings.refuse.type); - - // corpses - refuse_write_helper([=](const std::string& id) { refuse->add_corpses(id); }, - mPile->settings.refuse.corpses); - // body_parts - refuse_write_helper([=](const std::string& id) { refuse->add_body_parts(id); }, - mPile->settings.refuse.body_parts); - // skulls - refuse_write_helper([=](const std::string& id) { refuse->add_skulls(id); }, - mPile->settings.refuse.skulls); - // bones - refuse_write_helper([=](const std::string& id) { refuse->add_bones(id); }, - mPile->settings.refuse.bones); - // hair - refuse_write_helper([=](const std::string& id) { refuse->add_hair(id); }, - mPile->settings.refuse.hair); - // shells - refuse_write_helper([=](const std::string& id) { refuse->add_shells(id); }, - mPile->settings.refuse.shells); - // teeth - refuse_write_helper([=](const std::string& id) { refuse->add_teeth(id); }, - mPile->settings.refuse.teeth); - // horns - refuse_write_helper([=](const std::string& id) { refuse->add_horns(id); }, - mPile->settings.refuse.horns); -} - -void StockpileSerializer::refuse_read_helper(std::function get_value, size_t list_size, std::vector* pile_list) { - pile_list->clear(); - pile_list->resize(world->raws.creatures.all.size(), '\0'); - if (list_size > 0) { - for (size_t i = 0; i < list_size; ++i) { - const std::string creature_id = get_value(i); - const int idx = find_creature(creature_id); - const df::creature_raw* creature = find_creature(idx); - if (idx < 0 || !refuse_creature_is_allowed(creature) || size_t(idx) >= pile_list->size()) { - WARN(log).print("invalid refuse creature %s, idx=%d\n", creature_id.c_str(), idx); - continue; - } - DEBUG(log).print("creature %d is %s\n", idx, creature_id.c_str()); - pile_list->at(idx) = 1; - } - } -} - -void StockpileSerializer::read_refuse() { - if (mBuffer.has_refuse()) { - mPile->settings.flags.bits.refuse = 1; - const StockpileSettings::RefuseSet refuse = mBuffer.refuse(); - DEBUG(log).print("refuse:\n"); - DEBUG(log).print(" fresh hide %d\n", refuse.fresh_raw_hide()); - DEBUG(log).print(" rotten hide %d\n", refuse.rotten_raw_hide()); - mPile->settings.refuse.fresh_raw_hide = refuse.fresh_raw_hide(); - mPile->settings.refuse.rotten_raw_hide = refuse.rotten_raw_hide(); - - // type - FuncItemAllowed filter = std::bind(&StockpileSerializer::refuse_type_is_allowed, this, _1); - unserialize_list_item_type( - filter, [=](const size_t& idx) -> const std::string& { return refuse.type(idx); }, - refuse.type_size(), &mPile->settings.refuse.type); - - // corpses - DEBUG(log).print(" corpses\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.corpses(idx); }, - refuse.corpses_size(), &mPile->settings.refuse.corpses); - // body_parts - DEBUG(log).print(" body_parts\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.body_parts(idx); }, - refuse.body_parts_size(), &mPile->settings.refuse.body_parts); - // skulls - DEBUG(log).print(" skulls\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.skulls(idx); }, - refuse.skulls_size(), &mPile->settings.refuse.skulls); - // bones - DEBUG(log).print(" bones\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.bones(idx); }, - refuse.bones_size(), &mPile->settings.refuse.bones); - // hair - DEBUG(log).print(" hair\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.hair(idx); }, - refuse.hair_size(), &mPile->settings.refuse.hair); - // shells - DEBUG(log).print(" shells\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.shells(idx); }, - refuse.shells_size(), &mPile->settings.refuse.shells); - // teeth - DEBUG(log).print(" teeth\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.teeth(idx); }, - refuse.teeth_size(), &mPile->settings.refuse.teeth); - // horns - DEBUG(log).print(" horns\n"); - refuse_read_helper([=](const size_t& idx) -> const std::string& { return refuse.horns(idx); }, - refuse.horns_size(), &mPile->settings.refuse.horns); - } - else { - mPile->settings.flags.bits.refuse = 0; - mPile->settings.refuse.type.clear(); - mPile->settings.refuse.corpses.clear(); - mPile->settings.refuse.body_parts.clear(); - mPile->settings.refuse.skulls.clear(); - mPile->settings.refuse.bones.clear(); - mPile->settings.refuse.hair.clear(); - mPile->settings.refuse.shells.clear(); - mPile->settings.refuse.teeth.clear(); - mPile->settings.refuse.horns.clear(); - mPile->settings.refuse.fresh_raw_hide = false; - mPile->settings.refuse.rotten_raw_hide = false; - } -} - -bool StockpileSerializer::stone_is_allowed(const MaterialInfo& mi) { - if (!mi.isValid()) - return false; - const bool is_allowed_soil = mi.inorganic->flags.is_set(inorganic_flags::SOIL) && !mi.inorganic->flags.is_set(inorganic_flags::AQUIFER); - const bool is_allowed_stone = mi.material->flags.is_set(material_flags::IS_STONE) && !mi.material->flags.is_set(material_flags::NO_STONE_STOCKPILE); - return is_allowed_soil || is_allowed_stone; -} + [&](bool all, char val) { + auto & bfood = mBuffer.food(); -void StockpileSerializer::write_stone() { - StockpileSettings::StoneSet* stone = mBuffer.mutable_stone(); + set_flag("preparedmeals", filters, all, val, bfood.prepared_meals(), pfood.prepared_meals); - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::stone_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { stone->add_mats(token); }, - mPile->settings.stone.mats); -} - -void StockpileSerializer::read_stone() { - if (mBuffer.has_stone()) { - mPile->settings.flags.bits.stone = 1; - const StockpileSettings::StoneSet stone = mBuffer.stone(); - DEBUG(log).print("stone:\n"); + for (int32_t mat_category = traits::first_item_value; mat_category < traits::last_item_value; ++mat_category) { + food_pair p = food_map((organic_mat_category)mat_category); + if (!p.valid) + continue; + unserialize_list_organic_mat(p.name, all, val, filters, + p.get_value, p.serialized_count, *p.stockpile_values, + (organic_mat_category)mat_category); + } + }); - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::stone_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return stone.mats(idx); }, - stone.mats_size(), &mPile->settings.stone.mats); - } - else { - mPile->settings.flags.bits.stone = 0; - mPile->settings.stone.mats.clear(); - } } -bool StockpileSerializer::ammo_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); +static bool furniture_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); } -void StockpileSerializer::write_ammo() { - StockpileSettings::AmmoSet* ammo = mBuffer.mutable_ammo(); - - // ammo type - serialize_list_itemdef([=](const std::string& token) { ammo->add_type(token); }, - mPile->settings.ammo.type, - std::vector(world->raws.itemdefs.ammo.begin(), world->raws.itemdefs.ammo.end()), - item_type::AMMO); - - // metal - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::ammo_mat_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { ammo->add_mats(token); }, - mPile->settings.ammo.mats); +bool StockpileSerializer::write_furniture(StockpileSettings::FurnitureSet* furniture) { + using df::enums::furniture_type::furniture_type; + using type_traits = df::enum_traits; - // other mats - only wood and bone - if (mPile->settings.ammo.other_mats.size() > 2) { - WARN(log).print("ammo other materials > 2: %zd\n", mPile->settings.ammo.other_mats.size()); - } + auto & pfurniture = mPile->settings.furniture; + bool all = true; - for (size_t i = 0; i < std::min(size_t(2), mPile->settings.ammo.other_mats.size()); ++i) { - if (!mPile->settings.ammo.other_mats.at(i)) + for (size_t i = 0; i < pfurniture.type.size(); ++i) { + if (!pfurniture.type.at(i)) { + all = false; continue; - const std::string token = i == 0 ? "WOOD" : "BONE"; - ammo->add_other_mats(token); - DEBUG(log).print(" other mats %zd is %s\n", i, token.c_str()); - } - - // quality core - serialize_list_quality([=](const std::string& token) { ammo->add_quality_core(token); }, - mPile->settings.ammo.quality_core); - - // quality total - serialize_list_quality([=](const std::string& token) { ammo->add_quality_total(token); }, - mPile->settings.ammo.quality_total); -} - -void StockpileSerializer::read_ammo() { - if (mBuffer.has_ammo()) { - mPile->settings.flags.bits.ammo = 1; - const StockpileSettings::AmmoSet ammo = mBuffer.ammo(); - DEBUG(log).print("ammo:\n"); - - // ammo type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return ammo.type(idx); }, - ammo.type_size(), &mPile->settings.ammo.type, item_type::AMMO); - - // materials metals - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::ammo_mat_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return ammo.mats(idx); }, - ammo.mats_size(), &mPile->settings.ammo.mats); - - // others - mPile->settings.ammo.other_mats.clear(); - mPile->settings.ammo.other_mats.resize(2, '\0'); - if (ammo.other_mats_size() > 0) { - // TODO remove hardcoded value - for (int i = 0; i < ammo.other_mats_size(); ++i) { - const std::string token = ammo.other_mats(i); - const int32_t idx = token == "WOOD" ? 0 : token == "BONE" ? 1 - : -1; - DEBUG(log).print("other mats %d is %s\n", idx, token.c_str()); - if (idx != -1) - mPile->settings.ammo.other_mats.at(idx) = 1; - } } + string f_type(type_traits::key_table[i]); + furniture->add_type(f_type); + DEBUG(log).print("furniture_type %zd is %s\n", i, f_type.c_str()); + } + + all = serialize_list_material( + furniture_mat_is_allowed, + [&](const string& token) { furniture->add_mats(token); }, + pfurniture.mats) && all; + all = serialize_list_other_mats( + mOtherMatsFurniture.mats, + [&](const string& token) { furniture->add_other_mats(token); }, + pfurniture.other_mats) && all; + all = serialize_list_quality( + [&](const string& token) { furniture->add_quality_core(token); }, + pfurniture.quality_core) && all; + all = serialize_list_quality( + [&](const string& token) { furniture->add_quality_total(token); }, + pfurniture.quality_total) && all; + + return all; +} + +void StockpileSerializer::read_furniture(DeserializeMode mode, const vector& filters) { + auto & pfurniture = mPile->settings.furniture; + read_category("furniture", mode, + std::bind(&StockpileSettings::has_furniture, mBuffer), + std::bind(&StockpileSettings::furniture, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_furniture, + [&]() { + pfurniture.type.clear(); + pfurniture.other_mats.clear(); + pfurniture.mats.clear(); + quality_clear(pfurniture.quality_core); + quality_clear(pfurniture.quality_total); + }, + [&](bool all, char val) { + auto & bfurniture = mBuffer.furniture(); + + using df::enums::furniture_type::furniture_type; + df::enum_traits type_traits; + size_t num_elems = type_traits.last_item_value + 1; + pfurniture.type.resize(num_elems, '\0'); + + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + string id = ENUM_KEY_STR(furniture_type, (df::furniture_type)idx); + set_filter_elem("type", filters, val, id, idx, pfurniture.type.at(idx)); + } + } else { + for (int i = 0; i < bfurniture.type_size(); ++i) { + const string token = bfurniture.type(i); + df::enum_traits::base_type idx = linear_index(type_traits, token); + if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { + WARN(log).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); + continue; + } + set_filter_elem("type", filters, val, token, idx, pfurniture.type.at(idx)); + } + } - // core quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return ammo.quality_core(idx); }, - ammo.quality_core_size(), mPile->settings.ammo.quality_core); - - // total quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return ammo.quality_total(idx); }, - ammo.quality_total_size(), mPile->settings.ammo.quality_total); - } - else { - mPile->settings.flags.bits.ammo = 0; - mPile->settings.ammo.type.clear(); - mPile->settings.ammo.mats.clear(); - mPile->settings.ammo.other_mats.clear(); - quality_clear(mPile->settings.ammo.quality_core); - quality_clear(mPile->settings.ammo.quality_total); - } -} - -bool StockpileSerializer::coins_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid(); -} + unserialize_list_material("mats", all, val, filters, furniture_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bfurniture.mats(idx); }, + bfurniture.mats_size(), pfurniture.mats); -void StockpileSerializer::write_coins() { - StockpileSettings::CoinSet* coins = mBuffer.mutable_coin(); - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::coins_mat_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { coins->add_mats(token); }, - mPile->settings.coins.mats); -} + unserialize_list_other_mats("other", all, val, filters, + mOtherMatsFurniture.mats, [&](const size_t& idx) -> const string& { return bfurniture.other_mats(idx); }, + bfurniture.other_mats_size(), pfurniture.other_mats); -void StockpileSerializer::read_coins() { - if (mBuffer.has_coin()) { - mPile->settings.flags.bits.coins = 1; - const StockpileSettings::CoinSet coins = mBuffer.coin(); - DEBUG(log).print("coins:\n"); + unserialize_list_quality("core", all, val, filters, + [&](const size_t& idx) -> const string& { return bfurniture.quality_core(idx); }, + bfurniture.quality_core_size(), pfurniture.quality_core); - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::coins_mat_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return coins.mats(idx); }, - coins.mats_size(), &mPile->settings.coins.mats); - } - else { - mPile->settings.flags.bits.coins = 0; - mPile->settings.coins.mats.clear(); - } + unserialize_list_quality("total", all, val, filters, + [&](const size_t& idx) -> const string& { return bfurniture.quality_total(idx); }, + bfurniture.quality_total_size(), pfurniture.quality_total); + }); } -void StockpileSerializer::bars_blocks_setup_other_mats() { - mOtherMatsBars.insert(std::make_pair(0, "COAL")); - mOtherMatsBars.insert(std::make_pair(1, "POTASH")); - mOtherMatsBars.insert(std::make_pair(2, "ASH")); - mOtherMatsBars.insert(std::make_pair(3, "PEARLASH")); - mOtherMatsBars.insert(std::make_pair(4, "SOAP")); - - mOtherMatsBlocks.insert(std::make_pair(0, "GREEN_GLASS")); - mOtherMatsBlocks.insert(std::make_pair(1, "CLEAR_GLASS")); - mOtherMatsBlocks.insert(std::make_pair(2, "CRYSTAL_GLASS")); - mOtherMatsBlocks.insert(std::make_pair(3, "WOOD")); +static bool gem_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_GEM); } -bool StockpileSerializer::bars_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); +static bool gem_cut_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_GEM) || mi.material->flags.is_set(material_flags::IS_STONE)); } -bool StockpileSerializer::blocks_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); +static bool gem_other_mat_is_allowed(MaterialInfo& mi) { + return mi.isValid() && (mi.getToken() == "GLASS_GREEN" || mi.getToken() == "GLASS_CLEAR" || mi.getToken() == "GLASS_CRYSTAL"); } -void StockpileSerializer::write_bars_blocks() { - StockpileSettings::BarsBlocksSet* bars_blocks = mBuffer.mutable_barsblocks(); +bool StockpileSerializer::write_gems(StockpileSettings::GemsSet* gems) { MaterialInfo mi; - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::bars_mat_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { bars_blocks->add_bars_mats(token); }, - mPile->settings.bars_blocks.bars_mats); - // blocks mats - filter = std::bind(&StockpileSerializer::blocks_mat_is_allowed, this, _1); - serialize_list_material( - filter, [=](const std::string& token) { bars_blocks->add_blocks_mats(token); }, - mPile->settings.bars_blocks.blocks_mats); - - // bars other mats - serialize_list_other_mats( - mOtherMatsBars, [=](const std::string& token) { bars_blocks->add_bars_other_mats(token); }, - mPile->settings.bars_blocks.bars_other_mats); - - // blocks other mats - serialize_list_other_mats( - mOtherMatsBlocks, [=](const std::string& token) { bars_blocks->add_blocks_other_mats(token); }, - mPile->settings.bars_blocks.blocks_other_mats); -} - -void StockpileSerializer::read_bars_blocks() { - if (mBuffer.has_barsblocks()) { - mPile->settings.flags.bits.bars_blocks = 1; - const StockpileSettings::BarsBlocksSet bars_blocks = mBuffer.barsblocks(); - DEBUG(log).print("bars_blocks:\n"); - // bars - FuncMaterialAllowed filter = std::bind(&StockpileSerializer::bars_mat_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return bars_blocks.bars_mats(idx); }, - bars_blocks.bars_mats_size(), &mPile->settings.bars_blocks.bars_mats); - - // blocks - filter = std::bind(&StockpileSerializer::blocks_mat_is_allowed, this, _1); - unserialize_list_material( - filter, [=](const size_t& idx) -> const std::string& { return bars_blocks.blocks_mats(idx); }, - bars_blocks.blocks_mats_size(), &mPile->settings.bars_blocks.blocks_mats); - // bars other mats - unserialize_list_other_mats( - mOtherMatsBars, [=](const size_t& idx) -> const std::string& { return bars_blocks.bars_other_mats(idx); }, - bars_blocks.bars_other_mats_size(), &mPile->settings.bars_blocks.bars_other_mats); - - // blocks other mats - unserialize_list_other_mats( - mOtherMatsBlocks, [=](const size_t& idx) -> const std::string& { return bars_blocks.blocks_other_mats(idx); }, - bars_blocks.blocks_other_mats_size(), &mPile->settings.bars_blocks.blocks_other_mats); - } - else { - mPile->settings.flags.bits.bars_blocks = 0; - mPile->settings.bars_blocks.bars_other_mats.clear(); - mPile->settings.bars_blocks.bars_mats.clear(); - mPile->settings.bars_blocks.blocks_other_mats.clear(); - mPile->settings.bars_blocks.blocks_mats.clear(); - } -} - -bool StockpileSerializer::gem_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_GEM); -} -bool StockpileSerializer::gem_cut_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_GEM) || mi.material->flags.is_set(material_flags::IS_STONE)); -} -bool StockpileSerializer::gem_other_mat_is_allowed(MaterialInfo& mi) { - return mi.isValid() && (mi.getToken() == "GLASS_GREEN" || mi.getToken() == "GLASS_CLEAR" || mi.getToken() == "GLASS_CRYSTAL"); -} + auto & pgems = mPile->settings.gems; -void StockpileSerializer::write_gems() { - StockpileSettings::GemsSet* gems = mBuffer.mutable_gems(); - MaterialInfo mi; - // rough mats - FuncMaterialAllowed filter_rough = std::bind(&StockpileSerializer::gem_mat_is_allowed, this, _1); - serialize_list_material( - filter_rough, [=](const std::string& token) { gems->add_rough_mats(token); }, - mPile->settings.gems.rough_mats); - // cut mats - FuncMaterialAllowed filter_cut = std::bind(&StockpileSerializer::gem_cut_mat_is_allowed, this, _1); - serialize_list_material( - filter_cut, [=](const std::string& token) { gems->add_cut_mats(token); }, - mPile->settings.gems.cut_mats); - // rough other - for (size_t i = 0; i < mPile->settings.gems.rough_other_mats.size(); ++i) { - if (mPile->settings.gems.rough_other_mats.at(i)) { - mi.decode(i, -1); - if (!gem_other_mat_is_allowed(mi)) - continue; - DEBUG(log).print("gem rough_other mat %zd is %s\n", i, mi.getToken().c_str()); - gems->add_rough_other_mats(mi.getToken()); - } - } - // cut other - for (size_t i = 0; i < mPile->settings.gems.cut_other_mats.size(); ++i) { - if (mPile->settings.gems.cut_other_mats.at(i)) { - mi.decode(i, -1); - if (!mi.isValid()) - mi.decode(0, i); - if (!gem_other_mat_is_allowed(mi)) - continue; - DEBUG(log).print("gem cut_other mat %zd is %s\n", i, mi.getToken().c_str()); - gems->add_cut_other_mats(mi.getToken()); - } - } -} + bool all = serialize_list_material( + gem_mat_is_allowed, + [&](const string& token) { gems->add_rough_mats(token); }, + pgems.rough_mats); -void StockpileSerializer::read_gems() { - if (mBuffer.has_gems()) { - mPile->settings.flags.bits.gems = 1; - const StockpileSettings::GemsSet gems = mBuffer.gems(); - DEBUG(log).print("gems:\n"); - // rough - FuncMaterialAllowed filter_rough = std::bind(&StockpileSerializer::gem_mat_is_allowed, this, _1); - unserialize_list_material( - filter_rough, [=](const size_t& idx) -> const std::string& { return gems.rough_mats(idx); }, - gems.rough_mats_size(), &mPile->settings.gems.rough_mats); - - // cut - FuncMaterialAllowed filter_cut = std::bind(&StockpileSerializer::gem_cut_mat_is_allowed, this, _1); - unserialize_list_material( - filter_cut, [=](const size_t& idx) -> const std::string& { return gems.cut_mats(idx); }, - gems.cut_mats_size(), &mPile->settings.gems.cut_mats); - - const size_t builtin_size = std::extentraws.mat_table.builtin)>::value; - // rough other - mPile->settings.gems.rough_other_mats.clear(); - mPile->settings.gems.rough_other_mats.resize(builtin_size, '\0'); - for (int i = 0; i < gems.rough_other_mats_size(); ++i) { - const std::string token = gems.rough_other_mats(i); - MaterialInfo mi; - mi.find(token); - if (!mi.isValid() || size_t(mi.type) >= builtin_size) { - WARN(log).print("invalid gem mat %s idx=%d\n", token.c_str(), mi.type); - continue; - } - DEBUG(log).print("rough_other mats %d is %s\n", mi.type, token.c_str()); - mPile->settings.gems.rough_other_mats.at(mi.type) = 1; - } + all = serialize_list_material( + gem_cut_mat_is_allowed, + [&](const string& token) { gems->add_cut_mats(token); }, + pgems.cut_mats) && all; - // cut other - mPile->settings.gems.cut_other_mats.clear(); - mPile->settings.gems.cut_other_mats.resize(builtin_size, '\0'); - for (int i = 0; i < gems.cut_other_mats_size(); ++i) { - const std::string token = gems.cut_other_mats(i); - MaterialInfo mi; - mi.find(token); - if (!mi.isValid() || size_t(mi.type) >= builtin_size) { - WARN(log).print("invalid gem mat %s idx=%d\n", token.c_str(), mi.type); - continue; - } - DEBUG(log).print("cut_other mats %d is %s\n", mi.type, token.c_str()); - mPile->settings.gems.cut_other_mats.at(mi.type) = 1; + for (size_t i = 0; i < pgems.rough_other_mats.size(); ++i) { + if (!pgems.rough_other_mats.at(i)) { + all = false; + continue; } + mi.decode(i, -1); + if (!gem_other_mat_is_allowed(mi)) + continue; + DEBUG(log).print("gem rough_other mat %zd is %s\n", i, mi.getToken().c_str()); + gems->add_rough_other_mats(mi.getToken()); } - else { - mPile->settings.flags.bits.gems = 0; - mPile->settings.gems.cut_other_mats.clear(); - mPile->settings.gems.cut_mats.clear(); - mPile->settings.gems.rough_other_mats.clear(); - mPile->settings.gems.rough_mats.clear(); - } -} -bool StockpileSerializer::finished_goods_type_is_allowed(item_type::item_type type) { - switch (type) { - case item_type::CHAIN: - case item_type::FLASK: - case item_type::GOBLET: - case item_type::INSTRUMENT: - case item_type::TOY: - case item_type::ARMOR: - case item_type::SHOES: - case item_type::HELM: - case item_type::GLOVES: - case item_type::FIGURINE: - case item_type::AMULET: - case item_type::SCEPTER: - case item_type::CROWN: - case item_type::RING: - case item_type::EARRING: - case item_type::BRACELET: - case item_type::GEM: - case item_type::TOTEM: - case item_type::PANTS: - case item_type::BACKPACK: - case item_type::QUIVER: - case item_type::SPLINT: - case item_type::CRUTCH: - case item_type::TOOL: - case item_type::BOOK: - return true; - default: - return false; + for (size_t i = 0; i < pgems.cut_other_mats.size(); ++i) { + if (!pgems.cut_other_mats.at(i)) { + all = false; + continue; + } + mi.decode(i, -1); + if (!mi.isValid()) + mi.decode(0, i); + if (!gem_other_mat_is_allowed(mi)) + continue; + DEBUG(log).print("gem cut_other mat %zd is %s\n", i, mi.getToken().c_str()); + gems->add_cut_other_mats(mi.getToken()); } -} -void StockpileSerializer::finished_goods_setup_other_mats() { - mOtherMatsFinishedGoods.insert(std::make_pair(0, "WOOD")); - mOtherMatsFinishedGoods.insert(std::make_pair(1, "PLANT_CLOTH")); - mOtherMatsFinishedGoods.insert(std::make_pair(2, "BONE")); - mOtherMatsFinishedGoods.insert(std::make_pair(3, "TOOTH")); - mOtherMatsFinishedGoods.insert(std::make_pair(4, "HORN")); - mOtherMatsFinishedGoods.insert(std::make_pair(5, "PEARL")); - mOtherMatsFinishedGoods.insert(std::make_pair(6, "SHELL")); - mOtherMatsFinishedGoods.insert(std::make_pair(7, "LEATHER")); - mOtherMatsFinishedGoods.insert(std::make_pair(8, "SILK")); - mOtherMatsFinishedGoods.insert(std::make_pair(9, "AMBER")); - mOtherMatsFinishedGoods.insert(std::make_pair(10, "CORAL")); - mOtherMatsFinishedGoods.insert(std::make_pair(11, "GREEN_GLASS")); - mOtherMatsFinishedGoods.insert(std::make_pair(12, "CLEAR_GLASS")); - mOtherMatsFinishedGoods.insert(std::make_pair(13, "CRYSTAL_GLASS")); - mOtherMatsFinishedGoods.insert(std::make_pair(14, "YARN")); - mOtherMatsFinishedGoods.insert(std::make_pair(15, "WAX")); + return all; } -bool StockpileSerializer::finished_goods_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_GEM) || mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); +void StockpileSerializer::read_gems(DeserializeMode mode, const vector& filters) { + auto & pgems = mPile->settings.gems; + read_category("gems", mode, + std::bind(&StockpileSettings::has_gems, mBuffer), + std::bind(&StockpileSettings::gems, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_gems, + [&]() { + pgems.cut_other_mats.clear(); + pgems.cut_mats.clear(); + pgems.rough_other_mats.clear(); + pgems.rough_mats.clear(); + }, + [&](bool all, char val) { + auto & bgems = mBuffer.gems(); + + unserialize_list_material("mats/rough", all, val, filters, gem_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, + bgems.rough_mats_size(), pgems.rough_mats); + + unserialize_list_material("mats/cut", all, val, filters, gem_cut_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, + bgems.cut_mats_size(), pgems.cut_mats); + + const size_t builtin_size = std::extentraws.mat_table.builtin)>::value; + pgems.rough_other_mats.resize(builtin_size, '\0'); + pgems.cut_other_mats.resize(builtin_size, '\0'); + if (all) { + for (size_t idx = 0; idx < builtin_size; ++idx) { + MaterialInfo mi; + mi.decode(idx, -1); + if (gem_other_mat_is_allowed(mi)) + set_filter_elem("other/rough", filters, val, mi.getToken(), idx, pgems.rough_other_mats.at(idx)); + if (!mi.isValid()) + mi.decode(0, idx); + if (gem_other_mat_is_allowed(mi)) + set_filter_elem("other/cut", filters, val, mi.getToken(), idx, pgems.cut_other_mats.at(idx)); + } + return; + } else { + MaterialInfo mi; + for (int i = 0; i < (int)builtin_size; ++i) { + if (i < bgems.rough_other_mats_size()) { + string id = bgems.rough_other_mats(i); + if (mi.find(id) && mi.isValid() && size_t(mi.type) < builtin_size) + set_filter_elem("other/rough", filters, val, id, mi.type, pgems.rough_other_mats.at(mi.type)); + } + if (i < bgems.cut_other_mats_size()) { + string id = bgems.cut_other_mats(i); + if (mi.find(id) && mi.isValid() && size_t(mi.type) < builtin_size) + set_filter_elem("other/cut", filters, val, id, mi.type, pgems.cut_other_mats.at(mi.type)); + } + } + } + }); } -void StockpileSerializer::write_finished_goods() { - StockpileSettings::FinishedGoodsSet* finished_goods = mBuffer.mutable_finished_goods(); - - // type - FuncItemAllowed filter = std::bind(&StockpileSerializer::finished_goods_type_is_allowed, this, _1); - serialize_list_item_type( - filter, [=](const std::string& token) { finished_goods->add_type(token); }, - mPile->settings.finished_goods.type); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::finished_goods_mat_is_allowed, this, _1); - serialize_list_material( - mat_filter, [=](const std::string& token) { finished_goods->add_mats(token); }, - mPile->settings.finished_goods.mats); - - // other mats - serialize_list_other_mats( - mOtherMatsFinishedGoods, [=](const std::string& token) { finished_goods->add_other_mats(token); }, - mPile->settings.finished_goods.other_mats); - - // quality core - serialize_list_quality([=](const std::string& token) { finished_goods->add_quality_core(token); }, - mPile->settings.finished_goods.quality_core); - - // quality total - serialize_list_quality([=](const std::string& token) { finished_goods->add_quality_total(token); }, - mPile->settings.finished_goods.quality_total); +bool StockpileSerializer::write_leather(StockpileSettings::LeatherSet* leather) { + return serialize_list_organic_mat( + [&](const string& id) { leather->add_mats(id); }, + &mPile->settings.leather.mats, organic_mat_category::Leather); } -void StockpileSerializer::read_finished_goods() { - if (mBuffer.has_finished_goods()) { - mPile->settings.flags.bits.finished_goods = 1; - const StockpileSettings::FinishedGoodsSet finished_goods = mBuffer.finished_goods(); - DEBUG(log).print("finished_goods:\n"); - - // type - FuncItemAllowed filter = std::bind(&StockpileSerializer::finished_goods_type_is_allowed, this, _1); - unserialize_list_item_type( - filter, [=](const size_t& idx) -> const std::string& { return finished_goods.type(idx); }, - finished_goods.type_size(), &mPile->settings.finished_goods.type); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::finished_goods_mat_is_allowed, this, _1); - unserialize_list_material( - mat_filter, [=](const size_t& idx) -> const std::string& { return finished_goods.mats(idx); }, - finished_goods.mats_size(), &mPile->settings.finished_goods.mats); +void StockpileSerializer::read_leather(DeserializeMode mode, const vector& filters) { + auto & pleather = mPile->settings.leather; + read_category("leather", mode, + std::bind(&StockpileSettings::has_leather, mBuffer), + std::bind(&StockpileSettings::leather, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_leather, + [&]() { + pleather.mats.clear(); + }, + [&](bool all, char val) { + auto & bleather = mBuffer.leather(); + + unserialize_list_organic_mat("", all, val, filters, + [&](size_t idx) -> string { return bleather.mats(idx); }, + bleather.mats_size(), pleather.mats, organic_mat_category::Leather); + }); +} + +bool StockpileSerializer::write_corpses(StockpileSettings::CorpsesSet* corpses) { + return serialize_list_creature( + [&](const string& token) { corpses->add_corpses(token); }, + mPile->settings.corpses.corpses); +} + +void StockpileSerializer::read_corpses(DeserializeMode mode, const vector& filters) { + auto & pcorpses = mPile->settings.corpses; + read_category("corpses", mode, + std::bind(&StockpileSettings::has_corpses_v50, mBuffer), + std::bind(&StockpileSettings::corpses_v50, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_corpses, + [&]() { + pcorpses.corpses.clear(); + }, + [&](bool all, char val) { + auto & bcorpses = mBuffer.corpses_v50(); + unserialize_list_creature("", all, val, filters, + [&](const size_t& idx) -> const string& { return bcorpses.corpses(idx); }, + bcorpses.corpses_size(), pcorpses.corpses); + }); +} + +static bool refuse_type_is_allowed(item_type::item_type type) { + if (type == item_type::NONE || type == item_type::BAR || type == item_type::SMALLGEM + || type == item_type::BLOCKS || type == item_type::ROUGH || type == item_type::BOULDER + || type == item_type::CORPSE || type == item_type::CORPSEPIECE || type == item_type::ROCK + || type == item_type::ORTHOPEDIC_CAST) + return false; + return true; +} - // other mats - unserialize_list_other_mats( - mOtherMatsFinishedGoods, [=](const size_t& idx) -> const std::string& { return finished_goods.other_mats(idx); }, - finished_goods.other_mats_size(), &mPile->settings.finished_goods.other_mats); +bool StockpileSerializer::write_refuse(StockpileSettings::RefuseSet* refuse) { + auto & prefuse = mPile->settings.refuse; + bool all = prefuse.fresh_raw_hide && prefuse.rotten_raw_hide; + + refuse->set_fresh_raw_hide(prefuse.fresh_raw_hide); + refuse->set_rotten_raw_hide(prefuse.rotten_raw_hide); + + all = serialize_list_item_type(refuse_type_is_allowed, + [&](const string& token) { refuse->add_type(token); }, + prefuse.type) && all; + + all = serialize_list_creature( + [&](const string& token) { refuse->add_corpses(token); }, + prefuse.corpses) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_body_parts(token); }, + prefuse.body_parts) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_skulls(token); }, + prefuse.skulls) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_bones(token); }, + prefuse.bones) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_hair(token); }, + prefuse.hair) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_shells(token); }, + prefuse.shells) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_teeth(token); }, + prefuse.teeth) && all; + all = serialize_list_creature( + [&](const string& token) { refuse->add_horns(token); }, + prefuse.horns) && all; + + return all; +} + +void StockpileSerializer::read_refuse(DeserializeMode mode, const vector& filters) { + auto & prefuse = mPile->settings.refuse; + read_category("refuse", mode, + std::bind(&StockpileSettings::has_refuse, mBuffer), + std::bind(&StockpileSettings::refuse, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_refuse, + [&]() { + prefuse.fresh_raw_hide = false; + prefuse.rotten_raw_hide = false; + prefuse.type.clear(); + prefuse.corpses.clear(); + prefuse.body_parts.clear(); + prefuse.skulls.clear(); + prefuse.bones.clear(); + prefuse.hair.clear(); + prefuse.shells.clear(); + prefuse.teeth.clear(); + prefuse.horns.clear(); + }, + [&](bool all, char val) { + auto & brefuse = mBuffer.refuse(); + + set_flag("rawhide/fresh", filters, all, val, brefuse.fresh_raw_hide(), prefuse.fresh_raw_hide); + set_flag("rawhide/rotten", filters, all, val, brefuse.rotten_raw_hide(), prefuse.rotten_raw_hide); + + unserialize_list_item_type("type", all, val, filters, refuse_type_is_allowed, + [&](const size_t& idx) -> const string& { return brefuse.type(idx); }, + brefuse.type_size(), prefuse.type); + + unserialize_list_creature("corpses", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.corpses(idx); }, + brefuse.corpses_size(), prefuse.corpses); + unserialize_list_creature("bodyparts", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.body_parts(idx); }, + brefuse.body_parts_size(), prefuse.body_parts); + unserialize_list_creature("skulls", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.skulls(idx); }, + brefuse.skulls_size(), prefuse.skulls); + unserialize_list_creature("bones", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.bones(idx); }, + brefuse.bones_size(), prefuse.bones); + unserialize_list_creature("hair", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.hair(idx); }, + brefuse.hair_size(), prefuse.hair); + unserialize_list_creature("shells", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.shells(idx); }, + brefuse.shells_size(), prefuse.shells); + unserialize_list_creature("teeth", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.teeth(idx); }, + brefuse.teeth_size(), prefuse.teeth); + unserialize_list_creature("horns", all, val, filters, + [&](const size_t& idx) -> const string& { return brefuse.horns(idx); }, + brefuse.horns_size(), prefuse.horns); + }); + +} + +bool StockpileSerializer::write_sheet(StockpileSettings::SheetSet* sheet) { + bool all = serialize_list_organic_mat( + [&](const string& token) { sheet->add_paper(token); }, + &mPile->settings.sheet.paper, organic_mat_category::Paper); + + all = serialize_list_organic_mat( + [&](const string& token) { sheet->add_parchment(token); }, + &mPile->settings.sheet.parchment, organic_mat_category::Parchment) && all; + + return all; +} + +void StockpileSerializer::read_sheet(DeserializeMode mode, const vector& filters) { + auto & psheet = mPile->settings.sheet; + read_category("sheet", mode, + std::bind(&StockpileSettings::has_sheet, mBuffer), + std::bind(&StockpileSettings::sheet, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_sheet, + [&]() { + psheet.paper.clear(); + psheet.parchment.clear(); + }, + [&](bool all, char val) { + auto & bsheet = mBuffer.sheet(); - // core quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return finished_goods.quality_core(idx); }, - finished_goods.quality_core_size(), mPile->settings.finished_goods.quality_core); + unserialize_list_organic_mat("paper", all, val, filters, + [&](size_t idx) -> string { return bsheet.paper(idx); }, + bsheet.paper_size(), psheet.paper, organic_mat_category::Paper); - // total quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return finished_goods.quality_total(idx); }, - finished_goods.quality_total_size(), mPile->settings.finished_goods.quality_total); - } - else { - mPile->settings.flags.bits.finished_goods = 0; - mPile->settings.finished_goods.type.clear(); - mPile->settings.finished_goods.other_mats.clear(); - mPile->settings.finished_goods.mats.clear(); - quality_clear(mPile->settings.finished_goods.quality_core); - quality_clear(mPile->settings.finished_goods.quality_total); - } + unserialize_list_organic_mat("parchment", all, val, filters, + [&](size_t idx) -> string { return bsheet.parchment(idx); }, + bsheet.parchment_size(), psheet.parchment, organic_mat_category::Parchment); + }); } -void StockpileSerializer::write_leather() { - StockpileSettings::LeatherSet* leather = mBuffer.mutable_leather(); - - FuncWriteExport setter = [=](const std::string& id) { - leather->add_mats(id); - }; - serialize_list_organic_mat(setter, &mPile->settings.leather.mats, organic_mat_category::Leather); +static bool stone_is_allowed(const MaterialInfo& mi) { + if (!mi.isValid()) + return false; + const bool is_allowed_soil = mi.inorganic->flags.is_set(inorganic_flags::SOIL) && !mi.inorganic->flags.is_set(inorganic_flags::AQUIFER); + const bool is_allowed_stone = mi.material->flags.is_set(material_flags::IS_STONE) && !mi.material->flags.is_set(material_flags::NO_STONE_STOCKPILE); + return is_allowed_soil || is_allowed_stone; } -void StockpileSerializer::read_leather() { - if (mBuffer.has_leather()) { - mPile->settings.flags.bits.leather = 1; - const StockpileSettings::LeatherSet leather = mBuffer.leather(); - DEBUG(log).print("leather:\n"); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return leather.mats(idx); }, - leather.mats_size(), &mPile->settings.leather.mats, organic_mat_category::Leather); - } - else { - mPile->settings.flags.bits.leather = 0; - mPile->settings.leather.mats.clear(); - } +bool StockpileSerializer::write_stone(StockpileSettings::StoneSet* stone) { + return serialize_list_material( + stone_is_allowed, + [&](const string& token) { stone->add_mats(token); }, + mPile->settings.stone.mats); } -void StockpileSerializer::write_cloth() { - StockpileSettings::ClothSet* cloth = mBuffer.mutable_cloth(); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_thread_silk(token); }, - &mPile->settings.cloth.thread_silk, organic_mat_category::Silk); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_thread_plant(token); }, - &mPile->settings.cloth.thread_plant, organic_mat_category::PlantFiber); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_thread_yarn(token); }, - &mPile->settings.cloth.thread_yarn, organic_mat_category::Yarn); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_thread_metal(token); }, - &mPile->settings.cloth.thread_metal, organic_mat_category::MetalThread); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_cloth_silk(token); }, - &mPile->settings.cloth.cloth_silk, organic_mat_category::Silk); - - serialize_list_organic_mat([=](const std::string& token) { cloth->add_cloth_plant(token); }, - &mPile->settings.cloth.cloth_plant, organic_mat_category::PlantFiber); +void StockpileSerializer::read_stone(DeserializeMode mode, const vector& filters) { + auto & pstone = mPile->settings.stone; + read_category("stone", mode, + std::bind(&StockpileSettings::has_stone, mBuffer), + std::bind(&StockpileSettings::stone, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_stone, + [&]() { + pstone.mats.clear(); + }, + [&](bool all, char val) { + auto & bstone = mBuffer.stone(); - serialize_list_organic_mat([=](const std::string& token) { cloth->add_cloth_yarn(token); }, - &mPile->settings.cloth.cloth_yarn, organic_mat_category::Yarn); + unserialize_list_material("", all, val, filters, stone_is_allowed, + [&](const size_t& idx) -> const string& { return bstone.mats(idx); }, + bstone.mats_size(), pstone.mats); + }); +} - serialize_list_organic_mat([=](const std::string& token) { cloth->add_cloth_metal(token); }, - &mPile->settings.cloth.cloth_metal, organic_mat_category::MetalThread); +static bool weapons_mat_is_allowed(const MaterialInfo& mi) { + return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); } -void StockpileSerializer::read_cloth() { - if (mBuffer.has_cloth()) { - mPile->settings.flags.bits.cloth = 1; - const StockpileSettings::ClothSet cloth = mBuffer.cloth(); - DEBUG(log).print("cloth:\n"); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.thread_silk(idx); }, - cloth.thread_silk_size(), &mPile->settings.cloth.thread_silk, organic_mat_category::Silk); +bool StockpileSerializer::write_weapons(StockpileSettings::WeaponsSet* weapons) { + auto & pweapons = mPile->settings.weapons; + bool all = pweapons.unusable && pweapons.usable; + + weapons->set_unusable(pweapons.unusable); + weapons->set_usable(pweapons.usable); + + all = serialize_list_itemdef( + [&](const string& token) { weapons->add_weapon_type(token); }, + pweapons.weapon_type, + vector(world->raws.itemdefs.weapons.begin(), world->raws.itemdefs.weapons.end()), + item_type::WEAPON) && all; + + all = serialize_list_itemdef( + [&](const string& token) { weapons->add_trapcomp_type(token); }, + pweapons.trapcomp_type, + vector(world->raws.itemdefs.trapcomps.begin(), world->raws.itemdefs.trapcomps.end()), + item_type::TRAPCOMP) && all; + + all = serialize_list_material( + weapons_mat_is_allowed, + [&](const string& token) { weapons->add_mats(token); }, + pweapons.mats) && all; + + all = serialize_list_other_mats( + mOtherMatsWeaponsArmor.mats, + [&](const string& token) { weapons->add_other_mats(token); }, + pweapons.other_mats) && all; + + all = serialize_list_quality( + [&](const string& token) { weapons->add_quality_core(token); }, + pweapons.quality_core) && all; + + all = serialize_list_quality( + [&](const string& token) { weapons->add_quality_total(token); }, + pweapons.quality_total) && all; + + return all; +} + +void StockpileSerializer::read_weapons(DeserializeMode mode, const vector& filters) { + auto & pweapons = mPile->settings.weapons; + read_category("weapons", mode, + std::bind(&StockpileSettings::has_weapons, mBuffer), + std::bind(&StockpileSettings::weapons, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_weapons, + [&]() { + pweapons.unusable = false; + pweapons.usable = false; + pweapons.weapon_type.clear(); + pweapons.trapcomp_type.clear(); + pweapons.other_mats.clear(); + pweapons.mats.clear(); + quality_clear(pweapons.quality_core); + quality_clear(pweapons.quality_total); + }, + [&](bool all, char val) { + auto & bweapons = mBuffer.weapons(); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.thread_plant(idx); }, - cloth.thread_plant_size(), &mPile->settings.cloth.thread_plant, organic_mat_category::PlantFiber); + set_flag("nouse", filters, all, val, bweapons.unusable(), pweapons.unusable); + set_flag("canuse", filters, all, val, bweapons.usable(), pweapons.usable); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.thread_yarn(idx); }, - cloth.thread_yarn_size(), &mPile->settings.cloth.thread_yarn, organic_mat_category::Yarn); + unserialize_list_itemdef("type/weapon", all, val, filters, + [&](const size_t& idx) -> const string& { return bweapons.weapon_type(idx); }, + bweapons.weapon_type_size(), pweapons.weapon_type, item_type::WEAPON); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.thread_metal(idx); }, - cloth.thread_metal_size(), &mPile->settings.cloth.thread_metal, organic_mat_category::MetalThread); + unserialize_list_itemdef("type/trapcomp", all, val, filters, + [&](const size_t& idx) -> const string& { return bweapons.trapcomp_type(idx); }, + bweapons.trapcomp_type_size(), pweapons.trapcomp_type, item_type::TRAPCOMP); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.cloth_silk(idx); }, - cloth.cloth_silk_size(), &mPile->settings.cloth.cloth_silk, organic_mat_category::Silk); + unserialize_list_material("mats", all, val, filters, weapons_mat_is_allowed, + [&](const size_t& idx) -> const string& { return bweapons.mats(idx); }, + bweapons.mats_size(), pweapons.mats); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.cloth_plant(idx); }, - cloth.cloth_plant_size(), &mPile->settings.cloth.cloth_plant, organic_mat_category::PlantFiber); + unserialize_list_other_mats("other", all, val, filters, mOtherMatsWeaponsArmor.mats, + [&](const size_t& idx) -> const string& { return bweapons.other_mats(idx); }, + bweapons.other_mats_size(), pweapons.other_mats); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.cloth_yarn(idx); }, - cloth.cloth_yarn_size(), &mPile->settings.cloth.cloth_yarn, organic_mat_category::Yarn); + unserialize_list_quality("core", all, val, filters, + [&](const size_t& idx) -> const string& { return bweapons.quality_core(idx); }, + bweapons.quality_core_size(), pweapons.quality_core); - unserialize_list_organic_mat([=](size_t idx) -> std::string { return cloth.cloth_metal(idx); }, - cloth.cloth_metal_size(), &mPile->settings.cloth.cloth_metal, organic_mat_category::MetalThread); - } - else { - mPile->settings.cloth.thread_metal.clear(); - mPile->settings.cloth.thread_plant.clear(); - mPile->settings.cloth.thread_silk.clear(); - mPile->settings.cloth.thread_yarn.clear(); - mPile->settings.cloth.cloth_metal.clear(); - mPile->settings.cloth.cloth_plant.clear(); - mPile->settings.cloth.cloth_silk.clear(); - mPile->settings.cloth.cloth_yarn.clear(); - mPile->settings.flags.bits.cloth = 0; - } + unserialize_list_quality("total", all, val, filters, + [&](const size_t& idx) -> const string& { return bweapons.quality_total(idx); }, + bweapons.quality_total_size(), pweapons.quality_total); + }); } -bool StockpileSerializer::wood_mat_is_allowed(const df::plant_raw* plant) { +static bool wood_mat_is_allowed(const df::plant_raw* plant) { return plant && plant->flags.is_set(plant_raw_flags::TREE); } -void StockpileSerializer::write_wood() { - StockpileSettings::WoodSet* wood = mBuffer.mutable_wood(); +bool StockpileSerializer::write_wood(StockpileSettings::WoodSet* wood) { + bool all = true; for (size_t i = 0; i < mPile->settings.wood.mats.size(); ++i) { - if (mPile->settings.wood.mats.at(i)) { - const df::plant_raw* plant = find_plant(i); - if (!wood_mat_is_allowed(plant)) - continue; - wood->add_mats(plant->id); - DEBUG(log).print("plant %zd is %s\n", i, plant->id.c_str()); - } - } -} -void StockpileSerializer::read_wood() { - if (mBuffer.has_wood()) { - mPile->settings.flags.bits.wood = 1; - const StockpileSettings::WoodSet wood = mBuffer.wood(); - DEBUG(log).print("wood: \n"); - - mPile->settings.wood.mats.clear(); - mPile->settings.wood.mats.resize(world->raws.plants.all.size(), '\0'); - for (int i = 0; i < wood.mats_size(); ++i) { - const std::string token = wood.mats(i); - const size_t idx = find_plant(token); - if (idx < 0 || idx >= mPile->settings.wood.mats.size()) { - WARN(log).print("wood mat index invalid %s idx=%zd\n", token.c_str(), idx); - continue; - } - DEBUG(log).print("plant %zd is %s\n", idx, token.c_str()); - mPile->settings.wood.mats.at(idx) = 1; + if (!mPile->settings.wood.mats.at(i)) { + all = false; + continue; } + const df::plant_raw* plant = find_plant(i); + if (!wood_mat_is_allowed(plant)) + continue; + wood->add_mats(plant->id); + DEBUG(log).print("plant %zd is %s\n", i, plant->id.c_str()); } - else { - mPile->settings.flags.bits.wood = 0; - mPile->settings.wood.mats.clear(); - } -} - -bool StockpileSerializer::weapons_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && (mi.material->flags.is_set(material_flags::IS_METAL) || mi.material->flags.is_set(material_flags::IS_STONE)); -} - -void StockpileSerializer::write_weapons() { - StockpileSettings::WeaponsSet* weapons = mBuffer.mutable_weapons(); - - weapons->set_unusable(mPile->settings.weapons.unusable); - weapons->set_usable(mPile->settings.weapons.usable); - - // weapon type - serialize_list_itemdef([=](const std::string& token) { weapons->add_weapon_type(token); }, - mPile->settings.weapons.weapon_type, - std::vector(world->raws.itemdefs.weapons.begin(), world->raws.itemdefs.weapons.end()), - item_type::WEAPON); - - // trapcomp type - serialize_list_itemdef([=](const std::string& token) { weapons->add_trapcomp_type(token); }, - mPile->settings.weapons.trapcomp_type, - std::vector(world->raws.itemdefs.trapcomps.begin(), world->raws.itemdefs.trapcomps.end()), - item_type::TRAPCOMP); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::weapons_mat_is_allowed, this, _1); - serialize_list_material( - mat_filter, [=](const std::string& token) { weapons->add_mats(token); }, - mPile->settings.weapons.mats); - - // other mats - serialize_list_other_mats( - mOtherMatsWeaponsArmor, [=](const std::string& token) { weapons->add_other_mats(token); }, - mPile->settings.weapons.other_mats); - - // quality core - serialize_list_quality([=](const std::string& token) { weapons->add_quality_core(token); }, - mPile->settings.weapons.quality_core); - - // quality total - serialize_list_quality([=](const std::string& token) { weapons->add_quality_total(token); }, - mPile->settings.weapons.quality_total); -} - -void StockpileSerializer::read_weapons() { - if (mBuffer.has_weapons()) { - mPile->settings.flags.bits.weapons = 1; - const StockpileSettings::WeaponsSet weapons = mBuffer.weapons(); - DEBUG(log).print("weapons: \n"); - - bool unusable = weapons.unusable(); - bool usable = weapons.usable(); - DEBUG(log).print("unusable %d\n", unusable); - DEBUG(log).print("usable %d\n", usable); - mPile->settings.weapons.unusable = unusable; - mPile->settings.weapons.usable = usable; - - // weapon type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return weapons.weapon_type(idx); }, - weapons.weapon_type_size(), &mPile->settings.weapons.weapon_type, item_type::WEAPON); - - // trapcomp type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return weapons.trapcomp_type(idx); }, - weapons.trapcomp_type_size(), &mPile->settings.weapons.trapcomp_type, item_type::TRAPCOMP); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::weapons_mat_is_allowed, this, _1); - unserialize_list_material( - mat_filter, [=](const size_t& idx) -> const std::string& { return weapons.mats(idx); }, - weapons.mats_size(), &mPile->settings.weapons.mats); - - // other mats - unserialize_list_other_mats( - mOtherMatsWeaponsArmor, [=](const size_t& idx) -> const std::string& { return weapons.other_mats(idx); }, - weapons.other_mats_size(), &mPile->settings.weapons.other_mats); - - // core quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return weapons.quality_core(idx); }, - weapons.quality_core_size(), mPile->settings.weapons.quality_core); - // total quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return weapons.quality_total(idx); }, - weapons.quality_total_size(), mPile->settings.weapons.quality_total); - } - else { - mPile->settings.flags.bits.weapons = 0; - mPile->settings.weapons.weapon_type.clear(); - mPile->settings.weapons.trapcomp_type.clear(); - mPile->settings.weapons.other_mats.clear(); - mPile->settings.weapons.mats.clear(); - quality_clear(mPile->settings.weapons.quality_core); - quality_clear(mPile->settings.weapons.quality_total); - } -} - -void StockpileSerializer::weapons_armor_setup_other_mats() { - mOtherMatsWeaponsArmor.insert(std::make_pair(0, "WOOD")); - mOtherMatsWeaponsArmor.insert(std::make_pair(1, "PLANT_CLOTH")); - mOtherMatsWeaponsArmor.insert(std::make_pair(2, "BONE")); - mOtherMatsWeaponsArmor.insert(std::make_pair(3, "SHELL")); - mOtherMatsWeaponsArmor.insert(std::make_pair(4, "LEATHER")); - mOtherMatsWeaponsArmor.insert(std::make_pair(5, "SILK")); - mOtherMatsWeaponsArmor.insert(std::make_pair(6, "GREEN_GLASS")); - mOtherMatsWeaponsArmor.insert(std::make_pair(7, "CLEAR_GLASS")); - mOtherMatsWeaponsArmor.insert(std::make_pair(8, "CRYSTAL_GLASS")); - mOtherMatsWeaponsArmor.insert(std::make_pair(9, "YARN")); -} - -bool StockpileSerializer::armor_mat_is_allowed(const MaterialInfo& mi) { - return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); + return all; } -void StockpileSerializer::write_armor() { - StockpileSettings::ArmorSet* armor = mBuffer.mutable_armor(); - - armor->set_unusable(mPile->settings.armor.unusable); - armor->set_usable(mPile->settings.armor.usable); - - // armor type - serialize_list_itemdef([=](const std::string& token) { armor->add_body(token); }, - mPile->settings.armor.body, - std::vector(world->raws.itemdefs.armor.begin(), world->raws.itemdefs.armor.end()), - item_type::ARMOR); - - // helm type - serialize_list_itemdef([=](const std::string& token) { armor->add_head(token); }, - mPile->settings.armor.head, - std::vector(world->raws.itemdefs.helms.begin(), world->raws.itemdefs.helms.end()), - item_type::HELM); - - // shoes type - serialize_list_itemdef([=](const std::string& token) { armor->add_feet(token); }, - mPile->settings.armor.feet, - std::vector(world->raws.itemdefs.shoes.begin(), world->raws.itemdefs.shoes.end()), - item_type::SHOES); - - // gloves type - serialize_list_itemdef([=](const std::string& token) { armor->add_hands(token); }, - mPile->settings.armor.hands, - std::vector(world->raws.itemdefs.gloves.begin(), world->raws.itemdefs.gloves.end()), - item_type::GLOVES); - - // pant type - serialize_list_itemdef([=](const std::string& token) { armor->add_legs(token); }, - mPile->settings.armor.legs, - std::vector(world->raws.itemdefs.pants.begin(), world->raws.itemdefs.pants.end()), - item_type::PANTS); - - // shield type - serialize_list_itemdef([=](const std::string& token) { armor->add_shield(token); }, - mPile->settings.armor.shield, - std::vector(world->raws.itemdefs.shields.begin(), world->raws.itemdefs.shields.end()), - item_type::SHIELD); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::armor_mat_is_allowed, this, _1); - serialize_list_material( - mat_filter, [=](const std::string& token) { armor->add_mats(token); }, - mPile->settings.armor.mats); - - // other mats - serialize_list_other_mats( - mOtherMatsWeaponsArmor, [=](const std::string& token) { armor->add_other_mats(token); }, - mPile->settings.armor.other_mats); +void StockpileSerializer::read_wood(DeserializeMode mode, const vector& filters) { + auto & pwood = mPile->settings.wood; + read_category("wood", mode, + std::bind(&StockpileSettings::has_wood, mBuffer), + std::bind(&StockpileSettings::wood, mBuffer), + mPile->settings.flags.whole, + mPile->settings.flags.mask_wood, + [&]() { + pwood.mats.clear(); + }, + [&](bool all, char val) { + auto & bwood = mBuffer.wood(); - // quality core - serialize_list_quality([=](const std::string& token) { armor->add_quality_core(token); }, - mPile->settings.armor.quality_core); + size_t num_elems = world->raws.plants.all.size(); + pwood.mats.resize(num_elems, '\0'); - // quality total - serialize_list_quality([=](const std::string& token) { armor->add_quality_total(token); }, - mPile->settings.armor.quality_total); -} - -void StockpileSerializer::read_armor() { - if (mBuffer.has_armor()) { - mPile->settings.flags.bits.armor = 1; - const StockpileSettings::ArmorSet armor = mBuffer.armor(); - DEBUG(log).print("armor:\n"); - - bool unusable = armor.unusable(); - bool usable = armor.usable(); - DEBUG(log).print("unusable %d\n", unusable); - DEBUG(log).print("usable %d\n", usable); - mPile->settings.armor.unusable = unusable; - mPile->settings.armor.usable = usable; - - // body type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.body(idx); }, - armor.body_size(), &mPile->settings.armor.body, item_type::ARMOR); - - // head type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.head(idx); }, - armor.head_size(), &mPile->settings.armor.head, item_type::HELM); - - // feet type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.feet(idx); }, - armor.feet_size(), &mPile->settings.armor.feet, item_type::SHOES); - - // hands type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.hands(idx); }, - armor.hands_size(), &mPile->settings.armor.hands, item_type::GLOVES); - - // legs type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.legs(idx); }, - armor.legs_size(), &mPile->settings.armor.legs, item_type::PANTS); - - // shield type - unserialize_list_itemdef([=](const size_t& idx) -> const std::string& { return armor.shield(idx); }, - armor.shield_size(), &mPile->settings.armor.shield, item_type::SHIELD); - - // materials - FuncMaterialAllowed mat_filter = std::bind(&StockpileSerializer::armor_mat_is_allowed, this, _1); - unserialize_list_material( - mat_filter, [=](const size_t& idx) -> const std::string& { return armor.mats(idx); }, - armor.mats_size(), &mPile->settings.armor.mats); - - // other mats - unserialize_list_other_mats( - mOtherMatsWeaponsArmor, [=](const size_t& idx) -> const std::string& { return armor.other_mats(idx); }, - armor.other_mats_size(), &mPile->settings.armor.other_mats); - - // core quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return armor.quality_core(idx); }, - armor.quality_core_size(), mPile->settings.armor.quality_core); - // total quality - unserialize_list_quality([=](const size_t& idx) -> const std::string& { return armor.quality_total(idx); }, - armor.quality_total_size(), mPile->settings.armor.quality_total); - } - else { - mPile->settings.flags.bits.armor = 0; - mPile->settings.armor.body.clear(); - mPile->settings.armor.head.clear(); - mPile->settings.armor.feet.clear(); - mPile->settings.armor.hands.clear(); - mPile->settings.armor.legs.clear(); - mPile->settings.armor.shield.clear(); - mPile->settings.armor.other_mats.clear(); - mPile->settings.armor.mats.clear(); - quality_clear(mPile->settings.armor.quality_core); - quality_clear(mPile->settings.armor.quality_total); - } + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + string id = world->raws.plants.all[idx]->id; + set_filter_elem("", filters, val, id, idx, pwood.mats.at(idx)); + } + } else { + for (int i = 0; i < bwood.mats_size(); ++i) { + const string token = bwood.mats(i); + const size_t idx = find_plant(token); + if (idx < 0 || (size_t)idx >= num_elems) { + WARN(log).print("wood mat index invalid %s idx=%zd\n", token.c_str(), idx); + continue; + } + set_filter_elem("", filters, val, token, idx, pwood.mats.at(idx)); + } + } + }); } diff --git a/plugins/stockpiles/StockpileSerializer.h b/plugins/stockpiles/StockpileSerializer.h index 3fe2087de..f0e1a62e2 100644 --- a/plugins/stockpiles/StockpileSerializer.h +++ b/plugins/stockpiles/StockpileSerializer.h @@ -14,16 +14,54 @@ namespace df struct building_stockpilest; } +enum IncludedElements { + INCLUDED_ELEMENTS_NONE = 0x00, + INCLUDED_ELEMENTS_CONTAINERS = 0x01, + INCLUDED_ELEMENTS_GENERAL = 0x02, + INCLUDED_ELEMENTS_CATEGORIES = 0x04, + INCLUDED_ELEMENTS_TYPES = 0x08, +}; + +enum DeserializeMode { + DESERIALIZE_MODE_SET = 0, + DESERIALIZE_MODE_ENABLE = 1, + DESERIALIZE_MODE_DISABLE = 2, +}; + +// read the token from the serialized list during import +typedef std::function FuncReadImport; +// add the token to the serialized list during export +typedef std::function FuncWriteExport; +// are item's of item_type allowed? +typedef std::function FuncItemAllowed; +// is this material allowed? +typedef std::function FuncMaterialAllowed; + +// convenient struct for parsing food stockpile items +struct food_pair { + const char * name; + // exporting + FuncWriteExport set_value; + std::vector* stockpile_values; + // importing + FuncReadImport get_value; + size_t serialized_count; + bool valid; + + food_pair(const char * n, FuncWriteExport s, std::vector* sp_v, FuncReadImport g, size_t count) + : name(n), set_value(s), stockpile_values(sp_v), get_value(g), serialized_count(count), + valid(true) { } + food_pair(): valid(false) { } +}; + /** * Class for serializing the stockpile_settings structure into a Google protobuf */ class StockpileSerializer { public: /** - * @param out for debugging * @param stockpile stockpile to read or write settings to */ - StockpileSerializer(df::building_stockpilest* stockpile); ~StockpileSerializer(); @@ -32,252 +70,72 @@ public: * Since we depend on protobuf-lite, not the full lib, we copy this function from * protobuf message.cc */ - bool serialize_to_ostream(std::ostream* output); + bool serialize_to_ostream(std::ostream* output, uint32_t includedElements); /** * Will serialize stockpile settings to a file (overwrites existing files) * @return success/failure */ - bool serialize_to_file(const std::string& file); + bool serialize_to_file(const std::string& file, uint32_t includedElements); /** * Again, copied from message.cc */ - bool parse_from_istream(std::istream* input); + bool parse_from_istream(std::istream* input, DeserializeMode mode, const std::vector& filters); /** * Read stockpile settings from file */ - bool unserialize_from_file(const std::string& file); + bool unserialize_from_file(const std::string& file, DeserializeMode mode, const std::vector& filters); private: df::building_stockpilest* mPile; dfstockpiles::StockpileSettings mBuffer; - std::map mOtherMatsFurniture; - std::map mOtherMatsFinishedGoods; - std::map mOtherMatsBars; - std::map mOtherMatsBlocks; - std::map mOtherMatsWeaponsArmor; - - /** - read memory structures and serialize to protobuf - */ - void write(); - - // parse serialized data into ui indices - void read(); - - /** - * Find an enum's value based off the string label. - * @param traits the enum's trait struct - * @param token the string value in key_table - * @return the enum's value, -1 if not found - */ - template - static typename df::enum_traits::base_type linear_index(df::enum_traits traits, const std::string& token) { - auto j = traits.first_item_value; - auto limit = traits.last_item_value; - // sometimes enums start at -1, which is bad news for array indexing - if (j < 0) { - j += abs(traits.first_item_value); - limit += abs(traits.first_item_value); - } - for (; j <= limit; ++j) { - if (token.compare(traits.key_table[j]) == 0) - return j; - } - return -1; - } - - // read the token from the serailized list during import - typedef std::function FuncReadImport; - // add the token to the serialized list during export - typedef std::function FuncWriteExport; - // are item's of item_type allowed? - typedef std::function FuncItemAllowed; - // is this material allowed? - typedef std::function FuncMaterialAllowed; - - // convenient struct for parsing food stockpile items - struct food_pair { - // exporting - FuncWriteExport set_value; - std::vector* stockpile_values; - // importing - FuncReadImport get_value; - size_t serialized_count; - bool valid; - - food_pair(FuncWriteExport s, std::vector* sp_v, FuncReadImport g, size_t count) - : set_value(s), stockpile_values(sp_v), get_value(g), serialized_count(count), valid(true) { } - food_pair(): valid(false) { } - }; - - /** - * There are many repeated (un)serialization cases throughout the stockpile_settings structure, - * so the most common cases have been generalized into generic functions using lambdas. - * - * The basic process to serialize a stockpile_settings structure is: - * 1. loop through the list - * 2. for every element that is TRUE: - * 3. map the specific stockpile_settings index into a general material, creature, etc index - * 4. verify that type is allowed in the list (e.g., no stone in gems stockpiles) - * 5. add it to the protobuf using FuncWriteExport - * - * The unserialization process is the same in reverse. - */ - void serialize_list_organic_mat(FuncWriteExport add_value, const std::vector* list, df::enums::organic_mat_category::organic_mat_category cat); - - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_organic_mat(FuncReadImport get_value, size_t list_size, std::vector* pile_list, df::enums::organic_mat_category::organic_mat_category cat); - /** - * @see serialize_list_organic_mat - */ - void serialize_list_item_type(FuncItemAllowed is_allowed, FuncWriteExport add_value, const std::vector& list); + // read memory structures and serialize to protobuf + void write(uint32_t includedElements); - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_item_type(FuncItemAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector* pile_list); - - /** - * @see serialize_list_organic_mat - */ - void serialize_list_material(FuncMaterialAllowed is_allowed, FuncWriteExport add_value, const std::vector& list); - - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_material(FuncMaterialAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector* pile_list); - - /** - * @see serialize_list_organic_mat - */ - void serialize_list_quality(FuncWriteExport add_value, const bool(&quality_list)[7]); - - /** - * Set all values in a bool[7] to false - */ - void quality_clear(bool(&pile_list)[7]); - - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_quality(FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]); - - /** - * @see serialize_list_organic_mat - */ - void serialize_list_other_mats(const std::map other_mats, FuncWriteExport add_value, std::vector list); - - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_other_mats(const std::map other_mats, FuncReadImport read_value, int32_t list_size, std::vector* pile_list); - - /** - * @see serialize_list_organic_mat - */ - void serialize_list_itemdef(FuncWriteExport add_value, std::vector list, std::vector items, df::enums::item_type::item_type type); - - /** - * @see serialize_list_organic_mat - */ - void unserialize_list_itemdef(FuncReadImport read_value, int32_t list_size, std::vector* pile_list, df::enums::item_type::item_type type); - - /** - * Given a list of other_materials and an index, return its corresponding token - * @return empty string if not found - * @see other_mats_token - */ - std::string other_mats_index(const std::map other_mats, int idx); - - /** - * Given a list of other_materials and a token, return its corresponding index - * @return -1 if not found - * @see other_mats_index - */ - int other_mats_token(const std::map other_mats, const std::string& token); + // parse serialized data into ui indices + void read(DeserializeMode mode, const std::vector& filters); + void write_containers(); + void read_containers(DeserializeMode mode); void write_general(); - void read_general(); - - void write_animals(); - void read_animals(); - + void read_general(DeserializeMode mode); + + bool write_ammo(dfstockpiles::StockpileSettings::AmmoSet* ammo); + void read_ammo(DeserializeMode mode, const std::vector& filters); + bool write_animals(dfstockpiles::StockpileSettings::AnimalsSet* animals); + void read_animals(DeserializeMode mode, const std::vector& filters); + bool write_armor(dfstockpiles::StockpileSettings::ArmorSet* armor); + void read_armor(DeserializeMode mode, const std::vector& filters); + bool write_bars_blocks(dfstockpiles::StockpileSettings::BarsBlocksSet* bars_blocks); + void read_bars_blocks(DeserializeMode mode, const std::vector& filters); + bool write_cloth(dfstockpiles::StockpileSettings::ClothSet* cloth); + void read_cloth(DeserializeMode mode, const std::vector& filters); + bool write_coins(dfstockpiles::StockpileSettings::CoinSet* coins); + void read_coins(DeserializeMode mode, const std::vector& filters); + bool write_finished_goods(dfstockpiles::StockpileSettings::FinishedGoodsSet* finished_goods); + void read_finished_goods(DeserializeMode mode, const std::vector& filters); food_pair food_map(df::enums::organic_mat_category::organic_mat_category cat); - - void write_food(); - void read_food(); - - void furniture_setup_other_mats(); - void write_furniture(); - bool furniture_mat_is_allowed(const DFHack::MaterialInfo& mi); - void read_furniture(); - - bool refuse_creature_is_allowed(const df::creature_raw* raw); - - void refuse_write_helper(std::function add_value, const std::vector& list); - - bool refuse_type_is_allowed(df::enums::item_type::item_type type); - - void write_refuse(); - void refuse_read_helper(std::function get_value, size_t list_size, std::vector* pile_list); - - void read_refuse(); - - bool stone_is_allowed(const DFHack::MaterialInfo& mi); - - void write_stone(); - - void read_stone(); - - bool ammo_mat_is_allowed(const DFHack::MaterialInfo& mi); - - void write_ammo(); - void read_ammo(); - bool coins_mat_is_allowed(const DFHack::MaterialInfo& mi); - - void write_coins(); - - void read_coins(); - - void bars_blocks_setup_other_mats(); - - bool bars_mat_is_allowed(const DFHack::MaterialInfo& mi); - - bool blocks_mat_is_allowed(const DFHack::MaterialInfo& mi); - - void write_bars_blocks(); - void read_bars_blocks(); - bool gem_mat_is_allowed(const DFHack::MaterialInfo& mi); - bool gem_cut_mat_is_allowed(const DFHack::MaterialInfo& mi); - bool gem_other_mat_is_allowed(DFHack::MaterialInfo& mi); - - void write_gems(); - - void read_gems(); - - bool finished_goods_type_is_allowed(df::enums::item_type::item_type type); - void finished_goods_setup_other_mats(); - bool finished_goods_mat_is_allowed(const DFHack::MaterialInfo& mi); - void write_finished_goods(); - void read_finished_goods(); - void write_leather(); - void read_leather(); - void write_cloth(); - void read_cloth(); - bool wood_mat_is_allowed(const df::plant_raw* plant); - void write_wood(); - void read_wood(); - bool weapons_mat_is_allowed(const DFHack::MaterialInfo& mi); - void write_weapons(); - void read_weapons(); - void weapons_armor_setup_other_mats(); - bool armor_mat_is_allowed(const DFHack::MaterialInfo& mi); - void write_armor(); - void read_armor(); + bool write_food(dfstockpiles::StockpileSettings::FoodSet* food); + void read_food(DeserializeMode mode, const std::vector& filters); + bool write_furniture(dfstockpiles::StockpileSettings::FurnitureSet* furniture); + void read_furniture(DeserializeMode mode, const std::vector& filters); + bool write_gems(dfstockpiles::StockpileSettings::GemsSet* gems); + void read_gems(DeserializeMode mode, const std::vector& filters); + bool write_leather(dfstockpiles::StockpileSettings::LeatherSet* leather); + void read_leather(DeserializeMode mode, const std::vector& filters); + bool write_corpses(dfstockpiles::StockpileSettings::CorpsesSet* corpses); + void read_corpses(DeserializeMode mode, const std::vector& filters); + bool write_refuse(dfstockpiles::StockpileSettings::RefuseSet* refuse); + void read_refuse(DeserializeMode mode, const std::vector& filters); + bool write_sheet(dfstockpiles::StockpileSettings::SheetSet* sheet); + void read_sheet(DeserializeMode mode, const std::vector& filters); + bool write_stone(dfstockpiles::StockpileSettings::StoneSet* stone); + void read_stone(DeserializeMode mode, const std::vector& filters); + bool write_weapons(dfstockpiles::StockpileSettings::WeaponsSet* weapons); + void read_weapons(DeserializeMode mode, const std::vector& filters); + bool write_wood(dfstockpiles::StockpileSettings::WoodSet* wood); + void read_wood(DeserializeMode mode, const std::vector& filters); }; diff --git a/plugins/stockpiles/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 90c95b93f..681e7d927 100644 --- a/plugins/stockpiles/proto/stockpiles.proto +++ b/plugins/stockpiles/proto/stockpiles.proto @@ -7,12 +7,14 @@ option optimize_for = LITE_RUNTIME; message StockpileSettings { message AnimalsSet { + optional bool all = 4; optional bool empty_cages = 1; optional bool empty_traps = 2; repeated string enabled = 3; } message FoodSet { + optional bool all = 21; repeated string meat = 1; repeated string fish = 2; repeated string unprepared_fish = 20; @@ -36,6 +38,7 @@ message StockpileSettings { } message FurnitureSet { + optional bool all = 7; repeated string type = 1; repeated string other_mats = 2; repeated string mats = 3; @@ -44,6 +47,7 @@ message StockpileSettings { // UNUSED: optional bool sand_bags = 6; } message RefuseSet { + optional bool all = 12; repeated string type = 1; repeated string corpses = 2; repeated string body_parts = 3; @@ -57,12 +61,14 @@ message StockpileSettings { optional bool rotten_raw_hide = 11; } message StoneSet { - repeated string mats = 1; + optional bool all = 2; + repeated string mats = 1; } message OreSet { - repeated string mats = 1; + repeated string mats = 1; } message AmmoSet { + optional bool all = 6; repeated string type = 1; repeated string other_mats = 2; repeated string mats = 3; @@ -70,21 +76,25 @@ message StockpileSettings { repeated string quality_total = 5; } message CoinSet { + optional bool all = 2; repeated string mats = 1; } message BarsBlocksSet { + optional bool all = 5; repeated string bars_other_mats = 1; repeated string blocks_other_mats = 2; repeated string bars_mats = 3; repeated string blocks_mats = 4; } message GemsSet { + optional bool all = 5; repeated string rough_other_mats = 1; repeated string cut_other_mats = 2; repeated string rough_mats = 3; repeated string cut_mats = 4; } message FinishedGoodsSet { + optional bool all = 6; repeated string type = 1; repeated string other_mats = 2; repeated string mats = 3; @@ -92,9 +102,11 @@ message StockpileSettings { repeated string quality_total = 5; } message LeatherSet { + optional bool all = 2; repeated string mats = 1; } message ClothSet { + optional bool all = 9; repeated string thread_silk = 1; repeated string thread_plant = 2; repeated string thread_yarn = 3; @@ -105,9 +117,11 @@ message StockpileSettings { repeated string cloth_metal = 8; } message WoodSet { + optional bool all = 2; repeated string mats = 1; } message WeaponsSet { + optional bool all = 9; repeated string weapon_type = 1; repeated string trapcomp_type = 2; repeated string other_mats = 3; @@ -117,8 +131,8 @@ message StockpileSettings { optional bool usable = 7; optional bool unusable = 8; } - message ArmorSet { + optional bool all = 13; repeated string body = 1; repeated string head = 2; repeated string feet = 3; @@ -129,33 +143,48 @@ message StockpileSettings { repeated string mats = 8; repeated string quality_core = 9; repeated string quality_total = 10; - optional bool usable =11; + optional bool usable = 11; optional bool unusable = 12; } + message CorpsesSet { + optional bool all = 1; + repeated string corpses = 2; + } + message SheetSet { + optional bool all = 1; + repeated string paper = 2; + repeated string parchment = 3; + } + + // general settings + optional int32 max_barrels = 20; + optional int32 max_bins = 21; + optional int32 max_wheelbarrows = 22; + optional bool use_links_only = 23; + optional bool allow_organic = 18; + optional bool allow_inorganic = 19; + // categories + optional AmmoSet ammo = 8; optional AnimalsSet animals = 1; + optional ArmorSet armor = 17; + optional BarsBlocksSet barsblocks = 10; + optional ClothSet cloth = 14; + optional CoinSet coin = 9; + optional FinishedGoodsSet finished_goods = 12; optional FoodSet food = 2; optional FurnitureSet furniture = 3; - optional int32 unknown1 = 4 [deprecated=true]; - optional RefuseSet refuse = 5; - optional StoneSet stone = 6; - optional OreSet ore = 7; - optional AmmoSet ammo = 8; - optional CoinSet coin = 9; - optional BarsBlocksSet barsblocks = 10; optional GemsSet gems = 11; - optional FinishedGoodsSet finished_goods = 12; optional LeatherSet leather = 13; - optional ClothSet cloth = 14; - optional WoodSet wood = 15; + optional CorpsesSet corpses_v50 = 25; + optional RefuseSet refuse = 5; + optional SheetSet sheet = 26; + optional StoneSet stone = 6; optional WeaponsSet weapons = 16; - optional ArmorSet armor = 17; - optional bool allow_organic = 18; - optional bool allow_inorganic = 19; - optional bool corpses = 24; - // extras - optional int32 max_barrels = 20; - optional int32 max_bins = 21; - optional int32 max_wheelbarrows = 22; - optional bool use_links_only = 23; + optional WoodSet wood = 15; + + // deprecated + optional bool corpses = 24; // not marked as deprecated since we still read it + optional OreSet ore = 7 [deprecated=true]; + optional int32 unknown1 = 4 [deprecated=true]; } diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 98c94ace0..d1ce46e9c 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -1,13 +1,19 @@ #include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" #include "StockpileUtils.h" #include "StockpileSerializer.h" #include "modules/Filesystem.h" -#include "modules/Gui.h" -using std::vector; +#include "df/building.h" +#include "df/building_stockpilest.h" + +#include +#include + using std::string; +using std::vector; using namespace DFHack; @@ -19,112 +25,130 @@ namespace DFHack { DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); } -static command_result savestock(color_ostream& out, vector & parameters); -static command_result loadstock(color_ostream& out, vector & parameters); +static command_result do_command(color_ostream &out, vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); -DFhackCExport command_result plugin_init(color_ostream& out, std::vector & commands) { - commands.push_back(PluginCommand( - "savestock", - "Save the active stockpile's settings to a file.", - savestock, - Gui::any_stockpile_hotkey)); commands.push_back(PluginCommand( - "loadstock", - "Load and apply stockpile settings from a file.", - loadstock, - Gui::any_stockpile_hotkey)); + plugin_name, + "Import, export, or modify stockpile settings and features.", + do_command)); return CR_OK; } -DFhackCExport command_result plugin_shutdown(color_ostream& out) { - return CR_OK; +static bool call_stockpiles_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(log).print("calling stockpiles 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.stockpiles", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } -// exporting -static command_result savestock(color_ostream& out, vector & parameters) { - df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true); - if (!sp) { - out.printerr("Selected building isn't a stockpile.\n"); - return CR_WRONG_USAGE; +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + bool show_help = false; + if (!call_stockpiles_lua(&out, "parse_commandline", 1, 1, + [&](lua_State *L) { + Lua::PushVector(L, parameters); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } - if (parameters.size() > 2) { - out.printerr("Invalid parameters\n"); - return CR_WRONG_USAGE; - } + return show_help ? CR_WRONG_USAGE : CR_OK; +} - std::string file; - for (size_t i = 0; i < parameters.size(); ++i) { - const std::string o = parameters.at(i); - if (!o.empty() && o[0] != '-') { - file = o; - } - } - if (file.empty()) { - out.printerr("You must supply a valid filename.\n"); - return CR_WRONG_USAGE; +///////////////////////////////////////////////////// +// Lua API +// + +static df::building_stockpilest* get_stockpile(int id) { + return virtual_cast(df::building::find(id)); +} + +static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t includedElements) { + df::building_stockpilest* sp = get_stockpile(id); + if (!sp) { + out.printerr("Specified building isn't a stockpile: %d.\n", id); + return false; } - StockpileSerializer cereal(sp); + if (!is_dfstockfile(fname)) + fname += ".dfstock"; - if (!is_dfstockfile(file)) file += ".dfstock"; try { - if (!cereal.serialize_to_file(file)) { - out.printerr("could not save to %s\n", file.c_str()); - return CR_FAILURE; + StockpileSerializer cereal(sp); + if (!cereal.serialize_to_file(fname, includedElements)) { + out.printerr("could not save to '%s'\n", fname.c_str()); + return false; } } catch (std::exception& e) { out.printerr("serialization failed: protobuf exception: %s\n", e.what()); - return CR_FAILURE; + return false; } - return CR_OK; + return true; } - -// importing -static command_result loadstock(color_ostream& out, vector & parameters) { - df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true); +static bool stockpiles_import(color_ostream& out, string fname, int id, string mode_str, string filter) { + df::building_stockpilest* sp = get_stockpile(id); if (!sp) { - out.printerr("Selected building isn't a stockpile.\n"); - return CR_WRONG_USAGE; + out.printerr("Specified building isn't a stockpile: %d.\n", id); + return false; } - if (parameters.size() < 1 || parameters.size() > 2) { - out.printerr("Invalid parameters\n"); - return CR_WRONG_USAGE; - } + if (!is_dfstockfile(fname)) + fname += ".dfstock"; - std::string file; - for (size_t i = 0; i < parameters.size(); ++i) { - const std::string o = parameters.at(i); - if (!o.empty() && o[0] != '-') { - file = o; - } - } - if (file.empty()) { - out.printerr("ERROR: missing .dfstock file parameter\n"); - return DFHack::CR_WRONG_USAGE; - } - if (!is_dfstockfile(file)) - file += ".dfstock"; - if (!Filesystem::exists(file)) { - out.printerr("ERROR: the .dfstock file doesn't exist: %s\n", file.c_str()); - return CR_WRONG_USAGE; + if (!Filesystem::exists(fname)) { + out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + return false; } - StockpileSerializer cereal(sp); + DeserializeMode mode = DESERIALIZE_MODE_SET; + if (mode_str == "enable") + mode = DESERIALIZE_MODE_ENABLE; + else if (mode_str == "disable") + mode = DESERIALIZE_MODE_DISABLE; + + vector filters; + split_string(&filters, filter, ",", true); + try { - if (!cereal.unserialize_from_file(file)) { - out.printerr("unserialization failed: %s\n", file.c_str()); - return CR_FAILURE; + StockpileSerializer cereal(sp); + if (!cereal.unserialize_from_file(fname, mode, filters)) { + out.printerr("deserialization failed: '%s'\n", fname.c_str()); + return false; } } catch (std::exception& e) { - out.printerr("unserialization failed: protobuf exception: %s\n", e.what()); - return CR_FAILURE; + out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + return false; } - return CR_OK; + + return true; } + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(stockpiles_export), + DFHACK_LUA_FUNCTION(stockpiles_import), + DFHACK_LUA_END +}; diff --git a/plugins/stonesense b/plugins/stonesense index 3e494d9d9..b7073b664 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 3e494d9d968add443ebd63cc167933cc813f0eee +Subproject commit b7073b664310b909989ebe68de36a164e452825a diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 19237429e..2b44d11ae 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -124,7 +124,8 @@ static const MatType M_ADAMANTINE = MatType("adamantine", df::job_material_category::mask_strand, df::armor_general_flags::SOFT); static const std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER, M_ADAMANTINE }; -static std::list material_order = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; // M_ADAMANTINE is not included by default +static const std::list default_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; // adamantine not included by default +static std::list material_order = default_materials; static struct BadFlags { uint32_t whole; @@ -143,36 +144,46 @@ static struct BadFlags { } badFlags; class Tailor { -private: - std::map, int> available; // key is item type & size - std::map, int> needed; // same - std::map, int> queued; // same +private: std::map sizes; // this maps body size to races - std::map, int> orders; // key is item type, item subtype, size + + std::map, int> available; + + std::map, int> needed; + + std::map, int> orders; std::map supply; std::map reserves; int default_reserve = 10; + bool inventory_sanity_checking = false; + public: + void set_debug_flag(bool f) + { + inventory_sanity_checking = f; + } + void reset() { available.clear(); needed.clear(); - queued.clear(); sizes.clear(); - orders.clear(); supply.clear(); + orders.clear(); } void scan_clothing() { - for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing" + for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "nontattered clothing" { if (i->flags.whole & badFlags.whole) + { continue; + } if (i->getWear() >= 1) continue; df::item_type t = i->getType(); @@ -180,6 +191,18 @@ public: available[std::make_pair(t, size)] += 1; } + + if (DBG_NAME(cycle).isEnabled(DebugCategory::LDEBUG)) + { + for (auto& i : available) + { + df::item_type t; + int size; + std::tie(t, size) = i.first; + DEBUG(cycle).print("tailor: %d %s of size %d found\n", + i.second, ENUM_KEY_STR(item_type, t).c_str(), size); + } + } } void scan_materials() @@ -191,7 +214,7 @@ public: if (i->flags.whole & badFlags.whole) continue; - if (require_dyed && !i->hasImprovements()) + if (require_dyed && (!i->isDyed())) { // only count dyed std::string d; @@ -239,14 +262,13 @@ public: if (!Units::isOwnCiv(u) || !Units::isOwnGroup(u) || !Units::isActive(u) || - Units::isBaby(u)) - continue; // skip units we don't control + Units::isBaby(u) || + !Units::casteFlagSet(u->race, u->caste, df::enums::caste_raw_flags::EQUIPS)) + continue; // skip units we don't control or that can't wear clothes std::set wearing; - wearing.clear(); - + std::set ordered; std::deque worn; - worn.clear(); for (auto inv : u->inventory) { @@ -261,33 +283,35 @@ public: int usize = world->raws.creatures.all[u->race]->adultsize; sizes[usize] = u->race; - for (auto ty : std::set{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES }) - { - if (wearing.count(ty) == 0) - { - TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", - ENUM_KEY_STR(item_type, ty).c_str(), - usize, - Translation::TranslateName(&u->name, false).c_str()); - needed[std::make_pair(ty, usize)] += 1; - } - } - for (auto w : worn) { auto ty = w->getType(); - auto oo = itemTypeMap.find(ty); - if (oo == itemTypeMap.end()) - { - continue; - } - const df::job_type o = oo->second; int isize = world->raws.creatures.all[w->getMakerRace()]->adultsize; std::string description; w->getItemDescription(&description, 0); - if (available[std::make_pair(ty, usize)] > 0) + if (wearing.count(ty) == 0) + { + if (available[std::make_pair(ty, usize)] > 0) + { + available[std::make_pair(ty, usize)] -= 1; + DEBUG(cycle).print("tailor: allocating a %s (size %d) to %s\n", + ENUM_KEY_STR(item_type, ty).c_str(), usize, + Translation::TranslateName(&u->name, false).c_str()); + wearing.insert(ty); + } + else if (ordered.count(ty) == 0) + { + DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement, but none available\n", + description.c_str(), isize, + Translation::TranslateName(&u->name, false).c_str(), usize); + needed[std::make_pair(ty, usize)] += 1; + ordered.insert(ty); + } + } + + if (wearing.count(ty) > 0) { if (w->flags.bits.owned) { @@ -301,23 +325,21 @@ public: ); } - if (wearing.count(ty) == 0) - { - DEBUG(cycle).print("tailor: allocating a %s (size %d) to %s\n", - ENUM_KEY_STR(item_type, ty).c_str(), usize, - Translation::TranslateName(&u->name, false).c_str()); - available[std::make_pair(ty, usize)] -= 1; - } - if (w->getWear() > 1) w->flags.bits.dump = true; } - else + + } + + for (auto ty : std::set{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES }) + { + if (wearing.count(ty) == 0 && ordered.count(ty) == 0) { - DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement, but none available\n", - description.c_str(), isize, - Translation::TranslateName(&u->name, false).c_str(), usize); - orders[std::make_tuple(o, w->getSubtype(), usize)] += 1; + TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", + ENUM_KEY_STR(item_type, ty).c_str(), + usize, + Translation::TranslateName(&u->name, false).c_str()); + needed[std::make_pair(ty, usize)] += 1; } } } @@ -333,6 +355,9 @@ public: int size = a.first.second; int count = a.second; + if (count <= 0) + continue; + int sub = 0; std::vector v; @@ -380,14 +405,31 @@ public: if (f == jobTypeMap.end()) continue; - auto sub = o->item_subtype; int race = o->hist_figure_id; + + for (auto& m : all_materials) + { + if (o->material_category.whole == m.job_material.whole) + { + supply[m] -= o->amount_left; + TRACE(cycle).print("tailor: supply of %s reduced by %d due to being required for an existing order\n", + m.name.c_str(), o->amount_left); + } + } + if (race == -1) continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs int size = world->raws.creatures.all[race]->adultsize; - orders[std::make_tuple(o->job_type, sub, size)] -= o->amount_left; + + auto tt = jobTypeMap.find(o->job_type); + if (tt == jobTypeMap.end()) + { + continue; + } + + needed[std::make_pair(tt->second, 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(), @@ -524,6 +566,18 @@ public: } return ordered; } + + int do_cycle() + { + reset(); + scan_clothing(); + scan_materials(); + scan_replacements(); + scan_existing_orders(); + create_orders(); + return place_orders(); + } + }; static std::unique_ptr tailor_instance; @@ -589,7 +643,7 @@ static void set_material_order() { material_order.push_back(M_ADAMANTINE); } if (!material_order.size()) - std::copy(all_materials.begin(), all_materials.end(), std::back_inserter(material_order)); + std::copy(default_materials.begin(), default_materials.end(), std::back_inserter(material_order)); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -683,13 +737,7 @@ static int do_cycle(color_ostream &out) { DEBUG(cycle,out).print("running %s cycle\n", plugin_name); - tailor_instance->reset(); - tailor_instance->scan_clothing(); - tailor_instance->scan_materials(); - tailor_instance->scan_replacements(); - tailor_instance->create_orders(); - tailor_instance->scan_existing_orders(); - return tailor_instance->place_orders(); + return tailor_instance->do_cycle(); } ///////////////////////////////////////////////////// @@ -730,9 +778,18 @@ static int tailor_getMaterialPreferences(lua_State *L) { return 1; } +static void tailor_setDebugFlag(color_ostream& out, bool enable) +{ + DEBUG(config, out).print("entering tailor_setDebugFlag\n"); + + tailor_instance->set_debug_flag(enable); + +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(tailor_doCycle), DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), + DFHACK_LUA_FUNCTION(tailor_setDebugFlag), DFHACK_LUA_END }; diff --git a/plugins/title-version.cpp b/plugins/title-version.cpp deleted file mode 100644 index 66bb159ad..000000000 --- a/plugins/title-version.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "Core.h" -#include "Console.h" -#include "Export.h" -#include "PluginManager.h" -#include "modules/Gui.h" -#include "modules/Screen.h" -#include "VTableInterpose.h" -#include "DFHackVersion.h" - -#include "df/graphic.h" -#include "df/viewscreen_optionst.h" -#include "df/viewscreen_titlest.h" -#include "uicommon.h" - -using std::vector; -using std::string; -using namespace DFHack; - -DFHACK_PLUGIN("title-version"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(gps); - -void draw_version(int start_x, int start_y) { - int x = start_x, - y = start_y; - - OutputString(COLOR_WHITE, x, y, string("DFHack ") + DFHACK_VERSION); - if (!DFHACK_IS_RELEASE) - { - OutputString(COLOR_WHITE, x, y, " (dev)"); - x = start_x; y++; - OutputString(COLOR_WHITE, x, y, "Git: "); - OutputString(COLOR_WHITE, x, y, DFHACK_GIT_DESCRIPTION); - } - if (strlen(DFHACK_BUILD_ID)) - { - x = start_x; y++; - OutputString(COLOR_WHITE, x, y, "Build ID: "); - OutputString(COLOR_WHITE, x, y, DFHACK_BUILD_ID); - } - if (DFHACK_IS_PRERELEASE) - { - x = start_x; y++; - OutputString(COLOR_LIGHTRED, x, y, "Pre-release build"); - } -} - -struct title_version_hook : df::viewscreen_titlest { - typedef df::viewscreen_titlest interpose_base; - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - if (!loading) - draw_version(0, 0); - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(title_version_hook, render); - -struct options_version_hook : df::viewscreen_optionst { - typedef df::viewscreen_optionst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - if (!msg_quit && !in_retire_adv && !msg_peasant && - !in_retire_dwf_abandon_adv && !in_abandon_dwf && !ending_game) - draw_version(2, gps->dimy - 6); - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(options_version_hook, render); - -DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; - - if (enable != is_enabled) - { - if (!INTERPOSE_HOOK(title_version_hook, render).apply(enable) || - !INTERPOSE_HOOK(options_version_hook, render).apply(enable)) - return CR_FAILURE; - - is_enabled = enable; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_init (color_ostream &out, vector &commands) -{ - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown (color_ostream &out) -{ - INTERPOSE_HOOK(title_version_hook, render).remove(); - INTERPOSE_HOOK(options_version_hook, render).remove(); - return CR_OK; -} diff --git a/scripts b/scripts index f3a8d2df7..ad1998a00 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f3a8d2df72fa10311c4c52471b01216e6da90d9b +Subproject commit ad1998a0032ce50e90d05429a4178b668c0840ba diff --git a/travis/authors-rst.py b/travis/authors-rst.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/authors-rst.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/buildmaster-rebuild-pr.py b/travis/buildmaster-rebuild-pr.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/buildmaster-rebuild-pr.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/check-rpc.py b/travis/check-rpc.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/check-rpc.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/download-df.sh b/travis/download-df.sh deleted file mode 100755 index aec2d6d99..000000000 --- a/travis/download-df.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -script_name="$(basename "$0")" -new_script_path="$(dirname "$0")/../ci/${script_name}" - -printf >&2 "\nNote: travis/%s is deprecated. Use ci/%s instead.\n\n" "${script_name}" "${script_name}" - -"${new_script_path}" "$@" -exit $? diff --git a/travis/get-df-version.sh b/travis/get-df-version.sh deleted file mode 100755 index aec2d6d99..000000000 --- a/travis/get-df-version.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -script_name="$(basename "$0")" -new_script_path="$(dirname "$0")/../ci/${script_name}" - -printf >&2 "\nNote: travis/%s is deprecated. Use ci/%s instead.\n\n" "${script_name}" "${script_name}" - -"${new_script_path}" "$@" -exit $? diff --git a/travis/lint.py b/travis/lint.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/lint.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/run-tests.py b/travis/run-tests.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/run-tests.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/script-docs.py b/travis/script-docs.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/script-docs.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode) diff --git a/travis/script-syntax.py b/travis/script-syntax.py deleted file mode 100755 index cf52385b5..000000000 --- a/travis/script-syntax.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import os -import subprocess -import sys - -script_name = os.path.basename(__file__) -new_script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ci', script_name) - -sys.stderr.write('\nNote: travis/{script_name} is deprecated. Use ci/{script_name} instead.\n\n'.format(script_name=script_name)) -sys.stderr.flush() - -p = subprocess.run([sys.executable, new_script_path] + sys.argv[1:]) -sys.exit(p.returncode)