diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3bc931305a..c2d946e300 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -66,7 +66,7 @@ on: default: false gcc-ver: type: string - default: "10" + default: "11" jobs: build-linux64: diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9257a21081..27d5c1f8ae 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -66,12 +66,20 @@ on: jobs: build-win64: name: Build win64 - runs-on: ubuntu-22.04 + runs-on: windows-2022 steps: - - name: Install dependencies + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Install build dependencies + run: | + choco install sccache + pip install Jinja2 + - name: Install doc dependencies + if: inputs.docs run: | - sudo apt-get update - sudo apt-get install ccache + pip install sphinx - name: Clone DFHack uses: actions/checkout@v4 with: @@ -108,58 +116,87 @@ jobs: ref: main ssh-key: ${{ secrets.DFHACK_3RDPARTY_TOKEN }} path: depends/steam + - name: Prepare output directories + run: | + mkdir output + mkdir pdb + - name: Get sccache path + run: echo ("SCCACHE_DIR=" + $env:LOCALAPPDATA + "\Mozilla\sccache\cache") >> $env:GITHUB_ENV - name: Fetch ccache if: inputs.platform-files uses: actions/cache/restore@v4 with: - path: build/win64-cross/ccache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} restore-keys: | win-msvc-${{ inputs.cache-id }} win-msvc - - name: Cross-compile + - uses: ilammy/msvc-dev-cmd@v1 + - name: Configure DFHack + run: | + cmake ` + -S . ` + -B build ` + -GNinja ` + -DDFHACK_BUILD_ARCH=64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX=output ` + -DCMAKE_C_COMPILER_LAUNCHER=sccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache ` + -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} ` + -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' ` + -DBUILD_LIBRARY=${{ inputs.platform-files }} ` + -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} ` + -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} ` + -DBUILD_DOCS:BOOL=${{ inputs.docs }} ` + -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} ` + -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} ` + -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} ` + -DBUILD_TESTS:BOOL=${{ inputs.tests }} ` + -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ` + ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} + - name: Build DFHack env: - CMAKE_EXTRA_ARGS: -DBUILD_PDBS:BOOL=${{ inputs.cache-id == 'release' }} -DDFHACK_RUN_URL='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' -DBUILD_LIBRARY=${{ inputs.platform-files }} -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files && inputs.plugins }} -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} -DBUILD_DOCS:BOOL=${{ inputs.docs }} -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} -DINSTALL_SCRIPTS:BOOL=${{ inputs.common-files }} -DBUILD_DFLAUNCH:BOOL=${{ inputs.launchdf }} -DBUILD_TESTS:BOOL=${{ inputs.tests }} -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || '' }} + SCCACHE_CACHE_SIZE: 500M run: | - cd build - bash -x build-win64-from-linux.sh + ninja install -C build - name: Finalize cache run: | cd build - ccache -d win64-cross/ccache --show-stats --verbose - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '500M' || '150M' }} - ccache -d win64-cross/ccache --cleanup - ccache -d win64-cross/ccache --max-size ${{ inputs.cache-id == 'release' && '2G' || '500M' }} - ccache -d win64-cross/ccache --zero-stats + sccache --show-stats + sccache --zero-stats - name: Save ccache if: inputs.platform-files && !inputs.cache-readonly uses: actions/cache/save@v4 with: - path: build/win64-cross/ccache + path: ${{ env.SCCACHE_DIR }} key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} - name: Format artifact name if: inputs.artifact-name id: artifactname run: | - if test "false" = "${{ inputs.append-date-and-hash }}"; then - echo name=${{ inputs.artifact-name }} >> $GITHUB_OUTPUT - else - echo name=${{ inputs.artifact-name }}-$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - fi - - name: Prep artifact - if: inputs.artifact-name + if ("${{ inputs.append-date-and-hash }}" -eq "false") { + "name=${{ inputs.artifact-name }}" | Out-File -Append $env:GITHUB_OUTPUT + } else { + $date = Get-Date -Format "yyyMMdd" + $hash = git rev-parse --short HEAD + "name=${{ inputs.artifact-name}}-$date-$hash" | Out-File -Append $env:GITHUB_OUTPUT + } + - name: Prep pdbs + if: inputs.artifact-name && inputs.cache-id == 'release' run: | - cd build/win64-cross/output - tar cjf ../../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + Get-ChildItem -Recurse -File -Path "build" -Filter *.pdb | + Copy-Item -Destination "pdb" - name: Upload artifact if: inputs.artifact-name uses: actions/upload-artifact@v4 with: name: ${{ steps.artifactname.outputs.name }} - path: ${{ steps.artifactname.outputs.name }}.tar.bz2 + path: output + include-hidden-files: true - name: Upload PDBs if: inputs.artifact-name && inputs.cache-id == 'release' uses: actions/upload-artifact@v4 with: name: ${{ steps.artifactname.outputs.name }}_pdb - path: build/win64-cross/pdb + path: pdb diff --git a/.github/workflows/generate-symbols.yml b/.github/workflows/generate-symbols.yml index 85a0067c85..27d2286474 100644 --- a/.github/workflows/generate-symbols.yml +++ b/.github/workflows/generate-symbols.yml @@ -36,6 +36,7 @@ on: default: default options: - default + - experimental - testing - adventure_test - beta @@ -133,7 +134,7 @@ jobs: +quit tar xjf dfhack-symbols-linux64-build.tar.bz2 -C DF_steam xml/symbols_gen_linux.sh ${{ inputs.version == 'auto' && '50.0' || inputs.version }} STEAM DF_steam - if [ "${{ inputs.version }}" = "auto" ]; then + if [ "${{ inputs.version }}" == "auto" ]; then while pgrep dwarfort; do echo "waiting for DF to exit" sleep 0.5 @@ -324,6 +325,7 @@ jobs: uses: actions/download-artifact@v4 with: name: dfhack-symbols-windows64-build + path: dfhack-windows64-build - name: Setup steamcmd id: steamcmd uses: CyberAndrii/setup-steamcmd@v1 @@ -354,7 +356,7 @@ jobs: +login $STEAM_USERNAME \ "+app_update 975370 $BETA_PARAMS validate" \ +quit - tar xjf dfhack-symbols-windows64-build.tar.bz2 -C DF_steam + cp -r dfhack-windows64-build/. DF_steam/ cp xml/symbols.xml DF_steam/hack cd DF_steam "./Dwarf Fortress.exe" & diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 16a4602141..28a56fc609 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -58,8 +58,6 @@ jobs: run: | mkdir artifacts cd dfhack-windows64-build - tar xjf dfhack-windows64-build.tar.bz2 - rm dfhack-windows64-build.tar.bz2 zip -qr ../artifacts/dfhack-${{ steps.gettag.outputs.name }}-Windows-64bit.zip . cd ../dfhack-linux64-build mv dfhack-linux64-build.tar.bz2 ../artifacts/dfhack-${{ steps.gettag.outputs.name }}-Linux-64bit.tar.bz2 diff --git a/.github/workflows/steam-deploy.yml b/.github/workflows/steam-deploy.yml index 2fbe6849e1..ccd486bda3 100644 --- a/.github/workflows/steam-deploy.yml +++ b/.github/workflows/steam-deploy.yml @@ -70,8 +70,10 @@ jobs: run: | for name in common win64 linux64; do cd ${name}-depot - tar xjf ${name}-depot.tar.bz2 - rm ${name}-depot.tar.bz2 + if [ -f ${name}-depot.tar.bz2 ]; then + tar xjf ${name}-depot.tar.bz2 + rm ${name}-depot.tar.bz2 + fi cd .. done - name: Get short SHA of commit diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 3fbf683b63..054c964751 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -65,9 +65,14 @@ jobs: uses: actions/download-artifact@v4 with: name: test-${{ inputs.compiler }} + path: ${{ env.DF_FOLDER }} - name: Install DFHack shell: bash - run: tar xjf test-${{ inputs.compiler }}.tar.bz2 -C ${{ env.DF_FOLDER }} + run: | + if [ -f "${{ env.DF_FOLDER }}/test-${{ inputs.compiler }}.tar.bz2" ]; then + tar xjf "${{ env.DF_FOLDER }}/test-${{ inputs.compiler }}.tar.bz2" -C "${{ env.DF_FOLDER }}" + rm "${{ env.DF_FOLDER }}/test-${{ inputs.compiler }}.tar.bz2" + fi - name: Start X server if: inputs.os == 'ubuntu' run: Xvfb :0 -screen 0 1600x1200x24 & @@ -77,7 +82,7 @@ jobs: DISPLAY: :0 TERM: xterm-256color with: - timeout_minutes: 1 + timeout_minutes: 3 command: python ci/run-tests.py --keep-status "${{ env.DF_FOLDER }}" - name: Check RPC interface run: python ci/check-rpc.py "${{ env.DF_FOLDER }}/dfhack-rpc.txt" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73bb3f82d..175a1adf48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,9 +59,9 @@ jobs: fail-fast: false matrix: include: - - gcc: 10 + - gcc: 11 # baseline compatibility with ubuntu LTS 22.04 plugins: "default" - - gcc: 12 + - gcc: 12 # highest available in ubuntu 22.04 plugins: "all" test-windows: @@ -96,7 +96,7 @@ jobs: dfhack_repo: ${{ inputs.dfhack_repo }} dfhack_ref: ${{ inputs.dfhack_ref }} os: ubuntu - compiler: gcc-10 + compiler: gcc-11 plugins: default config: default diff --git a/.github/workflows/watch-df-release.yml b/.github/workflows/watch-df-release.yml index d86f993430..50950140d1 100644 --- a/.github/workflows/watch-df-release.yml +++ b/.github/workflows/watch-df-release.yml @@ -15,13 +15,17 @@ jobs: fail-fast: false matrix: # df_steam_branch: which DF Steam branch to watch - # platform: leave blank to default to all + # platform: for symbols generation; leave blank to default to all # structures_ref: leave blank to default to master # dfhack_ref: leave blank if no structures update is desired # steam_branch: leave blank if no DFHack steam push is desired include: - df_steam_branch: public - - df_steam_branch: beta +# - df_steam_branch: beta +# - df_steam_branch: experimental +# structures_ref: experimental +# dfhack_ref: experimental +# steam_branch: experimental steps: - name: Fetch state uses: actions/cache/restore@v4 @@ -31,7 +35,8 @@ jobs: - name: Compare branch metadata uses: nick-fields/retry@v3 with: - timeout_minutes: 2 + timeout_minutes: 5 + retry_wait_seconds: 60 command: | blob=$(wget 'https://api.steamcmd.net/v1/info/975370?pretty=1' -O- | \ awk '/^ *"branches"/,0' | \ diff --git a/.gitignore b/.gitignore index 60a0e1b6b2..eda2b226d5 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ build/compile_commands.json build/dfhack_setarch.txt build/ImportExecutables.cmake build/Testing +build/_deps # Python binding binaries *.pyc @@ -64,6 +65,7 @@ build/CPack*Config.cmake # VSCode files .vscode +*.code-workspace # ctags file tags diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01094ed98d..7cd0f4c1f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,11 +20,11 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.39.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: forbid-tabs exclude_types: diff --git a/CMakeLists.txt b/CMakeLists.txt index f102c35554..8a8223561e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,12 @@ cmake_minimum_required(VERSION 3.18 FATAL_ERROR) cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) +set(CMAKE_INSTALL_MESSAGE "LAZY") # set up versioning. -set(DF_VERSION "51.06") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DF_VERSION "53.16") +set(DFHACK_RELEASE "r4rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 2) @@ -32,7 +33,7 @@ else(CMAKE_CONFIGURATION_TYPES) set(DFHACK_TYPE_HELP "Choose the type of build, options are: Release and RelWithDebInfo") # Prevent cmake C module attempts to overwrite our help string if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "${DFHACK_TYPE_HELP}") + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "${DFHACK_TYPE_HELP}" FORCE) else(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}" CACHE STRING "${DFHACK_TYPE_HELP}") endif(NOT CMAKE_BUILD_TYPE) @@ -46,8 +47,14 @@ option(REMOVE_SYMBOLS_FROM_DF_STUBS "Remove debug symbols from DF stubs. (Reduce macro(CHECK_GCC compiler_path) execute_process(COMMAND ${compiler_path} -dumpversion OUTPUT_VARIABLE GCC_VERSION_OUT) string(STRIP "${GCC_VERSION_OUT}" GCC_VERSION_OUT) - if(${GCC_VERSION_OUT} VERSION_LESS "10") - message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 10 or later") + if(${GCC_VERSION_OUT} VERSION_LESS "11") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 11 or later") + endif() + # GCC 16 currently has a defect that prevents it from compiling DFHack + # -Warray-bounds is broken in GCC 16 and we'd rather disable the compiler than remove this warning + # will reconsider when this defect is fixed in a future GCC release + if(${GCC_VERSION_OUT} VERSION_GREATER_EQUAL "16") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 15 or earlier") endif() endmacro() @@ -66,7 +73,7 @@ endif() if(WIN32) set(MSVC_MIN_VER 1930) - set(MSVC_MAX_VER 1942) + set(MSVC_MAX_VER 1944) if(NOT MSVC) message(SEND_ERROR "No MSVC found! MSVC 2022 version ${MSVC_MIN_VER} to ${MSVC_MAX_VER} is required.") elseif((MSVC_VERSION LESS MSVC_MIN_VER) OR (MSVC_VERSION GREATER MSVC_MAX_VER)) @@ -122,6 +129,9 @@ if(MSVC) # Enable C5038 - This is equivalent to gcc's -Werror=reorder, which is enabled by default by gcc -Wall add_compile_options("/w15038") + # Enable C4062 - Warns about missing enum case in switch statement, equivalent to gcc -Wswitch + add_compile_options("/w14062") + # MSVC panics if an object file contains more than 65,279 sections. this # happens quite frequently with code that uses templates, such as vectors. add_compile_options("/bigobj") @@ -223,13 +233,10 @@ set(DFHACK_DATA_DESTINATION hack) ## where to install things (after the build is done, classic 'make install' or package structure) # the dfhack libraries will be installed here: -if(UNIX) - # put the lib into DF/hack - set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) -else() - # windows is crap, therefore we can't do nice things with it. leave the libs on a nasty pile... - set(DFHACK_LIBRARY_DESTINATION .) -endif() + +# put the lib into DF/hack +# windows will find it because dfhooks will `AddDllDirectory` the hack folder at runtime +set(DFHACK_LIBRARY_DESTINATION ${DFHACK_DATA_DESTINATION}) # external tools will be installed here: set(DFHACK_BINARY_DESTINATION .) @@ -264,7 +271,7 @@ if(UNIX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32 -march=i686") endif() string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") - set(CMAKE_INSTALL_RPATH ${DFHACK_LIBRARY_DESTINATION}) + set(CMAKE_INSTALL_RPATH "$ORIGIN") elseif(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") @@ -344,6 +351,17 @@ if(BUILD_LIBRARY) endif() endif() +# this can be made conditional once we get to better platform support for std::format +INCLUDE(FetchContent) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 12.2.0 +) +FetchContent_MakeAvailable(fmt) +set(FMTLIB fmt) +add_definitions("-DUSE_FMTLIB") + if(APPLE) # libstdc++ (GCC 4.8.5 for OS X 10.6) # fixes crash-on-unwind bug in DF's libstdc++ @@ -425,7 +443,7 @@ macro(dfhack_test name files) if(BUILD_LIBRARY AND UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated add_executable(${name} ${files}) target_include_directories(${name} PUBLIC depends/googletest/googletest/include) - target_link_libraries(${name} dfhack gtest) + target_link_libraries(${name} dfhack lua ${FMTLIB} gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() @@ -523,7 +541,7 @@ if(BUILD_DOCS) add_custom_command(OUTPUT ${SPHINX_OUTPUT} COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/build.py" - ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" -- -q -W + ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" --quiet -- -W DEPENDS ${SPHINX_DEPS} COMMENT "Building documentation with Sphinx" ) diff --git a/build/.gitignore b/build/.gitignore index f25d10ca7b..0675a165cf 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -7,3 +7,5 @@ _CPack_Packages *.tar.* .cmake win64-cross +dest +DF diff --git a/ci/download-df.sh b/ci/download-df.sh index ed8b373a25..a07f75f2a1 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -14,7 +14,7 @@ fi df_url="https://www.bay12games.com/dwarves/df_${minor}_${patch}" if test "$OS_TARGET" = "windows"; then WGET="C:/msys64/usr/bin/wget.exe" - df_url="${df_url}_win_s.zip" + df_url="${df_url}_win.zip" df_archive_name="df.zip" df_extract_cmd="unzip -d ${DF_FOLDER}" elif test "$OS_TARGET" = "ubuntu"; then diff --git a/ci/test.lua b/ci/test.lua index e57facf14b..6cc7cafc75 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -6,6 +6,7 @@ local gui = require('gui') local helpdb = require('helpdb') local json = require('json') local mock = require('test_util.mock') +local overlay = require('plugins.overlay') local script = require('gui.script') local utils = require('utils') @@ -289,12 +290,13 @@ local MODES = { local function load_test_config(config_file) local config = {} + print ("loading test config from " .. config_file) if dfhack.filesystem.isfile(config_file) then config = json.decode_file(config_file) end if not config.test_dir then - config.test_dir = dfhack.getHackPath() .. 'scripts/test' + config.test_dir = dfhack.getHackPath() .. '/scripts/test' end if not config.save_dir then @@ -426,6 +428,7 @@ end local function finish_tests(done_command) dfhack.internal.IN_TEST = false + overlay.rescan() if done_command and #done_command > 0 then dfhack.run_command(done_command) end @@ -724,6 +727,10 @@ local function main(args) qerror(('Invalid test folder: "%s"'):format(config.test_dir)) end + if config.done_command then + print(('Will run "%s" after tests complete'):format(config.done_command)) + end + local counts = { tests = 0, tests_ok = 0, diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 355182bfb7..75146071b8 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -24,6 +24,10 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/ FILES_MATCHING PATTERN "*" PATTERN blueprints/test EXCLUDE) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/patches/ + DESTINATION ${DFHACK_DATA_DESTINATION}/patches +) + if(BUILD_TESTS) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/test/ DESTINATION "${DFHACK_DATA_DESTINATION}/data/blueprints/test") diff --git a/data/art/design.png b/data/art/design.png index 787b9962a1..38fed7e088 100644 Binary files a/data/art/design.png and b/data/art/design.png differ diff --git a/data/art/design_toolbar.png b/data/art/design_toolbar.png new file mode 100644 index 0000000000..935a332f00 Binary files /dev/null and b/data/art/design_toolbar.png differ diff --git a/data/art/logo.png b/data/art/logo.png index 133dc5aaf8..c34407b372 100644 Binary files a/data/art/logo.png and b/data/art/logo.png differ diff --git a/data/art/mass_remove_toolbar.png b/data/art/mass_remove_toolbar.png new file mode 100644 index 0000000000..d1b09ac93b Binary files /dev/null and b/data/art/mass_remove_toolbar.png differ diff --git a/data/art/logo_hovered.png b/data/art/sitemap_toolbar.png similarity index 55% rename from data/art/logo_hovered.png rename to data/art/sitemap_toolbar.png index fe82cc1aed..a29fa9b900 100644 Binary files a/data/art/logo_hovered.png and b/data/art/sitemap_toolbar.png differ diff --git a/data/blueprints/aquifer_tap.csv b/data/blueprints/aquifer_tap.csv index b6675cee28..dc79e523d4 100644 --- a/data/blueprints/aquifer_tap.csv +++ b/data/blueprints/aquifer_tap.csv @@ -7,9 +7,9 @@ Here's the procedure: "" 2) Dig a one-tile-wide tunnel from where you want the water to end up (e.g. your well cistern) to an area on the same z-level directly below the target light aquifer. Dig a one-tile-wide diagonal segment in this tunnel near the cistern side so water that will eventually flow through the tunnel is depressurized. "" -"3) Pause the game. From the end of that tunnel, go down one z-level, enable damp dig mode in the dig toolbar, then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). If you only have one layer of aquifer, you should end the staircase one level below the aquifer so when we dig the tap, it will extend up into the aquifer level. Your tunnel should connect to the staircase one z-level above the bottom of the staircase." +"3) Pause the game. From the end of that tunnel, go down one z-level then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). Your original tunnel should connect to the staircase one z-level above the bottom of the staircase." "" -"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint designates ramps for digging so two layers of aquifer can contribute to the water collector. It also changes the staircase tile below the tap to a ""blueprint"" tile so your miners don't dig the tap before your drainage tunnel is ready." +"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint also changes the staircase tile below the tap to a vanilla ""blueprint"" tile (shaded in blue) so your miners don't dig the tap before your drainage tunnel is ready." "" "5) You can now unpause the game. From the bottom of the staircase (the z-level below where the water will flow to your cisterns), dig a straight, one-tile wide tunnel to the closest edge of the map. This is your emergency drainage tunnel. Smooth the map edge tile and carve a fortification. The water can flow through the fortification and off the map, allowing the dwarves to dig out the aquifer tap without drowning." "" @@ -17,7 +17,7 @@ Here's the procedure: "" "7) If you want, haul away any boulders in the tunnels and/or smooth the tiles (e.g. mark them for dumping -- hotkey i-p -- and wait for them to be dumped). Enable prioritize in gui/control-panel to focus dwarves on dumping tasks and make it go faster. You won't be able to access any of this area once it fills up with water!" "" -"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders and remove the ramps if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." +"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." "8b) Sometimes, DF gets into a bad state with mining designations and miners will refuse to dig the stairway tile. If this happens to you, enter mining mode, enable the keyboard cursor if it's not already enabled (hotkey: Alt-k), highlight the undug stair designation, and run dig-now here in gui/launcher. You might also have to do this for the down stair designation in the center of the aquifer tap. Your miners should be able to handle the rest without assistance." "9) Once everything is dug out and all dwarves are out of the waterways, close the floodgate. Your cisterns will fill with water. Since the waterway to your cisterns is depressurized (due to the diagonal tunnel you dug), the cisterns will stay forever full, but will not flood." @@ -32,54 +32,42 @@ i <- cistern outlet level with diagonal tunnel to depressurize "u <- up stairs, drainage level" "" "Good luck! If done right, this method is the safest way to supply your fort with clean water." -#dig label(dig) start(13 13 center of tap) light aquifer water collector -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdj3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3, -,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, -,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,mdr3,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -#>,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,`,`,`,`,`,`,`,`,`,`,`,mbmdi3,`,`,`,`,`,`,`,`,`,`,`, -,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, -,,,`,,,`,,,,,,`,,,,,,`,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, -,,,`,,,,,,,,,`,,,,,,,,,`,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, -,,,,,,,,,,,,`,,,,,,,,,,,, +#dig label(dig) start(10 10 center of tap) light aquifer water collector +,,,,,,,,,,,,,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,mdd3,mdd3,,,mdd3,mdd3,,,mdj3,,,mdd3,mdd3,,,mdd3,mdd3, +,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3, +,,,mdd3,mdd3,,,mdd3,mdd3,,mdd3,mdd3,,,mdd3,mdd3,,, +,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,mdd3,, +,,mdd3,,,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,,,mdd3,, +,,,mdd3,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,mdd3,,, +,,,,mdd3,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,mdd3,,,, +,,,,,,mdd3,mdd3,mdd3,,mdd3,mdd3,mdd3,,,,,, +,,,,,,,,mdd3,mdd3,mdd3,,,,,,,, +,,,,,,,,,,,,,,,,,, +#>,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,, +,,,,,,,,`,`,`,,,,,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,`,`,,,`,`,,,mbmdi3,,,`,`,,,`,`, +,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`, +,,,`,`,,,`,`,,`,`,,,`,`,,, +,,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,, +,,`,,,`,`,,`,`,`,,`,`,,,`,, +,,,`,`,`,`,,`,`,`,,`,`,`,`,,, +,,,,`,,`,`,`,,`,`,`,,`,,,, +,,,,,,`,`,`,,`,`,`,,,,,, +,,,,,,,,`,`,`,,,,,,,, diff --git a/data/blueprints/dreamfort.csv b/data/blueprints/dreamfort.csv index 72013fd487..c1266edb6e 100644 --- a/data/blueprints/dreamfort.csv +++ b/data/blueprints/dreamfort.csv @@ -113,6 +113,7 @@ You can save some time by setting up your settings in gui/control-panel before y - autobutcher - autobutcher target 10 10 14 2 BIRD_GOOSE - autochop +- autocheese - autofarm - autofarm threshold 150 grass_tail_pig - autofish @@ -134,6 +135,7 @@ You can save some time by setting up your settings in gui/control-panel before y """Gameplay"" subtab:" - combine - dwarfvet +- immortal-cravings - timestream - work-now "Note that if you've already started your fort and have missed the ""new fort"" trigger, you can enable these tools on the ""Enabled"" tabs instead. You can run the one-time commands (like ban-cooking all) manually from gui/launcher." @@ -283,11 +285,11 @@ Surface Walkthrough: Sieges and Prisoner Processing: Here are some tips and procedures for handling seiges -- including how to clean up afterwards! "" -"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas of your fort. In particular, it should not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." +"- Your ""Inside+"" burrow will automatically grow with your fort and should include only safe areas. In particular, it does not include the ""atrium"" area (where the ""siege bait"" pasture is) or the trapped hallways." "" "- When a siege begins, set your civilian alert (attach the alert to your ""Inside+"" burrow if it isn't already) to ensure all your civilians stay out of danger. Immediately pull the lever to close the outer main gate. It is also wise to close the trade depot and inner main gate as well. That way, if enemies get past the traps, they'll have to go through the soldiers in your barracks (assuming you have a military)." "" -"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and meet the siege with your military. It's up to you!" +"- During a siege, you can use the levers to control how attackers path through the trapped corridors. If there are more enemies than cage traps, time your lever pulling so that the inner gates snap closed before your last cage trap is sprung. Then the remaining attackers will have to backtrack and go through the other trap-filled hallway. You can also choose *not* to use the trap hallways and instead meet the siege head-on with your military. It's up to you!" "" "- If your cage traps fill up, ensure your hallways are free of uncaged attackers, then close the trap hallway outer gates and open the inner gates. Clear the civilian alert and allow your dwarves to reset all the traps -- make some extra cages in preparation for this! Then re-enable the civilian alert and open the trap hallway outer gates." "" diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index f4068a5cd1..018c66ccde 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -41,6 +41,15 @@ keybinding add Ctrl-A@choose_start_site gui/embark-anywhere # in the game root directory keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeonmode/ViewSheets/UNIT|dungeonmode/ViewSheets/ITEM markdown +# gui/sitemap +keybinding add Ctrl-G@dwarfmode/Default|dungeonmode/Default gui/sitemap + +# toggle keyboard cursor +keybinding add Alt-K@dwarfmode|dungeonmode/Default|dungeonmode/Look toggle-kbd-cursor + +# gui/journal +keybinding add Ctrl-J@dwarfmode|dungeonmode gui/journal + ###################### # dwarfmode bindings # @@ -49,6 +58,9 @@ keybinding add Ctrl-T@dwarfmode/ViewSheets/UNIT|dwarfmode/ViewSheets/ITEM|dungeo # quicksave keybinding add Ctrl-Alt-S@dwarfmode quicksave +# toggle spectate +keybinding add Ctrl-Shift-S@dwarfmode/Default "spectate toggle" + # designate the whole vein for digging keybinding add Ctrl-V@dwarfmode digv keybinding add Ctrl-Shift-V@dwarfmode "digv x" @@ -68,9 +80,6 @@ keybinding add Ctrl-Shift-T@dwarfmode gui/teleport # apply blueprints to the map keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort -# toggle keyboard cursor -keybinding add Alt-K@dwarfmode toggle-kbd-cursor - # Stocks plugin #keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" @@ -145,9 +154,6 @@ keybinding add Alt-K@dwarfmode toggle-kbd-cursor keybinding add Ctrl-D@dwarfmode/Default gui/design keybinding add Ctrl-M@dwarfmode/Default gui/mass-remove -# gui/journal -keybinding add Ctrl-J@dwarfmode gui/journal - ######################## # dungeonmode bindings # diff --git a/data/init/onMapLoad.default.init b/data/init/onMapLoad.default.init index 44986a044c..fb5912fea4 100644 --- a/data/init/onMapLoad.default.init +++ b/data/init/onMapLoad.default.init @@ -4,3 +4,5 @@ # Please do not edit this file directly. It will be overwritten with new # defaults when you update DFHack. Instead, add your configuration to # dfhack-config/init/onMapLoad.init + +lua require('quickfix').repair_site_id() diff --git a/data/orders/basic.json b/data/orders/basic.json index 688bdaa7ab..e1cd5ac8ff 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -114,27 +114,6 @@ "job" : "CustomReaction", "reaction" : "BREW_DRINK_FROM_PLANT_GROWTH" }, - { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", - "id" : 3, - "is_active" : false, - "is_validated" : false, - "item_conditions" : - [ - { - "condition" : "AtLeast", - "flags" : - [ - "unrotten", - "milk" - ], - "value" : 2 - } - ], - "job" : "MakeCheese" - }, { "amount_left" : 1, "amount_total" : 1, diff --git a/data/patches/README.md b/data/patches/README.md new file mode 100644 index 0000000000..599bbf47fb --- /dev/null +++ b/data/patches/README.md @@ -0,0 +1,13 @@ +Place IDA-exported `.dif` files for use by `binpatch` in subdirectories of this +directory. Each `.dif` file must be in a subdirectory named after the full +symbol table version string. For example, for DF version 51.05, you would use +these subdirectories: + +- "v0.51.05 linux64 CLASSIC" +- "v0.51.05 linux64 ITCH" +- "v0.51.05 linux64 STEAM" +- "v0.51.05 win64 CLASSIC" +- "v0.51.05 win64 ITCH" +- "v0.51.05 win64 STEAM" + +See https://docs.dfhack.org/en/stable/docs/dev/Binpatches.html for more details. diff --git a/depends/dfhooks b/depends/dfhooks index 5b70cd886f..5a904e30a8 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5b70cd886f619b503aa770008fb79aa39a3be6fa +Subproject commit 5a904e30a8bace81c662b44ec7ff076b92edafd1 diff --git a/depends/lua/src/ldo.c b/depends/lua/src/ldo.c index 65158df0b7..6325ebf468 100644 --- a/depends/lua/src/ldo.c +++ b/depends/lua/src/ldo.c @@ -38,6 +38,33 @@ #define errorstatus(s) ((s) > LUA_YIELD) +/* +** {====================================================== +** DFHACK: runtime sanity checks around C function results +** ======================================================= +*/ + +/* +** api_checknelems() compiles to nothing in release builds, so a C +** function (or a continuation function) that returns a result count +** inconsistent with the number of values actually on the stack would +** make luaD_poscall()/moveresults() read and write outside the stack, +** hard-crashing the host process. Convert that condition into a +** catchable Lua error instead. This check is two comparisons on a path +** already dominated by the call itself, so it is cheap enough to run on +** every call, including hot paths like the overlay render loop. +*/ +#define DFHack_checkcresults(L,ci,n,f) \ + if ((n) < 0 || (n) >= (L)->top - (ci)->func) \ + luaG_runerror(L, \ + "lua_CFunction %p reported invalid result count %d", (void *)(f), (n)); + + +/* +** }====================================================== +*/ + + /* ** {====================================================== ** Error-recovery functions @@ -386,6 +413,15 @@ int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, int nres) { } res = ci->func; /* res == final position of 1st result */ L->ci = ci->previous; /* back to caller */ + /* DFHACK: sanity-check the pointers moveresults() is about to use. A + corrupted pointer here would otherwise read or write outside the + stack and hard-crash (or silently corrupt) the host process. */ + if (firstResult < L->stack || firstResult > L->top || + res < L->stack || res >= L->stack_last) { + if (L->top > L->ci->top) + L->top = L->ci->top; /* make sure the error value has a safe home */ + luaG_runerror(L, "corrupted Lua call frame"); + } /* move results to proper place */ return moveresults(L, firstResult, res, nres, wanted); } @@ -434,6 +470,7 @@ int luaD_precall (lua_State *L, StkId func, int nresults) { n = (*f)(L); /* do the actual call */ lua_lock(L); api_checknelems(L, n); + DFHack_checkcresults(L, ci, n, f); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); return 1; } @@ -533,6 +570,7 @@ static void finishCcall (lua_State *L, int status) { n = (*ci->u.c.k)(L, status, ci->u.c.ctx); /* call continuation function */ lua_lock(L); api_checknelems(L, n); + DFHack_checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); /* finish 'luaD_precall' */ } @@ -636,6 +674,7 @@ static void resume (lua_State *L, void *ud) { n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */ lua_lock(L); api_checknelems(L, n); + DFHack_checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ firstArg = L->top - n; /* yield results come from continuation */ } luaD_poscall(L, ci, firstArg, n); /* finish 'luaD_precall' */ diff --git a/docs/Installing.rst b/docs/Installing.rst index a687498e7e..6f925999bc 100644 --- a/docs/Installing.rst +++ b/docs/Installing.rst @@ -89,7 +89,8 @@ To download a development build from GitHub: - Click the number under "Artifacts" (or scroll down) - Click on the ``dfhack-*-build-*`` artifact for your platform to download -The artifacts are "double-zipped". That is, you will have to extract the +The Windows artifacts can be extracted directly into your DF folder. The +Linux artifacts are "double-zipped". That is, you will have to extract the initial zip file to get to the package archive. You can extract this second package the same as if you are doing a manual install (see the next section). diff --git a/docs/Tags.rst b/docs/Tags.rst index 55bfde977b..788520f3a2 100644 --- a/docs/Tags.rst +++ b/docs/Tags.rst @@ -43,7 +43,7 @@ for the tag assignment spreadsheet. - `map `: Tools that interact with the game map. - `military `: Tools that interact with the military. - `plants `: Tools that interact with grass, trees, shrubs, and crops. -- `stockpiles `: Tools that interact with stockpiles. +- `stockpile `: Tools that interact with stockpiles. - `units `: Tools that interact with units. - `workorders `: Tools that interact with workorders. diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 2908cae9d6..eefd440758 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -43,11 +43,13 @@ Cameron Ewell Ozzatron Carter Bray Qartar Chris Dombroski cdombroski Chris Parsons chrismdp +Christian Doczkal chdoc cjhammel cjhammel Clayton Hughes Clément Vuchener cvuchener Corey CoreyJ87 daedsidog daedsidog +Dailton Filho notliad Dan Amlund danamlund Daniel Brooks db48x David Nilsolm @@ -63,6 +65,7 @@ DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash doomchild doomchild Droseran Droseran +dvantwisk dvantwisk DwarvenM DwarvenM Eamon Bode eamondo2 Baron Von Munchhausen EarthPulseAcademy EarthPulseAcademy @@ -119,6 +122,7 @@ Kurik Amudnil Kévin Boissonneault KABoissonneault Lethosor lethosor LordGolias LordGolias +Magnus Anderson magnus-ISU Mark Nielson pseudodragon Mason11987 Mason11987 Matt Regul mattregul @@ -158,8 +162,10 @@ NotRexButCaesar NotRexButCaesar Nuno Fernandes UnknowableCoder nuvu vallode Omniclasm +Ong Ying Gao ong-yinggao98 oorzkws oorzkws OwnageIsMagic OwnageIsMagic +pajawojciech pajawojciech palenerd dlmarquis PassionateAngler PassionateAngler Patrik Lundell PatrikLundell @@ -242,6 +248,7 @@ thurin thurin Tim Siegel softmoth Tim Walberg twalberg Timothy Collett danaris +Timothy Torres timothymtorres Timur Kelman TymurGubayev Tom Jobbins TheBloke Tom Prince @@ -256,6 +263,7 @@ Vitaly Pronkin pronvit mifki ViTuRaS ViTuRaS Vjek vjek Vladimir Florov foxxelias +wala343 wala343 Warmist warmist Wes Malone wesQ3 Will H TSM-EVO diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 5f73b86be3..5235327634 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -28,7 +28,8 @@ autohauler ========== An automated labor management tool that only addressed hauling labors, leaving the assignment of skilled labors entirely up to the player. Fundamentally incompatible with the work detail -system of labor management in v50 of Dwarf Fortress. +system of labor management in v50 of Dwarf Fortress. A dedicated laborer pool for unskilled +jobs (including hauling) is now provided by the `labormanager` command of `autolabor`. .. _automaterial: @@ -258,6 +259,13 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +.. _gui/logcleaner: + +gui/logcleaner +============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _gui/manager-quantity: gui/manager-quantity @@ -278,6 +286,13 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _logcleaner: + +logcleaner +=============== +Removed because changes to Dwarf Fortress internals made the functionality +impossible to implement safely. + .. _masspit: masspit diff --git a/docs/build.py b/docs/build.py index bfb6780b23..bf0dd9e488 100755 --- a/docs/build.py +++ b/docs/build.py @@ -62,6 +62,8 @@ def output_format(s): help='Sphinx executable to run [environment variable: SPHINX; default: "sphinx-build"]') parser.add_argument('-j', '--jobs', type=str, default=os.environ.get('JOBS', 'auto'), help='Number of Sphinx threads to run [environment variable: JOBS; default: "auto"]') + parser.add_argument('-q', '--quiet', action='store_true', + help='Disable most output on stdout (also passed to sphinx-build)') parser.add_argument('--debug', action='store_true', help='Log commands that are run, etc.') parser.add_argument('--offline', action='store_true', @@ -91,6 +93,8 @@ def output_format(s): command = [args.sphinx] + OUTPUT_FORMATS[format_name].args + ['-j', args.jobs] if args.clean: command += ['-E'] + if args.quiet: + command += ['-q'] command += forward_args if args.debug: @@ -98,4 +102,5 @@ def output_format(s): print('Running:', command) subprocess.run(command, check=True, env=sphinx_env) - print('') + if not args.quiet: + print('') diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 3e6970b6fc..e8f206848d 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -9,9 +9,9 @@ Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in :file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` -(the key below the :kbd:`Esc` key on most keyboards). You can also represent -mouse buttons beyond the first three with ``MOUSE4`` through ``MOUSE15``. +Hotkeys can be any combinations of Ctrl/Alt/Super/Shift with any key recognized by SDL. +You can also represent mouse buttons beyond the first three with ``MOUSE4`` +through ``MOUSE15``. Usage ----- @@ -27,12 +27,13 @@ Usage ``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. -The ```` parameter above has the following **case-sensitive** syntax:: +The ```` parameter above has the following case-insensitive syntax:: - [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] + [Ctrl-][Alt-][Super-][Shift-]KEY[@context[|context...]] where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote -optional parts. +optional parts. It is important to note that the key is the non-shifted version +of the key. For example ``!`` would be defined as ``Shift-0``. DFHack commands can advertise the contexts in which they can be usefully run. For example, a command that acts on a selected unit can tell `keybinding` that diff --git a/docs/changelog.txt b/docs/changelog.txt index 774b93e67d..1473e42e81 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,26 +1,30 @@ === Scroll down for changes ===[[[ -The text below is included in docs/dev/Documentation.rst - see that file for more details on the changelog setup. -This is kept in this file as a quick syntax reference. +For information on how to edit/build the changelogs, see `docs/html/docs/dev/Documentation.html` or `docs/dev/Documentation.rst`. -===help +The text between the `syntax-reference` markers is included in `docs/dev/Documentation.rst`, so it must be valid RST. +It is kept in this file as a quick syntax reference. -changelog.txt uses a syntax similar to RST, with a few special sequences: +===syntax-reference-start + +The changelogs use a syntax similar to RST, with a few special sequences: - ``===`` indicates the start of a comment - ``#`` indicates the start of a release name (do not include "DFHack") -- ``##`` indicates the start of a section name (this must be listed in ``gen_changelog.py``) +- ``##`` indicates the start of a section name (which must be listed in ``docs/sphinx_extensions/dfhack/changelog.py``) - ``-`` indicates the start of a changelog entry. **Note:** an entry currently must be only one line. -- ``:`` (colon followed by space) separates the name of a feature from a description of a change to that feature. - Changes made to the same feature are grouped if they end up in the same section. -- ``:\`` (colon, backslash, space) avoids the above behavior -- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in NEWS-dev.rst. - Use this sparingly, e.g. for immediate fixes to one development build in another development build that - are not of interest to users of stable builds only. +- ``:`` (followed by space) separates the name of a feature from a description of a change to that feature. + - Changes made to the same feature are grouped if they end up in the same section. +- ``:\`` (followed by space) avoids the above behavior +- ``- @`` (the space is optional) indicates the start of an entry that should only be displayed in ``NEWS-dev.rst``. + - Use this sparingly, e.g. for immediate fixes to one development build in another development build that + are not of interest to users of stable builds only. - Three ``[`` characters indicate the start of a block (possibly a comment) that spans multiple lines. Three ``]`` characters indicate the end of such a block. -- ``!`` immediately before a phrase set up to be replaced (see gen_changelog.py) stops that occurrence from being replaced. +- ``!`` immediately before a configured replacement (see ``docs/sphinx_extensions/dfhack/changelog.py``) stops that occurrence from being replaced. + +===syntax-reference-end Template for new versions: @@ -37,11 +41,9 @@ Template for new versions: ## API ## Lua -- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings ## Removed -===end ]]] ================================================================================ @@ -54,11 +56,378 @@ Template for new versions: ## New Tools +## New Features +- `autofarm`: farm plots can now be individually excluded from autofarm management via new controls on the farm plot information sheet, which also warn when a farm's crops are being managed by autofarm +- `autofarm`: thresholds can now be assigned to specific growth products of a plant (e.g. ``BUSH_QUARRY:LEAVES``), so crops that are grown for their growth items can be farmed to a target amount of that product + +## Fixes +- Core: Windows console editing will no longer move the cursor to the next line or scroll the screen when the length of the text being edited exceeds the screen width. +- `autofarm`: farms that span multiple biomes are now handled correctly: crops that can grow in any of the farm's biomes can be assigned to the farm, instead of only using the biome of the farm's center tile +- `labormanager`: the modern engine's labor-mismatch probe no longer enables unmanaged labors on idle units or specialists, and no longer re-probes postings that can never be claimed (#6028) +- ``RemoteFortressReader``: ensured creature and caste names and caste descriptions in ``GetCreatureRaws``/``GetPartialCreatureRaws`` are transmitted in UTF-8 instead of CP437 +- ``Units::teleport`` don't teleport units (except ghosts) into walls or impassable parts of buildings; don't teleport into or out of hidden tiles. +- ``Units::teleport`` teleport babies along with their mothers, teleport merchant wagon-riders with their wagons. don't break links between wagons & their riders. +- ``Units::teleport`` when using teleport to separate a rider/carried unit from its mount/carrying unit, clean up the links between the two. +- ``Units::teleport`` when using teleport to separate a dragged unit from a dragger unit, clean up the links between the two. + +## Misc Improvements +- ``eventful``: ``onJobCompleted`` now fires for jobs that are deleted the same tick they complete (e.g. ``HarvestPlants``), detected via the worker's Job unit action timer +- ``RemoteFortressReader``: ensured creature and caste names and caste descriptions in ``GetCreatureRaws``/``GetPartialCreatureRaws`` are transmitted in UTF-8 instead of CP437 +- ``Units::teleport`` can now remove units from cages & chains, and will cancel any associated uncage/unchain jobs. +- ``Units::teleport`` when teleporting a merchant dragging a pack animal, also teleport the animal. + +## Documentation + +## API + +## Lua +- ``dfhack.timeout_until``: new function to schedule a callback to run on a specified in-game tick + +## Removed + +# 53.16-r3 + +## New Tools + +## New Features + +## Fixes +- Fixed ``scriptmanager.getModStatePath()`` causing scripts to crash when called +- Core: Windows console will no longer hang on close (on some builds of Windows) +- ``dwarfmode.lua``: properly handle adventure arena look cursor +- `prospect`: don't warn about a missing magma sea in the pre-embark estimate when the world was generated without one +- ``Units::getProfessionName``, ``Units::getCasteProfessionName``: now detect arena adventurer for default race name +- ``World::getAdventurer``: properly detect adventurer in arena mode + +## Misc Improvements +- crash reports now include a Lua stack traceback when the game crashes while Lua code is executing (``crashlog/crash_*.txt`` on Linux, ``stderr.log`` on Windows) +- `overlay`: errors raised by a bad C function result count or corrupted Lua call stack are now reported as Lua errors instead of crashing the game + +## Documentation +- ``dfhack.world.isAdventureMode``: doesn't include adventurer arena, advise checking gamemode +- ``export-world-map``: added more format conversion examples. + +## API + +## Lua +- ``printall_recurse``: no longer recurses into untagged unions, which have no way to identify their live member; fixes crashes when printing structures that contain them, such as ``df.army_controller`` + +## Removed + +# 53.16-r2 + +## New Tools +- `export-world-map`: Export world map data for GIS and other external tools. +- `smooth-movement`: smoothly animate creature, item, and vehicle movement in the fortress viewport +- `stockflow`: (reinstated) queue manager jobs based on free space in stockpiles + +## New Features +- `autodump`: new ``undestroy`` option reverts pending item destruction while the game is still paused +- `buildingplan`: new "Do now" option marks the jobs of subsequently placed buildings as high priority +- `getplants`: new ``--dry-run`` option counts affected plants without designating, and new ``--brewable``/``--edible``/``--oil``/``--cloth``/``--dye`` trait filters restrict the selection by plant use +- `labormanager`: the `autolabor` plugin now provides a second command that manages labors through the work detail system instead of writing the labor matrix; use `` mode legacy|modern`` or the work details overlay to switch +- `labormanager`: track job posting age and show a "task starvation" notification in the DFHack warning panel when any posting sits unclaimed for over a day +- `sort`: the stress/happiness icons in the top bar are now clickable, opening a list of the citizens in that stress category; selecting a citizen centers the map on them +- `stocks`: the overlay's ``collapse all`` hotkey now toggles, expanding all categories again when everything is collapsed +- `stockpiles`: the overlay now also attaches to the minecart hauling stop conditions panel, offering import/export of route stop conditions +- `suspendmanager`: suspend deconstruction jobs that would collapse connected machinery, such as waterwheels supporting other components + +## Fixes +- Fixed broken weather lookup in ``World::ReadCurrentWeather`` +- Fixed ``dfhack.items.createItem`` making glob items of an incorrect size +- Fixed a possible hang or assertion failure when pressing a hotkey while a DFHack GUI window was open but unfocused +- Fixed persistent site data API (``dfhack.persistent.saveSiteData``/``getSiteData``) failing on newly reclaimed fortresses until the first save +- Fixed out-of-bounds color table access in ``Screen`` module when Lua pens use ``COLOR_RESET`` +- Plugin commands that throw uncaught C++ exceptions now report an error instead of crashing the game +- ``Units::isCitizen`` and ``Units::isResident`` now exclude dead units even when ``include_insane`` is set; the flag previously skipped the sanity check that also happened to exclude the dead +- ``Units::setPathGoal``: no longer crashes when passed NULL as a unit. Also fixes ``dfhack.units.setPathGoal()``. +- ``Units::teleport``: update unit occupancy on the full 3x3 footprint of EQUIPMENT units (e.g. wagons) instead of only their center tile +- ``Units::teleport``: only clear tile unit occupancy flags when no other unit of the same kind remains on the tile +- `3dveins`: fix crash when a tile's biome cannot be resolved +- `3dveins`: fix failure on embarks that cross midmap tiles +- `aquifer`: make ``--help``/``-h`` actually print help instead of running a map-wide ``list`` +- `aquifer`: make ``--skip-top`` and top-relative ``--levels`` take effect for the ``drain``, ``convert``, and ``add`` actions instead of being silently ignored +- `autochop`: fix crash when a burrow-based command names a burrow that doesn't exist +- `autodump`: ``destroy`` no longer leaves the contents of destroyed containers in limbo or crashes when destroying unit-held items +- `buildingplan`: fix roller material estimate asking for one chain per tile instead of one chain total +- `buildingplan`: fix "Unlink all" only unlinking some mechanisms (or crashing) when freeing mechanisms from the building +- `burrow`: fix ``tiles clear`` and other tile operations silently failing because burrow block z-coordinates were stored incorrectly in the ``Burrows`` module +- `dfhack-run`: on Windows, no longer mutates, hides, and closes the console it inherits when run from an existing terminal (issue #3827) +- `dig-now`: update light/outside flags of tiles below tiles that are dug into solid rather than open tiles +- `export-world-map`: fix crash exporting site data for a site whose owner has an undefined race +- `gui/quickfort`: show smoothing designations immediately after applying a blueprint +- `liquids`: allow painting liquids and obsidian into unallocated sky blocks +- `liquids`: update light/outside flags of covered tiles when painting obsidian or river sources +- `overlay`: overlay hotkeys no longer steal typed characters while the game is capturing text input, e.g. when renaming a building +- `preserve-rooms`: no longer assigns zones to dead units who still hold a noble position +- `probe`: fix malformed output of local/global map feature names +- `sort`: the clickable stress icon citizen lists no longer include dead or missing units (issue #5975) +- `sort`: fix the Places/Workshops and task searches missing workshops by their current display names (e.g. "Stoneworker's Workshop", "Screw Press") +- `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable +- `strangemood`: updated to match behavior of current DF version +- `tiletypes`: remove plants (including trees) rooted on a tile that is painted into something that cannot host them, so cleared ground no longer regrows floating trees +- `timestream`: deal properly with units who have breathing difficulties +- `timestream`: pass the validated numeric value to the fps setter so ``set fps`` with unusual numeric strings (e.g. ``60.9``) can't be silently misapplied +- `strangemood`: fix crash when ``--id`` is given a non-numeric unit id +- `suspendmanager`: fix out-of-bounds parameter read when ``set`` is given too few arguments + +## Misc Improvements +- Added ``Coord2d`` and ``Coord3d`` C++ templates, providing a standard set operations for 2-tuples and 3-tuples of any numeric type +- Renamed the ``stockpiles`` tool tag to ``stockpile`` so it no longer collides with the `stockpiles` plugin in `gui/launcher` +- Writes to ``stderr.log`` are no longer buffered on Windows, so log output is not lost if DF crashes +- ``quickfix``: repair ``plotinfo.site_id`` on load when DF leaves it unassigned on reclaimed fortresses + +## Documentation +- Document ``autodump-destroy-here``. Remove mention of ``autodump destroy`` able to revert destruction. + +## API +- Added ``Maps::addRegionBiomeOffset`` to shift world region coordinate by region_details biome reference +- Added ``Maps::describeSurroundings`` to get surroundings classification from savagery and evilness +- Added ``MapExtras::MapCache::propagateVerticalFlags`` to update the light/outside/subterranean flags of tiles below a tile that changed between an open and a solid shape +- Added ``Maps::getCurrentWeather`` to determine the current weather on the map. +- Added ``Maps::getSiteTypeName`` to print the sites classification as a string (e.g. "mountain halls") +- Added ``Maps::getTileBlockCoord``, ``Maps::getBlockOrigin``, and ``Maps::getTileBlockOffset`` to convert between tile and block coordinates +- Added ``Maps::forEachTile`` with ``Maps::TileFilter``/``Maps::TileActions`` to scan a cuboid of tiles, matching tiles by tiletype, tiletype attributes, and designation/occupancy bits, and counting, rewriting tiletypes, editing designation/occupancy bits, spawning constructions, or invoking a callback on matching tiles +- Added ``MiscUtils::print_range`` to print ranges with prefix, separator, and suffix +- Added ``Units::breathes`` and ``Units::getBreathingState`` +- Deprecated ``World::ReadCurrentWeather`` in favor of ``Maps::getCurrentWeather`` + +## Lua +- Added ``dfhack.maps.forEachTile`` to scan a cuboid of tiles with a declarative filter and apply actions (count, set tiletype or per-tiletype replacements, set designation/occupancy fields, spawn constructions, Lua callback) in a single native call +- Added ``dfhack.maps.getTileBlockCoord``, ``dfhack.maps.getBlockOrigin``, and ``dfhack.maps.getTileBlockOffset`` to convert between tile and block coordinates +- ``widgets.Slider`` and ``widgets.RangeSlider``: fix sliders allowing values outside their number of stops (``num_stops``) when clicked or dragged with the mouse +- Fixed ``ipairs()`` on enum attribute tables (e.g. ``df.item_type.attrs``) never terminating (issue #1860) + +## Removed + +# 53.16-r1.1 + +## New Tools + +## New Features + +## Fixes +- `stockpiles` will no longer incorrectly size the "rough gem" and "cut gem" vectors, which avoids a crash when viewing stockpile settings. This potentially affects anything that uses blueprints to create a stockpile, including `gui/quantum` + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.16-r1 + +## New Tools + +## New Features + +## Fixes +- ``TextArea`` widget corrected to use ``COLOR_BLACK`` instead of ``COLOR_RESET`` as default background color + +## Misc Improvements +- a safety check was added to ``Screen::doSetTile_char`` for out of bound pen color values + +## Documentation + +## API + +## Lua + +## Removed + +# 53.15-r3 + +## New Tools + +## New Features + +## Fixes +- `buildingplan`: restore planner UI elements: hollow constructions, only engraved slabs, only empty cages, weapon count + +## Misc Improvements +- `EventManager`: add safety check to potentially avoid a DFHack crash when DF's ``reports`` table is out of order + +## Documentation +- ``dfhack.translation.generateName``: added a table containing explanations and data types for each of the parameters + +## API +- ``DFSDL``: added ``obtain_library_handle`` and ``obtain_image_library_handle`` so that a plugin can obtain DFHack's already-open handle to these libs instead of having to do it itself. +- ``Screen``: new functions ``paintMapPortTile`` and ``readMapPortTile`` to write and read world and region map tiles. +- ``Materials``: ``MaterialInfo`` constants ``NUM_BUILTIN``, ``GROUP_SIZE``, ``CREATURE_BASE``, ``FIGURE_BASE``, ``PLANT_BASE``, and ``END_BASE`` removed. New plugins should use appropriate members of the ``df::builtin_mats`` enum. + +## Lua +- Added ``Screen::paintMapPortTile`` as ``dfhack.screen.paintMapPortTile`` +- Added ``Screen::readMapPortTile`` as ``dfhack.screen.readMapPortTile`` +- Deprecated ``gui.materials.CREATURE_BASE`` and ``gui.materials.PLANT_BASE`` - scripts should instead use ``df.builtin_mats.CREATURE_1`` and ``df.builtin_mats.PLANT_1``, respectively. + +## Removed + +# 53.15-r2 + +## New Tools + +## New Features +- `stocks`: add button/hotkey for expanding all categories on stocks interface +- `buildingplan`: add a ``Pull`` button next to linked levers on a building's "Show linked buildings" tab so you can queue a high-priority pull-lever job (or cancel a queued one) without navigating to the lever + +## Fixes +- `autoclothing`: correct defect in validating material specification on command line +- `autolabor`: Fix running 1 tick less frequently than intended. +- `buildingplan`: fixed non-clickable pressure plates's triggers (issue #5736) +- `getplants`: added protective code to avoid misoperation when a plant has an invalid material (which should never happen, but...) + +## Misc Improvements +- `buildingplan`: added a slider on the weapontrap overlay +- `buildingplan`: added a small tooltip text about renaming favorites in the UI +- `buildingplan`: buildingplan can now generate work orders +- `orders`: exported orders now include a human-readable ``name`` field + +## Documentation + +## API +- Core: added ``getConfigPath()`` API for obtaining the path to user-specific configuration files +- ``widgets.RadioButton``: New button widget resembling those used in ``gui/control-panel`` + +## Lua +- Added ``dfhack.getConfigPath()`` API, proxying ``Core::getConfigPath`` + +## Removed + +# 53.15-r1 + +## New Tools + +## New Features + +## Fixes +- `autoclothing`: will no longer count gloves and pants as if they were helms +- `timestream`: do not skip ticks when a caravan is loading or unloading, and be more careful about skipping ticks when flows are active + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.14-r2 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- Core: attempts to delete a pool-allocated DF object will now throw an exception instead of corrupting the heap + +## Documentation + +## API + +## Lua + +## Removed +- `logcleaner`: Removed (cannot be safely implemented at this time) + +# 53.14-r1 + +## New Tools + +## New Features +- Compatibility with Dwarf Fortress 53.14 + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.13-r2 + +## New Tools + +## New Features + +## Fixes +- ``Gui::makeAnnoucement``, ``Gui::showPopupAnnouncement`, and ``Gui::autoDFAnnouncement`` will no longer attempt to cull the DF announcement vector + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.13-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation +- updated documentation for ``autofarm`` for more clarity + +## API +- add flexible casting to ``enum_field`` to enable explicit casting to more types +- Handle units without current soul in ``Units::getFocusPenalty`` + +## Lua + +## Removed + +# 53.12-r1 + +## New Tools + +## New Features +- Compatibility with Dwarf Fortress 53.12 + +## Fixes +- Stockpile definitions in the default library will be correctly found and used (fixed missing path separator) + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r3 + +## New Tools + ## New Features ## Fixes +- Core: Windows console will always use UTF-8 regardless of system code page settings +- Steam launcher: Switch to injection strategy, allowing Dwarf Fortress and DFHack to be installed in disparate locations ## Misc Improvements +- Make DFHack relocatable so that it doesn't depend on being fully co-installed with Dwarf Fortress ## Documentation @@ -68,6 +437,664 @@ Template for new versions: ## Removed +# 53.11-r2 + +## New Tools + +## New Features + +## Fixes +- `autoclothing`, `autoslab`, `tailor`: orders will no longer be created with a repetition frequency of ``NONE`` + +## Misc Improvements +- General: DFHack will unconditionally use UTF-8 for the console on Windows, now that DF forces the process effective system code page to 65001 during startup + +## Documentation + +## API + +## Lua + +## Removed + +# 53.11-r1 + +## New Tools + +## New Features + +## Fixes +- `sort`: correct misspelling of ``PERSEVERENCE``; fixes "hates combat" filter in squad selection screen + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.10-r2 + +## New Tools +- ``logcleaner``: New plugin for time-triggered clearing of combat, sparring, and hunting reports with configurable filtering and overlay UI. + +## New Features +- `orders`: added search overlay to find and navigate to matching manager orders with arrow indicators +- `sort`: added ``Uniformed`` filter to squad assignment screen to filter dwarves with mining, woodcutting, or hunting labors +- `sort`: Add death cause button to dead/missing tab in the creatures screen + +## Fixes + +## Misc Improvements +- Core: DFHack now validates vtable pointers in objects read from memory and will throw an exception instead of crashing when an invalid vtable pointer is encountered. This makes it easier to identify which DF data structure contains corrupted data when this manifests in the form of a bad vtable pointer, and shifts blame for such crashes from DFHack to DF. + +## Documentation + +## API +- Added ``Items::pickGrowthPrint``: given a plant material and a growth index, returns the print variant corresponding to the current in-game time. +- Added ``Items::useStandardMaterial``: given an item type, returns true if the item is made of a specific material and false if it has a race and caste instead. +- Added ``Maps::addItemSpatter``: add a spatter of the specified item + material + growth print to the indicated tile, returning whatever amount wouldn't fit in the tile. +- Added ``Maps::addMaterialSpatter``: add a spatter of the specified material + state to the indicated tile, returning whatever amount wouldn't fit in the tile. + +## Lua +- Added ``Maps::addItemSpatter`` as ``dfhack.maps.addItemSpatter``. +- Added ``Maps::addMaterialSpatter`` as ``dfhack.maps.addMaterialSpatter``. + +## Removed + +# 53.10-r1 + +## New Tools + +## New Features + +## Fixes +- `autochop`: the report will no longer throw a C++ exception when burrows are defined. +- `suspendmanager`: Fix the overlay appearing where it should not when following a unit + +## Misc Improvements + +## Documentation + +## API +- Added ``Burrows::getName``: obtains the name of a burrow, or the same placeholder name that DF would show if the burrow is unnamed. + +## Lua +- Added ``Burrows::getName`` as ``dfhack.burrows.getName``. + +## Removed + +# 53.09-r1 + +## New Tools + +## New Features +- `tweak`: ``drawbridge-tiles``: Make it so raised bridges render with different tiles in ASCII mode to make it more obvious that they ARE raised (and to indicate their direction) + +## Fixes +- ``Filesystem::as_string`` now always uses UTF-8 encoding rather than using the system locale encoding + +## Misc Improvements + +## Documentation + +## API +- ``dfhack.job.getManagerOrderName``: New function to get the display name of a manager order + +## Lua + +## Removed + +# 53.08-r1 + +## New Tools + +## New Features +- compatibility with DF 53.08 + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.07-r1 + +## New Tools +- ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. +- `infinite-sky`: Re-enabled with compatibility with new siege map data. + +## New Features +- `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status + +## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit +- `sort`: Places search widget moved to account for DF's new "Siege engines" subtab + +## Misc Improvements +- `createitem`: created items can now be placed onto/into tables, nests, bookcases, display cases, and altars +- The ``fpause`` console command can now be used to force world generation to pause (as it did prior to version 50). +- `keybinding`: keybinds may now include the super key, and are no longer limited to particular keys ranges of keys, allowing any recognized by SDL. + +## Documentation + +## API +- ``Hotkey``: New module for hotkey functionality + +## Lua +- The ``Lua interactive interpreter`` banner now documents keywords such as ``unit`` and ``item`` which reference the currently-selected object in the DF UI. +- ``dfhack.hotkey.addKeybind``: Creates new keybindings +- ``dfhack.hotkey.removeKeybind``: Removes existing keybindings +- ``dfhack.hotkey.listActiveKeybinds``: Lists all keybinds for the current context +- ``dfhack.hotkey.listAllKeybinds``: Lists all keybinds for all contexts +- ``dfhack.hotkey.requestKeybindingInput``: Requests the next keybind-compatible input is saved +- ``dfhack.hotkey.getKeybindingInput``: Reads the input saved in response to a request. + +## Removed + +# 53.06-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed +- `infiniteSky`: Temporarily disabled due to incompatibility with changes made as part of DF's siege update + +# 53.05-r1 + +## New Tools + +## New Features +- compatibility with 53.05 + +## Fixes +- `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.04-r1.1 + +## New Tools + +## New Features + +## Fixes +- fixed misalignment in ``widgets::unit_list`` + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 53.04-r1 + +## New Tools + +## New Features + +## Fixes +- `buildingplan`: Bolt throwers will no longer be constructed using populated bins. +- `RemoteFortressReader`: updated siege engine facing enums for new diagonal directions +- `suspendmanager`: treat reinforced walls as a blocking construction and buildable platform + +## Misc Improvements +- `autolabor`: support for new dying and siege-related labors +- `blueprint`: support for reinforced walls and bolt throwers + +## Documentation + +## API + +## Lua + +## Removed + +# 53.03-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- Release builds for Linux are now compiled with gcc 11 + +## Documentation + +## API + +## Lua + +## Removed + +# 53.02-r2 + +## New Tools + +## New Features + +## Fixes +- `buildingplan`: Building costs for reinforced walls are now correct. +- `cleanconst`: do not attempt to clean Reinforced constructions + +## Misc Improvements +- `buildingplan`: Added support for bolt throwers and siege engine rotation. + +## Documentation + +## API + +## Lua + +## Removed + +# 53.02-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- Core: added ``gps`` (``graphicst``) to the set of globals whose sizes must agree for DFHack to pass initialization checks + +## Documentation + +## API + +## Lua + +## Removed + +# 53.01-r1 + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements +- `stockpiles`: add support for managing the dyed, undyed, and color filter settings. + +## Documentation + +## API + +## Lua + +## Removed + +# 52.05-r2 + +## New Tools + +## New Features + +## Fixes +- `script-manager`: the ``scripts_modactive`` and ``scripts_modinstalled`` folders of a script-enabled mod will be properly added to the script path search list + +## Misc Improvements + +## Documentation +- added a clarification link to DF's Lua API documentation to the DFHack Lua API documentation, as a way to reduce end-user confusion + +## API + +## Lua + +## Removed + +# 52.05-r1 + +## New Tools + +## New Features + +## Fixes +- improved file system handling: gracefully handle errors from operations, preventing crashes. +- `zone`: animal assignment dialog now tolerates corrupt animal-to-pasture links. + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.04-r1 + +## New Tools + +## New Features +- Compatibility with DF 52.04 + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r2 + +## New Tools + +## New Features +- `nestboxes`: allow limiting egg protection to nestboxes inside a designated burrow +- `tailor`: tailor now provides optional dye automation + +## Fixes +- ``Units::getReadableName`` will no longer append a comma to the names of histfigs with no profession +- `stockpiles`: fixed off-by-one error in exporting furniture stockpiles + +## Misc Improvements + +## Documentation + +## API +- ``Job``: new functions ``createLinked`` and ``assignToWorkshop`` +- ``Units``: new functions ``getFocusPenalty``, ``unbailableSocialActivity``, ``isJobAvailable`` + +## Lua +- New functions: ``dfhack.jobs.createLinked``, ``dfhack.jobs.assignToWorkshop``, ``dfhack.units.getFocusPenalty``, ``dfhack.units.unbailableSocialActivity``, and ``dfhack.units.isJobAvailable`` + +## Removed + +# 52.03-r1.1 + +## New Tools + +## New Features + +## Fixes +- job descriptions of mix dye job will display proper dye names + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.03-r1 + +## New Tools + +## New Features + +## Fixes +- `preserve-rooms` will no longer hang on startup in the presence of a cycle in the replacement relationship of noble positions + +## Misc Improvements + +## Documentation + +## API +- Adjusted the logic inside ``Military::removeFromSquad`` to more closely match the game's own behavior + +## Lua + +## Removed + +# 52.02-r2 + +## New Tools + +## New Features + +## Fixes +- Several fixes related to changes in file system handling in DF 52.01 +- `dig-now`: don't allow UNDIGGABLE stones to be excavated + +## Misc Improvements +- `autoclothing`: added a ``clear`` option to unset previously set orders + +## Documentation + +## API +- Added GUI focus strings for new_arena: ``/Loading`` and ``/Mods`` +- ``Filesystem::getBaseDir`` and ``Filesystem::getInstallDir`` added (and made available in Lua) +- Expanded the partial implementations of ``Military::addToSquad`` and ``Military::removeFromSquad`` + +## Lua +- Inserting values into STL containers containing nonprimitive types is now supported + +## Removed + +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- Honor the "portable mode" preference setting for locating save folders. Fixes DFHack cosaves not working in most cases. + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + +# 52.01-r1 + +## New Tools + +## New Features +- `tweak`: ``animaltrap-reuse``: make it so built animal traps automatically unload the vermin they catch into stockpiled animal traps, so that they can be automatically re-baited and reused + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua +- ``widgets.Slider``: new mouse-controlled single-headed slider widget + +## Removed + +# 51.13-r1 + +## New Features + +- Compatibility with DF 51.13 + +# 51.12-r1.1 + +## New Features + +- Compatibility with Itch release of DF 51.12 + +# 51.12-r1 + +## Fixes +- `getplants`: will no longer crash when faced with plants with growths that do not drop seeds when processed +- `getplants`: use updated formula for calculating whether plant growths are ripe +- `getplants`: fix logic for determining whether plant growths have been picked +- `gui/teleport`: adapt to new behavior in DF 51.11 to avoid a crash when teleporting items into mid-air +- `script-manager`: fix lua scripts in mods not being reloaded properly upon entering a saved world on Windows +- `preserve-rooms`: don't warn when a room is assigned to a non-existent unit. this is now common behavior for DF when it keeps a room for an unloaded unit +- fixed an overly restrictive type constraint that resulted in some object types being glossed as a boolean when passed as an argument from C++ to Lua +- `plants`: will no longer generate a traceback when a filter is used +- `createitem`: multiple items should be properly created in stacks again + +## Misc Improvements +- All places where units are listed in DFHack tools now show the translated English name in addition to the native name. In particular, this makes units searchable by English name in `gui/sitemap`. +- `dig`: ASCII overlay now displays priority of digging designations +- `spectate`: added prefer nicknamed units +- `blueprint`: support for recording zones +- `blueprint`: support for recording stockpile properties like names and stockpile links; does not yet support recording detailed contents configuration +- `strangemood`: support added for specifying unit id instead of selected unit or random one. + +## API +- ``Random`` module: added ``SplitmixRNG`` class, implements the Splitmix64 RNG used by Dwarf Fortress for "simple" randomness +- ``Items::getDescription``: fixed display of quality levels, now displays ALL item designations (in correct order) and obeys vanilla SHOW_IMP_QUALITY setting +- ``cuboid::forCoord``, ``Maps::forCoord``: take additional parameter to control whether iteration goes in column major or row major order + +## Lua +- ``script-manager``: new ``get_active_mods()`` function for getting information on active mods +- ``script-manager``: new ``get_mod_info_metadata()`` function for getting information out of mod ``info.txt`` files + +## Removed +- removed historically unused ``Core::RegisterData``/``Core::GetData`` API and associated internal data structures + +# 51.11-r1.2 + +## Fixes +- `preserve-tombs`: will no longer crash when a tomb is assigned to a unit that does not exist + +# 51.11-r1.1 + +## Fixes +- `preserve-rooms`: will no longer crash when a civzone is assigned to a unit that does not exist +- `gui/design`: fix misaligned shape icons + +# 51.11-r1 + +## Fixes +- `spectate`: don't show a hover tooltip for hidden units (e.g. invisible snatchers) +- `stockpiles`: fix one-off error in item type when importing furniture stockpile settings +- `dig-now`: fix cases where boulders/rough gems of incorrect material were being generated when digging through walls +- `dig-now`: properly generate ice boulders when digging through ice walls +- `gui/teleport`: now properly handles teleporting units that are currently falling or being flung +- `unload`: fix recent regression where `unload` would immediately `reload` the target +- ``Buildings`` module: do not crash if a ``map_block`` unexpectedly contains an item that is not on the master item vector +- `suspendmanager`: fix walls being treated as potential suitable access if another wall is built underneath +- text widgets no longer lose their cursor when the Ctrl-a (select all) hotkey is pressed when there is no text to select + +## Misc Improvements +- `spectate`: show dwarves' activities (like prayer) + +## API +- ``Military`` module: added ``addToSquad`` function +- ``Units`` module: added ``get_cached_unit_by_global_id`` to emulate how DF handles unit vector index caching (used in civzones and in general references) +- ``Buildings`` module: add ``getOwner`` (using the ``Units::get_cached_unit_by_global_id`` mechanic) to reflect changes in 51.11 +- ``Units::teleport``: projectile information is now cleared for teleported units +- ``Buildings::setOwner``: updated for changes in 51.11 + +## Lua +- ``dfhack.military.addToSquad``: expose Military API function +- ``dfhack.buildings.getOwner``: make new Buildings API available to Lua + +# 51.10-r1 + +## Misc Improvements +- Compatibility with DF 51.10 + +# 51.09-r1 + +## New Features +- `gui/journal`: Ctrl-j hotkey to launch `gui/journal` now works in adventure mode! + +## Fixes +- Fix processing error in the overlay that displays unit preferences in the baron selection list + +## API +- ``Filesystem`` module: rewritten to use C++ standard library components, for better portability + +# 51.08-r1 + +## Misc Improvements +- Compatibility update for DF 51.08 + +# 51.07-r1 + +## New Features +- `spectate`: can now specify number of seconds (in real time) before switching to follow a new unit +- `spectate`: new "cinematic-action" mode that dynamically speeds up perspective switches based on intensity of conflict +- `spectate`: new global keybinding for toggling spectate mode: Ctrl-Shift-S +- `spectate`: new overlay panel that allows you to cycle through following next/previous units (regardless of whether spectate mode is enabled) +- `gui/sitemap`: is now the official "go to" tool. new global hotkey for fort and adventure mode: Ctrl-G + +## Fixes +- Windows console: fix possible hang if the console returns a too-small window width (for any reason) +- `createitem`: produced items will now end up at the look cursor position (if it is active) +- `spectate`: don't allow temporarily modified announcement settings to be written to disk when "auto-unpause" mode is enabled +- `changevein`: fix a crash that could occur when attempting to change a vein into itself +- `overlay`: reset draw context between rendering widgets so context changes can't propagate from widget to widget +- `suspendmanager`: in ASCII mode, building planning mode overlay now only displays when viewing the default map, reducing issues with showing through the UI + +## Misc Improvements +- `spectate`: player-set configuration is now stored globally instead of per-fort +- `autobutcher`: treat animals on restraints as unavailable for slaughter +- `stockpiles`: add property filters for brewable, millable, and processable (e.g. at a Farmer's workshop) organic materials +- `quickfort`: redesigned ``library/aquifer_tap.cav`` to improve the water fill rate + +## Documentation +- `stonesense-art-guide`: guide for making sprite art for Stonesense + +## API +- ``Military::removeFromSquad``: removes unit from any squad assignments +- ``Buildings::checkFreeTiles``: now takes a building instead of a pointer to the building extents +- ``Units::isUnitInBox``, ``Units::getUnitsInBox``: don't include inactive units +- ``Items::getItemBaseValue``: adjust to the reduced value of prepared meals (changed in DF 51.06) +- ``Items::getValue``: magical powers now correctly contribute to item value + +## Lua +- ``dfhack.units.setAutomaticProfessions``: sets unit labors according to current work detail settings +- ``dfhack.military.removeFromSquad``: Lua API for ``Military::removeFromSquad`` +- ``gui.dwarfmode``: adventure mode cursor now supported in ``getCursorPos``, ``setCursorPos``, and ``clearCursorPos`` funcitons +- ``dfhack.buildings.checkFreeTiles``: now takes a building pointer instead of an extents parameter +- ``overlay.isOverlayEnabled``: new API for querying whether a given overlay is enabled +- ``overlay``: widgets can now declare ``overlay_onenable`` and ``overlay_ondisable`` functions to hook enable/disable + +## Removed +- `orders`: MakeCheese job removed from library/basic orders set. Please use `autocheese` instead! + # 51.06-r1 ## Misc Improvements diff --git a/docs/dev/Binpatches.rst b/docs/dev/Binpatches.rst index a566576168..d59b1f8482 100644 --- a/docs/dev/Binpatches.rst +++ b/docs/dev/Binpatches.rst @@ -50,10 +50,20 @@ directly in memory at runtime:: If the name of the patch has no extension or directory separators, the script uses :file:`hack/patches//.dif`, thus auto-selecting -the version appropriate for the currently loaded executable. +the version appropriate for the currently loaded executable. The ``df-version`` +is the version string in the loaded symbol table. For example, if you want to +make a patch for all distributed verisons of DF 51.05, you'd provide a ``dif`` +file in each of the following directories: + +- :file:`hack/patches/v0.51.05 linux64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 linux64 STEAM/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 CLASSIC/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 ITCH/mypatch.dif` +- :file:`hack/patches/v0.51.05 win64 STEAM/mypatch.dif` This is the preferred method; it's easier to debug, does not cause persistent -problems, and leaves file checksums alone. As with many other commands, users +problems, and leaves file checksums alone. As with many other commands, users can simply add it to `dfhack.init` to reapply the patch every time DF is run. diff --git a/docs/dev/Contributing.rst b/docs/dev/Contributing.rst index 7b34dc20ce..e9600bbce9 100644 --- a/docs/dev/Contributing.rst +++ b/docs/dev/Contributing.rst @@ -60,6 +60,24 @@ Code format * Braces placed at original indent level if on their own lines * ``#include`` directives should be sorted: C++ libraries first, then DFHack modules, then ``df/`` headers, then local includes. Within each category they should be sorted alphabetically. + * Avoid naming a type with a name that ends in ``st`` unless the structure is being named intentionally the same as a structure in Dwarf Fortress. If you are defining a new structure, avoid using a name that ends in ``st`` to avoid any risk of confusion. + +* For Lua: + + * Identifiers for publicly visible functions and variables should be in ``camelCase``. + +General C++ code guidelines +--------------------------- +* This project is currently built at the C++20 feature level, and C++20 features should be used when appropriate. C++23 features will be allowed once all of our build platforms support them. +* NEVER use ``using namespace`` in a header file. In source files, do not use ``using namespace std``; instead, import each STL identifier you need specifically (e.g. ``using std::string;``). +* Avoid platform specific code as much as possible. +* Avoid including ``Windows.h``; if you must, ensure that ``NOMINMAX`` and ``WIN32_LEAN_AND_MEAN`` are defined before including it. +* Do not include C headers (e.g. ````); use the C++ versions (e.g. ````) instead. +* Do not use ``std::string`` (or ``char *``) for path names; always use ``std::filesystem::path``. This avoids issues with encoding, especially on the Windows platform, which is roughly 80% of our user base. +* Do not use ``printf`` or similar functions for formatting strings; use C++ streams or ``fmt::format`` instead. We use the `fmt library `__ for formatting strings; this dependency is automatically fetched by our build system. +* Avoid out parameters; prefer returning a struct, pair, or tuple, or using ``std::optional`` instead. +* Prefer range for loops to traditional for loops when iterating over a container. +* Avoid macros when possible; prefer ``constexpr`` variables for constants and functions or templates for code generation. .. _contributing-pr-guidelines: @@ -107,6 +125,37 @@ Pull request guidelines or add "WIP" to the title. Otherwise, your pull request may be reviewed and/or merged prematurely. +* Avoid using force pushes to your pull request branch after it has been reviewed, + as this can make it difficult for reviewers to see what has changed since their + last review. If you need to make changes, consider creating a new commit instead + of amending or rebasing. We neither enforce nor recommend a "single commit" rule; if you do + choose to squash your commits, please ensure that the commit message is clear and descriptive of the changes made. + If your pull request has an unusually large number of commits, a maintainer may + request that you squash your commits into a smaller number of commits before merging. + +* All pull requests must be accompanied by a description of the changes made, and + any relevant information for reviewers. If your pull request addresses an + issue, please include a reference to that issue in the description (e.g. + "Fixes #1234"). If your pull request is related to another pull request, please + include a reference to that pull request in the description (e.g. "Related to + #1234"). + +* All pull requests which have user facing changes, including all new features, bug fixes, or + changes to existing functionality, must include an entry in the "Future" section of + the changelog for the relevant repository. If your pull request is merged, this entry + will be added to the appropriate changelog. These entries are used when preparing the release + notes for each release, so please be sure to include a clear and concise description + of the changes made. See `build-changelog` for more information on the changelog format. + Changes that do not require a changelog entry are mainly those that are purely internal, + such as refactoring not intended to change semantics, code cleanup, changes to CI implementation + or to documentation, or changes directly related to the release process. When in doubt, + assume a changelog entry will be required. + +* Pull requests that add or modify tools must include a corresponding update to the documentation + for that tool. Similarly, pull requests that add or modify either the C++ or Lua APIs + must include a corresponding update to the appropriate API documentation. + See `docs-standards` for details. + Other ways to help ================== DFHack is a software project, but there's a lot more to it than programming. diff --git a/docs/dev/CoreSuspender.rst b/docs/dev/CoreSuspender.rst new file mode 100644 index 0000000000..385ecd8446 --- /dev/null +++ b/docs/dev/CoreSuspender.rst @@ -0,0 +1,360 @@ +.. _core-suspender: + +=============================== +CoreSuspender and the core lock +=============================== + +This page describes the ``DFHack::CoreSuspender`` mechanism: what it does, how +it works internally, when you must use it, and when you must not. It is aimed +at developers working on the DFHack core library or C++ plugins. Lua-side +wrappers (``dfhack.with_suspend`` etc.) are covered in `lua-api`. + +.. contents:: Contents + :local: + +What it is +---------- + +``CoreSuspender`` is an RAII guard that gives the calling thread *exclusive, +consistent access to DF's memory and to DFHack's shared Lua state*. While a +thread holds a ``CoreSuspender``, the Dwarf Fortress simulation thread is +parked at a well-defined point in its per-frame update and cannot mutate game +state. This is the only sanctioned way for a foreign thread (console input +thread, RPC server thread, plugin-created worker thread, etc.) to read or +write ``df::global::*`` data safely. + +The name is historical and slightly misleading: DFHack no longer suspends the +process's threads with OS facilities. Instead, the simulation thread +cooperatively yields at a synchronization point once per frame, and the lock +serializes who may run while it is yielded. + +Threading model +--------------- + +Three kinds of threads matter: + +**The DF simulation thread** + Runs the game logic. DFHack hooks it via ``dfhooks_update()``, which calls + ``Core::Update()`` once per frame. During ``Core::InitSimulationThread()`` + this thread acquires ``Core::CoreSuspendMutex`` via a ``thread_local`` + ``CoreSuspenderBase`` (``MainThread::suspend()``) and effectively holds it + for the rest of the session. This is what "the core is running" means: the + simulation thread owns the lock while it executes. + +**The DF render thread (a.k.a. "main thread" in the hooks layer)** + Runs SDL event handling and screen drawing. Its id is recorded in + ``Core::df_render_thread`` by ``InitMainThread()``. This thread **can + never acquire the lock** — see `cannot-use` below. Note that it keeps + running while a suspender is held: suspending the core does *not* stop + rendering, it only parks simulation. The current implementation of the DF + render thread will generally never read or write game state — it only renders + the frame that was just computed by the simulation thread, using a double buffer + mechanism, and then pauses until the simulation thread signals that a new + frame is ready — so generally it's safe to access game state (other than certain + fields in ``df::global::enabler`` which are used by the render thread) while it's running. + + The following hooks (exported from ``library/Hooks.cpp``) are invoked on + this thread: + + - ``dfhooks_init()`` — once, before the simulation thread is started and + the main event loop begins. + - ``dfhooks_sdl_event(SDL_Event*)`` — for each SDL event; returning true + consumes the event. + - ``dfhooks_sdl_loop()`` — once per rendered frame, after mouse state is + set in ``gps`` and just before the screen buffer is presented. + - ``dfhooks_ncurses_key(int)`` — for each ncurses input character + (non-SDL builds); returning true consumes the key. (This is a legacy + event from when DF used ncurses, and is not used in SDL builds.) + - ``dfhooks_shutdown()`` — once, after the main event loop exits. + + ``dfhooks_preinit()`` is called even earlier by the chainloader, before + the main thread is initialized. ``dfhooks_update()`` and + ``dfhooks_prerender()`` run on the simulation thread instead. + +**Foreign threads** + The DFHack console/IO thread (``fIOthread``/``fInitthread``), RPC server + connection threads, and any threads spawned by plugins or tools. These are + the threads ``CoreSuspender`` exists for. + +How it works +------------ + +The state lives on ``Core`` (see ``library/include/Core.h``): + +- ``CoreSuspendMutex`` — a ``std::recursive_timed_mutex``. The simulation + thread is the standing owner; suspender threads contend on it. +- ``CoreWakeup`` — a ``std::condition_variable_any`` the simulation thread + sleeps on while tools run. +- ``ownerThread`` — atomic ``std::thread::id`` of whoever currently owns the + suspend context. Powers ``Core::isSuspended()``. Empty when the mutex is + unlocked. +- ``toolCount`` — atomic count of threads that *want* the lock, incremented + **before** blocking on the mutex. This ordering is the crux of the + protocol: a tool that is still waiting on the mutex already counts, so the + simulation thread can never observe "no work pending" while a tool is on + its way in. + +Once per frame, at the end of ``Core::Update()``, the simulation thread runs:: + + CoreWakeup.wait(MainThread::suspend(), + [this] { return toolCount.load() == 0; }); + +``condition_variable_any::wait`` evaluates the predicate *before* releasing +the mutex, so when nothing is queued this is a single atomic load — the yield +has no measurable cost in the common case. If any tool has incremented +``toolCount``, the wait releases ``CoreSuspendMutex`` and parks the thread +until the count drains to zero. + +A ``CoreSuspender`` therefore does: + +- *Construct:* increment ``toolCount``, then ``lock()`` the recursive mutex. + If the simulation thread is mid-frame, this blocks until the wait above + releases the mutex. On successful acquisition, the thread stores itself in + ``ownerThread``, remembering the previous owner. +- *Destruct:* restore the previous ``ownerThread``. If there was no previous + owner (i.e. this was the outermost release), run ``Lua::Core::Reset()``, + which verifies the shared ``lua_State`` stack is empty — every suspension + gets a clean Lua context. Then release the mutex and decrement + ``toolCount``; when it reaches zero, ``CoreWakeup`` is notified and the + simulation thread resumes its frame loop. + +Because the underlying mutex is recursive, nested ``CoreSuspender`` +construction on the same thread is legal and cheap; only the outermost +release performs the owner restore, Lua reset, and wakeup. + +Guarantees and non-guarantees +----------------------------- + +What holding the lock guarantees: + +- The simulation thread is not running game code. ``df::global::world``, the + map, unit lists, viewscreen state, etc. are stable for the duration of the + critical section. +- Only one suspender runs at a time — tools serialize on the mutex. +- Exclusive access to the shared core ``lua_State``. ``Core::getLuaState()`` + asserts ``isSuspended()``, and the ``DFHack::Lua::Core::*`` helpers assume + it. +- All queued tools complete before the next simulation frame begins — the + simulation thread waits for ``toolCount == 0``, not for a specific tool. + +What it does *not* guarantee: + +- The render thread is not suspended. It continues to draw and process SDL + events while you hold the lock. Code running under the lock can still race + with render-thread reads of DF memory (an accepted, deliberate asymmetry — + the render thread never blocks, so those reads are best-effort). +- Timeliness. Acquisition can take up to a full frame; there is no priority + or fairness among waiting tools beyond the mutex's own ordering. +- Safety during teardown. See `caveats`_. + +``Core::isSuspended()`` returns true when the calling thread currently owns +the suspend context. Notably, it also returns true on the simulation thread +during normal execution (the standing owner) — so code like +``plugin_onupdate`` or viewscreen callbacks that run inside ``Core::Update`` +may use the core Lua state directly, without constructing a suspender. + +When the lock is required +------------------------- + +You must hold the core lock when, from any thread that is not already in a +suspended context, you: + +- Read or write ``df::global::*`` objects or anything reachable from them — + game state, map blocks, units, buildings, screen/viewscreen internals. +- Touch the core ``lua_State`` — anything under ``DFHack::Lua::Core``, script + invocation, ``Lua::Core::SafeCall``, etc. +- Perform any operation that would misbehave if the world mutated mid-read + (iterating vectors of DF objects is the canonical example — a frame + boundary can realloc or free them). + +In practice, most entry points already arrange this for you: + +- **Plugin commands** are invoked under a suspender by ``Plugin::invoke`` + unless the command is registered with ``unlocked = true``. +- **RPC methods** are invoked under a suspender unless the method is + registered with ``SF_DONT_SUSPEND``. +- **Lua scripts** run under the lock; ``dfhack.with_suspend(f, ...)`` adds an + explicit critical section if you need one inside script code. +- ``plugin_onupdate``, ``plugin_onstatechange``, EventManager handlers, and + viewscreen ``feed``/``render`` callbacks execute on the simulation thread + inside ``Core::Update`` and are implicitly in a suspended context. A + ``CoreSuspender`` there is redundant but harmless (the mutex is recursive; + ``EventManager::manageEvents`` does exactly this defensively). + +So the lock is *your* responsibility mainly when you create your own thread, +register an ``unlocked`` command or ``SF_DONT_SUSPEND`` RPC method, or write +code reachable from foreign-thread callbacks that touches DF memory. + +.. _cannot-use: + +When the lock cannot be used +---------------------------- + +**On the render thread.** The ``CoreSuspenderBase`` constructor asserts +``df_render_thread != std::this_thread::get_id()``. This includes +``ConditionalCoreSuspender`` — you cannot even *attempt* acquisition there. +Beyond the assert, blocking the render thread on the mutex would stall frame +presentation and can deadlock against the parked simulation thread. This is +why hotkey guards and SDL event handling (which run on the render thread via +``dfhooks_sdl_event``) must inspect only native viewscreen state and must not +enter Lua — see the workaround in ``Gui.cpp``'s ``dfscreen_selection`` and +``Core::isRenderThread()``. To move work the other way, use +``DFHack::runOnRenderThread()``. + +**Before the core is initialized.** The same constructor asserts +``df_render_thread`` is set, which only happens in ``InitMainThread()``. A +suspender cannot be constructed in static initializers or in code that runs +before ``dfhooks_init`` completes. + +**Where blocking until the next frame boundary is unacceptable.** The mutex +is only released at the ``CoreWakeup.wait`` at the end of ``Core::Update``. +During long stretches where DF does not reach that hook (heavy save/load or +world-gen work, a frozen/backgrounded game), acquisition stalls arbitrarily. +If you only want to *opportunistically* take the lock, use +``ConditionalCoreSuspender``. + +**While holding locks that suspended-context code also needs.** Classic +lock-ordering deadlock: the code comment in ``Plugin::unload`` exists because +holding the plugin ``access`` lock while the ``CoreSuspender`` destructs can +deadlock against the simulation thread's bookkeeping. Establish and follow a +lock order; when in doubt, release other locks before acquiring the suspend. + +**While waiting on input or on another thread.** Holding the lock across +``lineedit``-style input waits, thread joins, or blocking IPC freezes the +simulation for the duration — and deadlocks outright if the thing you're +waiting on needs the simulation thread. ``Lua::RunCoreQueryLoop`` shows the +correct shape: it releases the suspend while waiting for input and +re-acquires it for each execution. On the Lua side, ``dfhack.lineedit`` may +yield the coroutine so the C++ side can drop the lock; wrapping such a call in +``dfhack.with_suspend`` forces it to block with the lock held instead. + +``A note about vmethod interposes`` +----------------------------------- + +VMethod interposes normally run in the context of the simulation thread, +or alternatively in the context of another thread that has already acquired +a ``CoreSuspender``, so they are effectively already in a suspended context. +You should therefore not construct a ``CoreSuspender`` in the implementation +of a vmethod interpose, because there is a small chance that a vmethod interpose will +instead be run on one of DF's worker threads; attempting to acquire a lock in that +context is likely to cause a deadlock. + +In the event that a vmethod interpose is called from a DF worker thread, we assume +that Bay12 has already implemented synchronization for the data that the +interposed method would have accessed. Mainly, this means that implementations +for interposed methods should avoid accessing data that is "not related" to the data +the method being interposed would normally access, as such data might not be +protected by synchronization. Reverse engineering may be required to determine +what accesses are safe from interposed methods. At the current time, we do not +know of any interposable methods that are called from DF worker threads, but this +does not mean there aren't any. + +Note also that there are a handful of interposable methods that will be called by +DF from its render (main) thread. Acquiring a lock in these methods will also +likely cause a deadlock. Mainly, these are vmethods of the ``renderer`` class and +its subclasses. Exercise caution when writing interposes for these methods: they +must not attempt to acquire the core suspend lock or directly access DF game +data other than the renderer state itself. + +``ConditionalCoreSuspender`` +---------------------------- + +``ConditionalCoreSuspender`` attempts acquisition and is testable as a bool:: + + DFHack::ConditionalCoreSuspender suspend; + if (suspend) { + // owns the lock + } else { + // lock not acquired; skip or degrade + } + +Despite its doc comment, acquisition is not strictly non-blocking: it uses +``try_lock_for(100ms)``, i.e. it will wait up to ~100 ms for the simulation +thread to reach the yield point. It fails only if the mutex is still +unavailable after that — typically because a very long frame is in progress +or another tool is hogging the lock. Use it for latency-sensitive paths where +a dropped update is better than a stall. + +Note that ``toolCount`` is incremented before the attempt and decremented on +failure, so even a *failed* conditional suspender briefly makes the +simulation thread believe a tool is queued. + +RPC batching +------------ + +``CoreService::CoreSuspend``/``CoreResume`` let a remote client pin the lock +across multiple RPC calls for atomic batch operations: ``CoreSuspend`` +allocates a ``CoreSuspender`` on the connection thread and keeps it alive +until the matching ``CoreResume``. While a client holds it, the game is +frozen — this is a sharp tool; a misbehaving or disconnected client can stall +the session. + +.. _caveats: + +Caveats and pitfalls +-------------------- + +- **Suspension is observable.** Holding the lock stalls the simulation + thread, so game time literally stops: long critical sections manifest as + hitching or a frozen UI. Batch what you need, keep sections short, and + never do I/O or sleeps inside the lock. + +- **One yield point per frame.** ``CoreSuspender`` construction can block for + up to a whole frame even when the game is running smoothly. Releasing and + immediately re-acquiring in a loop can starve the simulation thread into + running one frame per re-acquisition. + +- **Frame-boundary visibility.** The yield happens at the *end* of + ``Core::Update``, after ``doUpdate`` (state-change events, per-frame + plugin/Lua handlers) has run. A tool's critical section therefore observes + a world that is between frames: internally consistent, but already fully + processed for the previous tick. + +- **Lua state is reset on release.** The outermost unlock runs + ``Lua::Core::Reset``, which clears the shared lua_State stack (logging + "Common lua context stack top left at N after suspend" if it wasn't + empty). Do not expect anything you left on the core Lua stack to survive + the end of your critical section. + +- **Shutdown is not synchronized.** When ``Core::Shutdown`` sets + ``shutdown``, the next ``Core::Update`` releases the standing lock and the + simulation thread stops waiting on ``toolCount``. A suspender acquired + during or after teardown still acquires the mutex but the world is going + away; core teardown code (e.g. ``Plugin::unload``) checks + ``Core::isValid()``/``isMapLoaded()`` before entering the lock for this + reason. + +- **The render thread races with you.** If you mutate data that drawing code + reads asynchronously, the render thread may observe intermediate states. + Usually cosmetic, but know that "the game is frozen" is not literally true. + Mainly, this applies to accessing objects reachable from ``df::global::enabler``, + which is the control object for the renderer. + +- **``MemAccess::isSuspended()`` is unrelated.** It's a legacy stub that + always returns true; the real check is ``Core::isSuspended()``. + +- **Stale advice.** The Lua API docs state "every thread is allowed only one + suspend per DF frame" — a leftover from the pre-mutex implementation. Under + the current mechanism repeated acquisition is legal; each one just risks a + frame-length wait. Grouping operations into one critical section is still + good advice for latency, not correctness. + +Reference +--------- + +All of the following are in ``library/include/Core.h``: + +``DFHack::CoreSuspender`` + RAII guard. Constructor blocks until the lock is acquired; destructor + releases. Publicly, construction and destruction are the entire + interface — ``lock``/``unlock``/``try_lock`` are deliberately protected. + +``DFHack::ConditionalCoreSuspender`` + As above, but acquisition is attempted with a 100 ms bound and the + object converts to ``bool`` to report success. + +``Core::isSuspended()`` + True if the calling thread owns the suspend context. + +``Core::isRenderThread()`` + True on the DF render thread, where the lock is forbidden. diff --git a/docs/dev/Documentation.rst b/docs/dev/Documentation.rst index 13eaf42da0..efd2e9885f 100644 --- a/docs/dev/Documentation.rst +++ b/docs/dev/Documentation.rst @@ -67,6 +67,8 @@ with relevant tags. These are used to compile indices and generate cross-links b commands, both in the HTML documents and in-game. See the list of available `tag-list` and think about which categories your new tool belongs in. +.. _docs-links: + Links ----- @@ -161,7 +163,7 @@ And documentation for the ``autodump`` plugin might look like:: .. dfhack-tool:: :summary: Automatically set items in a stockpile to be dumped. - :tags: fort armok fps productivity items stockpiles + :tags: fort armok fps productivity items stockpile :no-command: .. dfhack-command:: autodump @@ -418,6 +420,8 @@ Once you have pip available, you can install Sphinx with the following command:: Note that this may require opening a new (admin) command prompt if you just installed pip from the same command prompt. +.. _docs-build: + Building the documentation ========================== @@ -427,16 +431,18 @@ Sphinx to build the docs: Using CMake ----------- -See our page on `build options ` +See our page on `build options `. -Running Sphinx manually ------------------------ +Using the documentation build script +------------------------------------ You can also build the documentation without running CMake - this is faster if -you only want to rebuild the documentation regardless of any code changes. The -``docs/build.py`` script will build the documentation in any specified formats -(HTML only by default) using the same command that CMake runs when building the -docs. Run the script with ``--help`` to see additional options. +you only want to rebuild the documentation regardless of any code changes. + +The recommended approach is the ``docs/build.py`` script. This is the same +script that CMake uses internally, which wraps Sphinx with a few additional +options to handle common cases and can build multiple documentation formats with +a single invocation. Examples: @@ -449,15 +455,43 @@ Examples: * ``docs/build.py --clean`` Build HTML and force a clean build (all source files are re-read) -The resulting documentation will be stored in ``docs/html`` and/or ``docs/text``. +* ``docs/build.py --help`` + Display a full list of available options + +The resulting documentation will be stored in ``docs/html`` and/or ``docs/text`` +(or generally, a subfolder of ``docs/`` named after the requested output format(s)). + +Building a PDF version +---------------------- + +ReadTheDocs automatically builds a PDF version of the documentation (available +under the "Downloads" section when clicking on the release selector). If you +want to build a PDF version locally, you will need the ``pdflatex`` command, which is part +of a TeX distribution. The following command will then build a PDF, located in +``docs/pdf/latex/DFHack.pdf``, with default options:: + + docs/build.py pdf + +Running Sphinx manually +----------------------- + +If ``docs/build.py`` does not support what you need, you can also run Sphinx +manually. This is primarily useful for low-level debugging. -Alternatively, you can run Sphinx manually with:: +For a good starting point, add the ``--debug`` argument to your call to +``docs/build.py``. This will cause the script to print out the Sphinx command(s) +that it is running. - sphinx-build . docs/html +Some examples: -or, to build plain-text output:: +* ``sphinx-build . docs/html`` + Build the HTML docs (equivalent to ``docs/build.py``) - sphinx-build -b text . docs/text +* ``sphinx-build -b text . docs/text`` + Build the plain text docs (equivalent to ``docs/build.py text``) + +* ``sphinx-build -M latexpdf . docs/pdf`` + Build the PDF docs Sphinx has many options to enable clean builds, parallel builds, logging, and more - run ``sphinx-build --help`` for details. If you specify a different @@ -466,20 +500,47 @@ folder. Also be aware that when running ``sphinx-build`` directly, the ``docs/html`` folder may be polluted with intermediate build files that normally get written in the cmake ``build`` directory. -Building a PDF version ----------------------- +Troubleshooting +=============== -ReadTheDocs automatically builds a PDF version of the documentation (available -under the "Downloads" section when clicking on the release selector). If you -want to build a PDF version locally, you will need ``pdflatex``, which is part -of a TeX distribution. The following command will then build a PDF, located in -``docs/pdf/latex/DFHack.pdf``, with default options:: +Sphinx errors are typically printed by Sphinx, so ensure that you are not silencing Sphinx output. - docs/build.py pdf +When built with ``docs/build.py`` or CMake, errors are also logged to +``build/docs//sphinx-warnings.txt`` (for instance, if you are building +the HTML docs, ``build/docs/html/sphinx-warnings.txt``). + + +"undefined label" +----------------- + +Typical causes: + +* You have used single backticks for an inline code snippet, where double backticks should be used instead (see `docs-links`):: + + `this is an invalid inline code snippet (actually a link)` + ``this is a valid inline code snippet`` + +* You are attempting to link to a section/label, but either it does not have a label defined or you have spelled it incorrectly:: + + .. my-label: + + This is where the link should go to. + + ... + + This is `a valid link to the earlier label `. So is `my-label`. + +"toctree contains reference to document that doesn't have a title" +------------------------------------------------------------------ + +Due to the nature of our autogenerated documentation, this can sometimes occur +when switching between branches that have different autogenerated files, and can +result in autogenerated documentation (e.g. for individual tools) being missing +from the table of contents, or links failing to generate. -Alternatively, you can run Sphinx manually with:: +The quickest resolution is a clean docs build:: - sphinx-build -M latexpdf . docs/pdf + docs/build.py --clean .. _build-changelog: @@ -515,8 +576,8 @@ Changelog syntax ---------------- .. include:: /docs/changelog.txt - :start-after: ===help - :end-before: ===end + :start-after: ===syntax-reference-start + :end-before: ===syntax-reference-end .. _docs-ci: diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 000840c76b..a5c2146634 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -25,6 +25,13 @@ It does not describe all of the utility functions implemented by Lua files located in :file:`hack/lua/*` (:file:`library/lua/*` in the git repo). +.. admonition:: Is this the DF or DFHack Lua API? + :class: warning + + This document describes the Lua API provided by DFHack, not + the Lua API provided by Dwarf Fortress. For information about DF's Lua API, see + :wiki:`Lua scripting` + on the Dwarf Fortress Wiki. .. contents:: Contents :local: @@ -674,6 +681,10 @@ Miscellaneous both from the curry call and the closure call itself. I.e., ``curry(func,a,b)(c,d)`` equals ``func(a,b,c,d)``. +* ``dfhack.dump_interrupt_timing_data()`` + + Dumps the internal log of times between interrupt callbacks, as a table of integers. + Each entry is the difference between two consecutive interrupts, in microseconds. Locking and finalization ------------------------ @@ -931,7 +942,17 @@ can be omitted. * ``dfhack.getHackPath()`` - Returns the dfhack directory path, i.e., ``".../df/hack/"``. + Returns the DFHack installation directory path (the folder where DFHack is installed). + This may be the ``hack`` folder within the DF installation, but you should not rely on this. + Specifically, the installation folder is extremely likely to be somewhere else when DFHack is installed from Steam. + Always use this function to get the DFHack installation directory path instead of hardcoding it. + +* ``dfhack.getConfigPath()`` + + Returns the DFHack config directory path (the folder where user-specific configuration files are stored). + This is currently the ``dfhack-config`` folder within the DF installation, but you should not rely on this as it is likely to change in the future. + Always use this function to get the DFHack config directory path instead of hardcoding it. + Avoid storing this value in a long-lived variable, as it's possible that in future versions of DFHack, it may be possible for the config directory to be changed at runtime. * ``dfhack.getSavePath()`` @@ -1043,7 +1064,64 @@ Translation module * ``dfhack.translation.generateName(name,language,type,major_selector,minor_selector)`` - Dynamically generate a name using the same logic the game itself uses. + Dynamically generate a random name using the same logic the game itself uses. + + **Parameters:** + + .. list-table:: + :class: dfhack-param-table + :header-rows: 1 + :widths: 21 60 19 + + * - Name + - Explanation + - Data Type + * - ``name`` + - | ``name`` property of the object that will have its name generated. This is an output parameter. + | + | Example values: + | ``df.global.world.entities.all[0].name`` + | ``df.unit.find(79).name`` + | ``dfhack.gui.getSelectedUnit().name`` + | ``df.language_name::new()`` + - ``df.language_name`` + * - ``language`` + - | Integer index of the language used for generating the name within ``df.language_translation``. + | + | Example values: + | ``df.global.world.entities.all[0].name.language`` + | ``df.unit.find(79).name.language`` + | ``dfhack.gui.getSelectedUnit().name.language`` + | ``0`` + - ``int`` + * - ``type`` + - | Integer value of the name type. This ensures that the generated name will be appropriate for the given category of object. For allowed values see ``df.language_name_type`` enum. + | + | Example values: + | ``df.global.world.entities.all[100].name.type`` + | ``df.unit.find(79).name.type`` + | ``dfhack.gui.getSelectedUnit().name.type`` + | ``df.language_name_type.Figure`` + | ``13`` + - ``df.language_name_type`` + * - ``major_selector`` + - | Section of the loaded game raws containing words used for generating the name. + | + | Example value for sites belonging to civ with id 100: + | ``df.historical_entity.find(100).entity_raw.symbols.symbols_major[df.entity_name_type.SITE]`` + | + | Example value for units: + | ``df.global.world.raws.language.word_table[0][df.language_name_category.Unit]`` + - ``df.language_word_table`` + * - ``minor_selector`` + - | Section of the loaded game raws containing words used for generating the name. + | + | Example value for sites belonging to civ with id 100: + | ``df.historical_entity.find(100).entity_raw.symbols.symbols_minor[df.entity_name_type.SITE]`` + | + | Example value for units: + | ``df.global.world.raws.language.word_table[1][df.language_name_category.Unit]`` + - ``df.language_word_table`` Gui module ---------- @@ -1073,7 +1151,8 @@ Screens * ``dfhack.gui.getCurFocus([skip_dismissed])`` - Returns the focus string of the current viewscreen. + Returns a list of focus strings for the current viewscreen. Equivalent to + ``dfhack.gui.getFocusStrings(dfhack.gui.getCurViewscreen(skip_dismissed))``. * ``dfhack.gui.getViewscreenByType(type[, depth])`` @@ -1312,6 +1391,10 @@ Job module Creates a deep copy of the given job. +* ``dfhack.job.createLinked()`` + + Create a job and immediately link it into the global job list. + * ``dfhack.job.printJobDetails(job)`` Prints info about the job. @@ -1337,6 +1420,12 @@ Job module Searches for a specific_ref with the given type. +* ``dfhack.job.assignToWorkshop(job, workshop)`` + + Assign job to workshop (i.e. establish the bidirectional link between the job + and the workshop). Does nothing and returns ``false`` if the workshop already + has the maximum of ten jobs. + * ``dfhack.job.getHolder(job)`` Returns the building holding the job. @@ -1395,9 +1484,9 @@ Job module Attach a real item to this job. If the item is intended to satisfy a job_item filter, the index of that filter should be passed in ``filter_idx``; otherwise, pass ``-1``. Similarly, if you don't care where the item is inserted, pass - ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_item_ref.T_role``. + ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_role_type``. If the item needs to be brought to the job site, then the value should be - ``df.job_item_ref.T_role.Hauled``. + ``df.job_role_type.Hauled``. * ``dfhack.job.isSuitableItem(job_item, item_type, item_subtype)`` @@ -1412,6 +1501,57 @@ Job module Returns the job's description, as seen in the Units and Jobs screens. +* ``dfhack.job.getManagerOrderName(manager_order)`` + + Returns the manager order's description, as seen in the Work orders screen. + +Hotkey module +------------- + +* ``dfhack.hotkey.addKeybind(keyspec, command)`` + + Creates a new keybind with the provided keyspec (see the `keybinding` documentation + for details on format). + Returns false on failure to create keybind. + +* ``dfhack.hotkey.removeKeybind(keyspec, [match_focus=true, command])`` + + Removes keybinds matching the provided keyspec. + If match_focus is set, the focus portion of the keyspec is matched against. + If command is provided and not an empty string, the command is matched against. + Returns false if no keybinds were removed. + +* ``dfhack.hotkey.listActiveKeybinds()`` + + Returns a list of keybinds active within the current context. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.listAllKeybinds()`` + + Returns a list of all keybinds currently registered. + The items are tables with the following attributes: + :spec: The keyspec for the hotkey + :command: The command the hotkey runs when pressed + +* ``dfhack.hotkey.requestKeybindingInput([cancel=false])`` + + Enqueues or cancels a request that the next hotkey-compatible input is saved + and not processed, retrievable with ``dfhack.hotkey.getKeybindingInput()``. + If cancel is true, any current request is cancelled. + +* ``dfhack.hotkey.getKeybindingInput()`` + + Reads the latest saved keybind input that was requested. + Returns a keyspec string for the input, or nil if no input has been saved. + +* ``dfhack.hotkey.isDisruptiveKeybind(keyspec)`` + + Determines if the provided keyspec could be disruptive to the game experience. + This includes the majority of standard characters and special keys such as escape, + backspace, and return when lacking modifiers other than Shift. + Units module ------------ @@ -1429,12 +1569,14 @@ Units module same checks the game uses to decide game-over by extinction, with an additional sanity check. You can identify citizens, regardless of their sanity, by passing ``true`` as the optional - second parameter. + second parameter. Dead units are never considered citizens, even + when ``include_insane`` is set (though animate intelligent + undead citizens still count). * ``dfhack.units.isResident(unit[,include_insane])`` - The unit is a resident of the fortress. Same ``include_insane`` semantics as - ``isCitizen``. + The unit is a non-dead resident of the fortress. Same + ``include_insane`` semantics as ``isCitizen``. * ``dfhack.units.isFortControlled(unit)`` @@ -1567,6 +1709,7 @@ Units module * ``dfhack.units.hasExtravision(unit)`` * ``dfhack.units.isOpposedToLife(unit)`` * ``dfhack.units.isBloodsucker(unit)`` +* ``dfhack.units.breathes(unit)`` Simple checks of caste attributes that can be modified by curses. @@ -1627,7 +1770,7 @@ Units module Returns true if the unit is within a box defined by the specified coordinates. -``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` +* ``dfhack.units.getUnitsInBox(pos1, pos2[, filter])`` * ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` Returns a table of all units within the specified coordinates. @@ -1665,6 +1808,11 @@ Units module Moves the specified unit and any riders to the target coordinates, setting tile occupancy flags appropriately. Returns true if successful. + This function correctly removes units from cages and chains, and correctly + removes the links between one unit dragging another unit. As a special + case, teleporting a merchant that is dragging a pack animal will also + teleport the pack animal. + * ``dfhack.units.getGeneralRef(unit, type)`` Searches for a ``general_ref`` with the given type. @@ -1772,17 +1920,18 @@ Units module Get human-readable baby or child name (e.g., "dwarven baby" or "dwarven child"). -* ``dfhack.units.getReadableName(unit or historical_figure)`` +* ``dfhack.units.getReadableName(unit or historical_figure[, skip_english])`` - Returns a string that includes the language name of the unit (if any), the - race of the unit (if different from fort), whether it is trained for war or - hunting, any syndrome-given descriptions (such as "necromancer"), the - training level (if tame), and profession or noble role. If a - ``historical_figure`` is passed instead of a unit, some information - (e.g., agitation status) is not available, and the profession may be - different (e.g., "Monk") from what is displayed in fort mode. + Returns a string that includes the native and english language name (if + ``skip_english`` is not ``true``) of the unit (if any), the race of the unit + (if different from fort), whether it is trained for war or hunting, any + syndrome-given descriptions (such as "necromancer"), the training level (if + tame), and profession or noble role. If a ``historical_figure`` is passed + instead of a unit, some information (e.g., agitation status) is not + available, and the profession may be different (e.g., "Monk") from what is + displayed in fort mode. -* ``dfhack.units.getAge(unit[,true_age])`` +* ``dfhack.units.getAge(unit[, true_age])`` Returns the age of the unit in years as a floating-point value. If ``true_age`` is true, ignores false identities. @@ -1892,6 +2041,23 @@ Units module Return the ``df.activity_entry`` or ``df.activity_event`` representing the unit's current social activity. +* ``dfhack.units.hasUnbailableSocialActivity(unit)`` + + Unit has an uninterruptible social activity (e.g. a purple "Socialize!"). + +* ``dfhack.units.isJobAvailable(unit [, preserve_social])`` + + Check whether a unit can be assigned to (i.e. is looking for) a job. Will + return ``true`` if the unit is engaged in "green" social activities, unless + the boolean ``preserve_social`` is true. Will never interrupt uninterruptible + social activities (e.g. a purple "Socialize!"). + +* ``dfhack.units.getFocusPenalty(unit, need_type [, need_type, ...])`` + + Get largest (i.e. most negative) focus penalty associated to a collection of + ``df.need_type`` arguments. Returns a number strictly greater than 400 if the + unit does not have any of the requested needs. + * ``dfhack.units.getStressCategory(unit)`` Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. @@ -1907,6 +2073,13 @@ Units module Returns a table of the cutoffs used by the above stress level functions. +* ``dfhack.units.getBreathingState(unit)`` + + Returns a value indicating the unit's current breathing state, which can be one of the following: + * ``CANT`` (0) - The unit cannot breathe and is at risk of suffocation. + * ``TROUBLE`` (1) - The unit is having trouble breathing but is not in immediate danger. + * ``FINE`` (2) - The unit is breathing normally. + Action Timer API ~~~~~~~~~~~~~~~~ @@ -1986,6 +2159,32 @@ Military module Returns the name of a squad as a string. +* ``dfhack.military.removeFromSquad(unit_id)`` + + Removes a unit from its squad. Unsets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), modifies the unit's entity links + to indicate former squad membership or command, and creates a corresponding + world history event. + + * ``dfhack.military.addToSquad(unit_id, squad_id, squad_pos)`` + + Adds a unit to a squad. Sets the unit's + military information (i.e., ``unit.military.squad_id`` and + ``unit.military.squad_pos``), the squad's position information (i.e., + ``squad.positions[squad_pos].occupant``), adds a unit's entity links to + indicate squad membership. Does not currently add world history events. + If ``squad_pos`` is -1, the unit will be added to the first open slot in + the squad. + + This API cannot be used to set or change the leader of a squad and will fail + if ``squad_pos`` is specified as 0 or if ``squad_pos`` is specified as -1 and + the squad leader position is currently vacant. It will also fail if + the requested squad position is already occupied, the squad does not exist, + the unit does not exist, or the requested unit is already a member of another + squad. + Items module ------------ @@ -2099,7 +2298,7 @@ Items module * ``dfhack.items.moveToInventory(item,unit[,use_mode[,body_part]])`` Move the item to the unit inventory. Returns *false* if impossible. - ``use_mode`` defaults to ``df.unit_inventory_item.T_mode.Hauled``. + ``use_mode`` defaults to ``df.inv_item_role_type.Hauled``. ``body_part`` defaults to ``-1``. * ``dfhack.items.remove(item[,no_uncat])`` @@ -2219,7 +2418,8 @@ World module * ``dfhack.world.ReadCurrentWeather()`` - Returns the current game weather (``df.weather_type``). + Returns the current game weather (``df.weather_type``). Deprecated: use + ``dfhack.maps.getCurrentWeather()`` instead. * ``dfhack.world.SetCurrentWeather(weather)`` @@ -2236,7 +2436,12 @@ World module * ``dfhack.world.isLegends([gametype])`` Without any arguments, returns *true* if the current gametype matches. - Optionally accepts a ``gametype`` id to match against. + Optionally accepts a ``gametype`` ID to match against. + + Be advised that ``isAdventureMode()`` does not detect adventure arena (controlling + arena unit). It may be desirable to test ``df.global.gamemode == df.game_mode.ADVENTURE`` + to allow for both, or ``df.global.gametype == df.game_type.ADVENTURE_ARENA`` to check for + just adventure arena, depending on your purpose. * ``dfhack.world.getCurrentSite()`` @@ -2244,7 +2449,7 @@ World module * ``dfhack.world.getAdventurer()`` - Returns the current adventurer unit (if in adventure mode). + Returns the current adventurer unit (if in adventure mode or adventure arena) else ``nil``. .. _lua-maps: @@ -2280,6 +2485,23 @@ Maps module Like ``getTileBlock``, but if the block is not allocated, try creating it. +* ``dfhack.maps.getTileBlockCoord(coords)``, or ``getTileBlockCoord(x,y,z)`` + + Returns the *x, y, z* block coordinates of the map block containing the + given tile position. The z coordinate is a level and is preserved, not + scaled. + +* ``dfhack.maps.getBlockOrigin(coords)``, or ``getBlockOrigin(x,y,z)`` + + Returns the *x, y, z* tile coordinates of the north-west corner of the map + block with the given block coordinates. The z coordinate is a level and is + preserved, not scaled. + +* ``dfhack.maps.getTileBlockOffset(coords)``, or ``getTileBlockOffset(x,y,z)`` + + Returns the *x, y* offset (0-15) of the given tile position within its + containing map block, along with the unchanged *z* coordinate. + * ``dfhack.maps.getTileType(coords)``, or ``getTileType(x,y,z)`` Returns the tile type at the given coordinates, or *nil* if invalid. @@ -2318,6 +2540,13 @@ Maps module Returns *x, y* for use with ``getRegionBiome`` and ``getBiomeType``. +* ``dfhack.maps.getCurrentWeather(coords)`` + ``dfhack.maps.getCurrentWeather(x,y,z)`` + ``dfhack.maps.getCurrentWeather()`` + + Returns the current game weather (``df.weather_type``) at the specified tile + or at the center of the map if no argument is provided. + * ``dfhack.maps.getPlantAtTile(pos)``, or ``getPlantAtTile(x,y,z)`` Returns the plant struct that owns the tile at the specified position. @@ -2377,9 +2606,110 @@ Maps module Removes an aquifer from the given tile position. Returns *true* or *false* depending on success. +* ``dfhack.maps.addMaterialSpatter(pos, mat, matg, state, amount)`` + + Adds a material spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + Specifying a state of -1 (None) will automatically choose either Solid, + Liquid, or Gas based on the material properties and the tile temperature. + +* ``dfhack.maps.addItemSpatter(pos, i_type, i_subtype, subcat1, subcat2, print_variant, amount)`` + + Adds an item spatter to the specified map tile. If the tile is already + full of that spatter, returns the amount left over. + + For plant growths, specifying a print_variant of -1 will automatically + choose an appropriate value. For other item types, this field is ignored. + +* ``dfhack.maps.forEachTile(bounds[, filter[, actions]])`` + + Scans every tile inside a cuboid of map tiles in native code, tests each + tile against a filter, and applies one or more actions to the matching + tiles. This is dramatically faster than iterating tiles in Lua, since the + entire scan runs in a single C++ call. + + .. note:: + This API is experimental and may change in future releases. + + The *bounds* argument may be given as a ``{x1,y1,z1,x2,y2,z2}`` table, as + two coordinate values ``(pos1, pos2)``, or as six integers + ``(x1,y1,z1,x2,y2,z2)``. The cuboid is clamped to the loaded map and tiles + in unallocated map blocks are skipped. + + The *filter* argument selects which tiles are acted on. It may be a table + with the following keys (all optional; a tile must satisfy every given + criterion to match), or a function (see below), or *nil* to match all + tiles: + + - ``tiletype``/``tiletypes``: a tiletype or array of tiletypes (numbers or + ``df.tiletype`` names). The tile's tiletype must be one of them. + - ``material``, ``shape``, ``shape_basic``, ``special``, ``variant``: a + tiletype attribute value or array of values (numbers or names from + ``df.tiletype_material``, ``df.tiletype_shape``, + ``df.tiletype_shape_basic``, ``df.tiletype_special``, or + ``df.tiletype_variant`` respectively). The tile's tiletype must have one + of the listed values for that attribute. + - ``designation``: a table of ``df.tile_designation`` field names to + required values, e.g. ``{hidden=false, subterranean=true}``. + Multi-bit fields such as ``dig`` or ``flow_size`` take integer values. + - ``occupancy``: same, for ``df.tile_occupancy`` fields. + - ``filter``: a function ``fn(x, y, z, block, localx, localy, tiletype)`` + evaluated last for each candidate tile; the tile matches only if it + returns a truthy value. ``localx``/``localy`` are the tile's + coordinates within ``block`` (0-15). + + The *actions* argument says what to do with each matching tile. It may be + a table with the following keys (all optional), a function (equivalent to + ``{callback=fn}``), or *nil* to just count matches: + + - ``set_tiletype``: a tiletype (number or name) to write to matching + tiles, or a table mapping tiletypes to replacement tiletypes (keys and + values may be numbers or names). Tiles whose current tiletype is a key + in the table are rewritten to the mapped value, taking precedence over + a plain ``set_tiletype`` value; unmapped tiles fall back to it. + - ``designation``: a table of ``df.tile_designation`` field names to + values, assigned on each matching tile (e.g. ``{hidden=false}`` clears + the hidden flag). + - ``occupancy``: same, for ``df.tile_occupancy`` fields. + - ``construct``: spawn a ``df.construction`` record on each matching tile + that does not already have one. The value is a table with keys + ``item_type`` (number or ``df.item_type`` name), ``item_subtype``, + ``mat_type``, ``mat_index``, ``flags`` (a table of + ``df.construction_flags`` field names to values, e.g. + ``{no_build_item=true}``), and ``tiletype`` (a tiletype to write to the + map tile; the construction's ``original_tile`` records the tiletype + present before that write). Constructions are buffered during the scan + and merged into ``world.event.constructions`` in one sorted pass, so + bulk spawning is cheap. + - ``callback``: a function ``fn(x, y, z, block, localx, localy, + tiletype)`` invoked for each matching tile after the other actions are + applied. Returning exactly ``false`` aborts the scan. + + Returns a table with counts: ``scanned`` (tiles visited), ``matched`` + (tiles passing the filter), ``changed`` (tiles whose tiletype was + rewritten), ``constructed`` (construction records created), and + ``aborted`` (true if a callback stopped the scan early). + + Example:: + + local res = dfhack.maps.forEachTile({x1,y1,z1,x2,y2,z2}, { + material = {df.tiletype_material.STONE, df.tiletype_material.MINERAL}, + shape_basic = df.tiletype_shape_basic.Floor, + designation = {hidden = false}, + }, { + set_tiletype = df.tiletype.StoneFloorSmooth, + }) + print(res.matched) + Burrows module -------------- +* ``dfhack.burrows.getName(burrow)`` + + Returns the name of the burrow. + If the burrow has no set name, returns the same placeholder name that DF would show in the UI. + * ``dfhack.burrows.findByName(name[, ignore_final_plus])`` Returns the burrow pointer or *nil*. if ``ignore_final_plus`` is ``true``, @@ -2438,6 +2768,10 @@ General Searches for a specific_ref with the given type. +* ``dfhack.buildings.getOwner(civzone)`` + + Returns the owner of the zone or *nil* if there isn't one. + * ``dfhack.buildings.setOwner(civzone,unit)`` Replaces the owner of the civzone. If unit is *nil*, removes ownership. @@ -2468,14 +2802,15 @@ General using width and height for flexible dimensions. Returns *is_flexible, width, height, center_x, center_y*. -* ``dfhack.buildings.checkFreeTiles(pos,size[,extents[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` +* ``dfhack.buildings.checkFreeTiles(pos,size[,bld[,change_extents[,allow_occupied[,allow_wall[,allow_flow]]]]])`` - Checks if the rectangle defined by ``pos`` and ``size``, and possibly extents, - can be used for placing a building. If ``change_extents`` is true, bad tiles - are removed from extents. If ``allow_occupied``, the occupancy test is skipped. - Set ``allow_wall`` to true if the building is unhindered by walls (such as an - activity zone). Set ``allow_flow`` to true if the building can be built even - if there is deep water or any magma on the tile (such as abstract buildings). + Checks if the rectangle defined by ``pos`` and ``size``, and possibly the + extents associated with bld, can be used for placing a building. If + ``change_extents`` is true, bad tiles are removed from extents. If + ``allow_occupied``, the occupancy test is skipped. Set ``allow_wall`` to true + if the building is unhindered by walls (such as an activity zone). Set + ``allow_flow`` to true if the building can be built even if there is deep + water or any magma on the tile (such as abstract buildings). * ``dfhack.buildings.countExtentTiles(extents,defval)`` @@ -2723,12 +3058,9 @@ Common parameters to these functions include: * ``x``, ``y``: screen coordinates in tiles; the upper left corner of the screen is ``x = 0, y = 0`` * ``pen``: a `pen object ` -* ``map``: a boolean indicating whether to draw to a separate map buffer - (defaults to false, which is suitable for off-map text or a screen that hides - the map entirely). Note that only third-party plugins like TWBT currently - implement a separate map buffer. If no such plugins are enabled, passing - ``true`` has no effect. However, this parameter should still be used to ensure - that scripts work properly with such plugins. +* ``map``: a boolean (defaults to false) indicating whether to draw to a + separate map buffer. The Steam version uses separate map buffers with square + tiles for for all types of maps (i.e. fort, region, and world). Functions: @@ -2754,15 +3086,31 @@ Functions: * ``dfhack.screen.paintTile(pen,x,y[,char[,tile[,map]]])`` Paints a tile using given parameters. `See below ` for a - description of ``pen``. + description of ``pen``. The map argument is only supported for local maps + (i.e. fort mode and adventure mode outside of fast travel). The ``char`` and + ``tile`` arguments allow overriding the respective parts of the ``pen`` + without constructing a new pen beforehand. Returns *false* on error, e.g., if coordinates are out of bounds +* ``dfhack.screen.paintMapPortTile(pen,x,y[,char[,tile]])`` + + Paints a tile using given parameters onto the interface texpos layer of a map + port (e.g., the world map or the zoomed-in map for embark selection). The + ``char`` and ``tile`` arguments work as above. + * ``dfhack.screen.readTile(x,y[,map])`` Retrieves the contents of the specified tile from the screen buffers. Returns a `pen object `, or *nil* if invalid or TrueType. +* ``dfhack.screen.readMapPortTile(x,y)`` + + Retrieves the contents of the specified tile from the screen buffers. Returns + a `pen object `, or *nil* if invalid. + + For now only looks at the ``sites`` textpos layer. + * ``dfhack.screen.paintString(pen,x,y,text[,map])`` Paints the string starting at *x,y*. Uses the string characters @@ -3158,12 +3506,6 @@ unless otherwise noted. specified by ``path``, or -1 if ``path`` does not exist. This depends on the system clock and should only be used locally. -* ``dfhack.filesystem.atime(path)`` -* ``dfhack.filesystem.ctime(path)`` - - Return values vary across operating systems - return the ``st_atime`` and - ``st_ctime`` fields of a C++ stat struct, respectively. - * ``dfhack.filesystem.listdir(path)`` Lists files/directories in a directory. Returns ``{}`` if ``path`` does not exist. @@ -3179,6 +3521,16 @@ unless otherwise noted. following it for each entry. Set ``include_prefix`` to false if you don't want the ``path`` string prepended to the returned filenames. +* ``dfhack.filesystem.getBaseDir()`` + + Returns a directory to which DF (and thus DFHack) can save files. This will either + be DF's install directory, or the path returned by ``SDLGetPrefDir``, depending on whether + DF is in "portable mode" or not. + +* ``dfhack.filesystem.getInstallDir()`` + + Returns the the directory in which DF is installed. + Console API ----------- @@ -3312,12 +3664,14 @@ and are only documented here for completeness: * ``dfhack.internal.findScript(name)`` - Searches `script paths ` for the script ``name`` and returns the - path of the first file found, or ``nil`` on failure. + Searches `script paths ` for the script ``name`` (which + includes the ``.lua`` extension) and returns the absolute path of the first + file found, or ``nil`` on failure. Slashes in the path are canonicalized to + forward slashes. .. note:: - This requires an extension to be specified (``.lua`` or ``.rb``) - use - ``dfhack.findScript()`` to include the ``.lua`` extension automatically. + You can use the ``dfhack.findScript()`` wrapper if you want to specify the + script name without the ``.lua`` extension. * ``dfhack.internal.runCommand(command[, use_console])`` @@ -3418,6 +3772,11 @@ and are only documented here for completeness: Sets the system clipboard text from a CP437 string. Character 0x10 is interpreted as a newline instead of the usual CP437 glyph. +* ``dfhack.internal.getModifiers()`` + + Returns the state of the keyboard modifier keys in a table of string -> + boolean. The keys are ``ctrl``, ``shift``, ``super``, and ``alt``. + * ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` * ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` @@ -3470,6 +3829,15 @@ Core context specific functions: Returns the timer id, or *nil* if unsuccessful due to world being unloaded. +* ``dfhack.timeout_until(tick,callback)`` + + Arranges for the callback to be called once the in-game + tick count (``df.global.world.frame_counter``) reaches + the specified value. The timer is canceled when the + world is unloaded, and cannot be queued until it is + loaded again. Returns the timer id, or *nil* if + unsuccessful due to world being unloaded. + * ``dfhack.timeout_active(id[,new_callback])`` Returns the active callback with the given id, or *nil* @@ -3581,6 +3949,9 @@ environment by the mandatory init file dfhack.lua: ``COLOR_GREY`` and ``COLOR_DARKGREY`` can also be spelled ``COLOR_GRAY`` and ``COLOR_DARKGRAY``. + Note: ``COLOR_RESET`` is not valid in a `Pen `, and using it in a Pen color field + will result in runtime warnings and may result in color flashing or other unexpected results. + * State change event codes, used by ``dfhack.onStateChange`` Available only in the `core context `, as is the event itself: @@ -3612,6 +3983,9 @@ environment by the mandatory init file dfhack.lua: * ``printall_recurse(obj)`` If the argument is a lua table or DF object reference, prints all fields recursively. + The fields of untagged unions are not printed since only one member of such a + union is live at a time and there is no way to identify which one, so + accessing any other member is undefined behavior. * ``copyall(obj)`` @@ -3752,6 +4126,29 @@ paths will be relative to the top level game directory and will end in a slash Which would open ``dfhack-config/mods/my_awesome_mod/settings.json``. After calling ``getModStatePath``, the returned directory is guaranteed to exist. +* ``get_active_mods()`` + + Returns a list of all active mods in the current world. The list elements are + tables containing the following fields: + + - id: mod id + - name: mod display name + - version: mod display version + - numeric_version: numeric mod version + - path: path to the mod directory + - vanilla: true if this is a vanilla mod + +* ``get_mod_info_metadata(mod_path, tags)`` + + Returns a table with the values of the given tags from the ``info.txt`` file + in the given mod directory. The ``mod_path`` argument must be a path to a mod + directory (retrieved, say, from ``get_active_mods()``). The ``tags`` argument + is a string or a list of strings representing the tags to retrieve. The + function will return a table with the tag names as keys and their values as + values. If a requested tag includes the string ``NUMERIC_``, it will return + the numeric value for that tag (e.g., ``NUMERIC_VERSION`` will return the + numeric version of the mod as a number instead of a string). + utils ===== @@ -5584,7 +5981,7 @@ TextArea Functions: * ``textarea:getText()`` Returns the current text content of the ``TextArea`` widget as a string. - "\n" characters (``string.char(10)``) should be interpreted as new lines + ``\n`` characters (``string.char(10)``) should be interpreted as new lines * ``textarea:setText(text)`` @@ -5936,6 +6333,8 @@ common text token lists that you can then pass as ``text`` to a ``Label``: Example 2: The DFHack logo - a graphical button in graphics mode and a text button in ASCII mode. The ASCII colors use the default for hovering:: + local logo_textures=dfhack.textures.loadTileset( + 'hack/data/art/logo.png', 8, 12, true), widgets.Label{ text=widgets.makeButtonLabelText{ chars={ @@ -5943,10 +6342,12 @@ common text token lists that you can then pass as ``text`` to a ``Label``: {179, 'H', 'a', 179}, {179, 'c', 'k', 179}, }, - tileset=dfhack.textures.loadTileset( - 'hack/data/art/logo.png', 8, 12, true), - tileset_hover=dfhack.textures.loadTileset( - 'hack/data/art/logo_hovered.png', 8, 12, true), + tileset=logo_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} @@ -6169,12 +6570,27 @@ This is a specialized subclass of CycleHotkeyLabel that has two options: ``On`` (with a value of ``true``) and ``Off`` (with a value of ``false``). The ``On`` option is rendered in green. +ConfigureButton class +--------------------- + +A 3x1 tile button with a gear symbol on it, intended to represent a configure +icon. Clicking on the icon will run the given callback. The graphics can also +be overridden to create custom buttons. + +It has the following attributes: + +:on_click: The function to run when the icon is clicked. +:pen_left: Pen or function returning a pen to overwrite the left tile of the button. +:pen_center: As above, but for the center tile (gear symbol). +:pen_right: As above, but for the right tile. + HelpButton class ---------------- -A 3x1 tile button with a question mark on it, intended to represent a help -icon. Clicking on the icon will launch `gui/launcher` with a given command -string, showing the help text for that command. +Subclass of ConfigureButton; a 3x1 tile button with a question mark on it, +intended to represent a help icon. Clicking on the icon will launch +`gui/launcher` with a given command string, showing the help text for that +command. It has the following attributes: @@ -6184,15 +6600,23 @@ It also sets the ``frame`` attribute so the button appears in the upper right corner of the parent, but you can override this to your liking if you want a different position. -ConfigureButton class ---------------------- +RadioButton class +----------------- -A 3x1 tile button with a gear mark on it, intended to represent a configure -icon. Clicking on the icon will run the given callback. +Subclass of ConfigureButton; a 3x1 tile button that resembles a radio button +(or check box in ASCII mode), identical to the ones found in +`gui/control-panel`. Clicking on the button will toggle its enabled state. It has the following attributes: -:on_click: The function on run when the icon is clicked. +:initial_state: Whether to start in the ``true`` or ``false`` state. Defaults to ``true``. +:on_change: Callback to call when state changes, including initialization. Called as ``on_change(val)``. + +It implements the following method: + +* ``RadioButton:setState(val)`` + + Sets the state to boolean ``val`` and calls ``on_change`` (if defined). BannerPanel class ----------------- @@ -6349,7 +6773,8 @@ Filter behavior: By default, the filter matches substrings that start at the beginning of a word (or after any punctuation). You can instead configure filters to match any -substring across the full text with a command like:: +substring across the full text by setting ``FILTER_FULL_TEXT`` in `gui/control-panel` +or set it for the session by running a command like:: :lua require('utils').FILTER_FULL_TEXT=true @@ -6444,10 +6869,24 @@ change, the ``RangeSlider`` appearance will adjust automatically. :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. + 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. +Slider class +----------------- + +This widget implements a mouse-interactable slider. The player can move the handle to +set the value of the slider. The parent widget owns the slider value, and can control +it independently (e.g., with a ``CycleHotkeyLabel``). If the value changes, the ``Slider`` +appearance will adjust automatically. + +:num_stops: Used to specify the number of "notches" in the slider, the places + where the handle can stop. (This should match the parents' number of options.) +:get_idx_fn: The function used by the Slider to get the notch index on which + to display the handle. +:on_change: Callback executed when moving the handle. + DimensionsTooltip class ----------------------- @@ -7349,6 +7788,15 @@ Importing scripts --@ module = true + In order to be recognized, this line **must** begin with ``--@`` with no + whitespace characters before it:: + + --@ module = true OK + --@module = true OK + -- @module = true NOT OK (no --@ found due to space after --) + --@module = true NOT OK (leading space, --@ is not at the beginning of the line) + ---@module = true NOT OK (leading dash, --@ is not at the beginning of the line) + 2. Include a check for ``dfhack_flags.module``, and avoid running any code that has side-effects if this flag is true. For instance:: diff --git a/docs/dev/Material-encoding.rst b/docs/dev/Material-encoding.rst new file mode 100644 index 0000000000..e2506293d5 --- /dev/null +++ b/docs/dev/Material-encoding.rst @@ -0,0 +1,207 @@ +.. _material-encoding: + +##################################################### +How DF encodes materials: ``mat_type``/``mat_index`` +##################################################### + +Almost everywhere that Dwarf Fortress needs to refer to a material -- items, +item improvements, job requirements, plants, reactions, syndromes, map +features, and so on -- it does so with a pair of integers. In the names +provided by Bay 12, the pair is usually called ``material`` and +``matgloss``, although other naming is sometimes seen, such as ``subcat1``/ +``subcat2``. The ``subcat1``/``subcat2`` names usually appear on fields +that can hold *either* a material+matgloss pair *or* a race+caste pair, +with a companion ``item_type`` field determining how to interpret them. +Before we learned otherwise, the DFHack team originally called +what we now know to be a matgloss a ``mat_index`` ("material index"), and so +this term tends to be used in DFHack code instead of "matgloss". Thus, you may +see these two terms used interchangeably in DFHack code, in documentation, and +in community discussions. + +.. contents:: Contents + :local: + +The ``mat_type`` half +===================== + +``mat_type`` is an index into ``world.raws.mat_table.builtin`` +(a ``special_mat_table``, DF name ``material_infost.misc_def``), which is a +static array of pointers to ``df::material`` objects indexed by the +``builtin_mats`` enum (DF name ``MaterialType``). The enum is laid out as +follows: + +================= ===================== ============================================= +Numeric range Enum values Meaning +================= ===================== ============================================= +-1 ``NONE`` No material / invalid +0 ``INORGANIC`` ``mat_index`` selects a ``df::inorganic_raw`` +1..18 ``AMBER``..``GRIME`` True built-in materials +19..218 ``CREATURE_1..200`` Creature raw materials +219..418 ``HIST_FIG_1..200`` Historical figure creature materials +419..618 ``PLANT_1..200`` Plant raw materials +619..658 ``UNUSED01..40`` Unused; treated as built-ins, normally null +================= ===================== ============================================= + +The named built-ins are ``AMBER``, ``CORAL``, ``GLASS_GREEN``, +``GLASS_CLEAR``, ``GLASS_CRYSTAL``, ``WATER``, ``COAL``, ``POTASH``, +``ASH``, ``PEARLASH``, ``LYE``, ``MUD``, ``VOMIT``, ``SALT``, ``FILTH_B`` +(feces), ``FILTH_Y`` (urine), ``UNKNOWN_SUBSTANCE`` (semen), and ``GRIME``. +Several of these built-ins exist solely for legacy purposes and do not normally +arise in unmodded games; for example, ``FILTH_B`` and ``FILTH_Y`` only appear +in very specific situations, and ``UNKNOWN_SUBSTANCE`` was only ever seen in +connection with tentacle demons, which haven't been in the game since version +0.28.181.40d. These are materials constructed by the game itself (not loaded +from raws). The array still has a slot for every enum value, but entries outside +the named built-ins are not real materials -- for the ``CREATURE_*``, +``HIST_FIG_*``, and ``PLANT_*`` ranges the actual ``df::material`` lives +inside a raw object and ``mat_index`` is needed to find it. Entries in +the array may be null, so consumers should always check the resolved +pointer. + +The ``mat_index`` half +====================== + +The meaning of ``mat_index`` depends entirely on the ``mat_type`` range: + +* ``mat_type`` < 0 (``NONE``): no material. ``mat_index`` should also be -1. + +* ``mat_index`` == -1, any non-negative ``mat_type``: a "bare" reference to + the built-in material object ``mat_table.builtin[mat_type]``. For most + built-ins this is just the material itself, but ``(0, -1)`` is special: + it is the *inorganic wildcard*, meaning "any inorganic material", used in + contexts such as job item constraints (e.g. "any stone"). The same + ``(INORGANIC, -1)`` pair -- written ``INORGANIC:NONE`` in token form -- + is also used for the "magma" material of liquid items picked up in + minecarts. + +* ``mat_type`` == 0 (``INORGANIC``), ``mat_index`` >= 0: ``mat_index`` is + an index into ``world.raws.inorganics.all`` (a ``df::inorganic_raw``), + and the material is ``inorganic->material``. For example, + ``(0, )`` is iron. + +* 1 <= ``mat_type`` <= 18 (built-ins), ``mat_index`` >= 0: still a + built-in material; ``mat_index`` is normally ignored. The one exception + is ``COAL`` (7), where ``mat_index`` distinguishes the product: + 0 means coke and 1 means charcoal. DF has a dedicated enum for these + values, ``MatGlossCoal`` (``MATGLOSS_COAL_COKE`` = 0, + ``MATGLOSS_COAL_CHARCOAL`` = 1), though df-structures does not currently + expose it. + +* 19 <= ``mat_type`` <= 218 (``CREATURE_1``..``CREATURE_200``): + ``mat_index`` is an index into ``world.raws.creatures.all`` + (a ``df::creature_raw``), and ``mat_type - CREATURE_1`` is an index into + that creature's ``material`` vector. In other words, ``mat_type`` picks + both the table *and* which of the creature's (up to 200) defined + materials is meant, while ``mat_index`` picks the creature. + For example, dwarf bone is + ``(CREATURE_1 + , )``. + +* 219 <= ``mat_type`` <= 418 (``HIST_FIG_1``..``HIST_FIG_200``): like the + creature range, except that ``mat_index`` is a **historical figure id**, + not a vector index -- the figure is looked up by its ``id`` field in + ``world.history.figures``. The material itself is + ``creature->material[mat_type - HIST_FIG_1]`` of the figure's race + (``creature_raw::find(figure->race)``). These pairs identify materials + belonging to a *specific* historical figure. In practice they are most + often seen in body-material spatters (``spatter_common``, e.g. blood) + left by named creatures, and in products of named creatures such as + forgotten beasts, titans, and demons. + +* 419 <= ``mat_type`` <= 618 (``PLANT_1``..``PLANT_200``): + ``mat_index`` is an index into ``world.raws.plants.all`` + (a ``df::plant_raw``), and ``mat_type - PLANT_1`` is an index into that + plant's ``material`` vector. + +* ``mat_type`` > 618 (``UNUSED*``): treated as a built-in; + ``mat_table.builtin[mat_type]`` is used directly and is normally null. + +Where the materials themselves live +=================================== + +All of the above resolves to a ``df::material`` object (DF name +``material_definitionst``), which holds the material id, per-state names +and colors, flags, heat properties, and so on: + +* Built-ins: ``world.raws.mat_table.builtin[mat_type]``. +* Inorganics: embedded in ``world.raws.inorganics.all[mat_index]->material``. +* Creatures: ``world.raws.creatures.all[mat_index]->material[subtype]``. +* Plants: ``world.raws.plants.all[mat_index]->material[subtype]``. + Additionally, each ``df::plant_raw`` has a ``material_defs`` block -- + parallel ``type``/``idx`` arrays (DF names ``use_mat``/``use_matg``) + indexed by ``plant_material_def`` -- that records the canonical + ``(mat_type, mat_index)`` pair for each defined use of the plant + (``basic_mat``, ``tree``, ``drink``, ``seed``, ``thread``, ``mill``, + ``extract_vial``, ``extract_barrel``, ``extract_still_vial``). + +Where the pairs appear +====================== + +The ``mat_type``/``mat_index`` naming convention is used fairly consistently +in DF structures for individual fields, so ``MaterialInfo::decode`` can +consume any structure that exposes those members. Some representative +examples: + +* Most item types (``item_barst``, ``item_woodst``, ``item_boulderst``, + ``item_threadst``, etc.) store the pair directly in ``mat_type`` and + ``mat_index`` fields. +* ``item_body_component`` (corpses and body parts) instead stores + ``race``, ``caste``, ``hist_figure_id``, and ``unit_id``. The item + virtual methods ``getActualMaterial()`` and ``getActualMaterialIndex()`` + (DF names ``get_forced_material``/``get_forced_matgloss``) compute the + proper pair -- "an actual material, never a race/caste" -- so + ``MaterialInfo::decode(item)`` always uses those methods rather than the + raw fields. This is the dual-purpose encoding mentioned above: the raw + ``getMaterial()``/``getMaterialIndex()`` methods (DF ``get_subcat1``/ + ``get_subcat2``) return the race and caste for items like these. +* ``material_vec_ref`` is a DFHack convenience compound (not a real DF + structure) that bundles the parallel ``mat_type``/``mat_index`` vectors + DF uses for multi-material lists, e.g. in reaction products and + ``world.raws.mat_table.syndromes``/``effects``. In the actual Bay 12 + headers, these structures are parallel arrays appearing as sequential + members within a larger compound. +* ``job_item`` (work order and job requirements) uses ``mat_type``/ + ``mat_index`` for the required material, where the ``(0, -1)`` inorganic + wildcard is common. +* Historical-figure materials appear in things like body part items of + named creatures. + +Other names for the pair in DF code include ``material``/``matgloss`` +(items, improvements, contaminants), ``use_mat``/``use_matg`` (plant +material definitions), ``itemneeded_material``/``itemneeded_matgloss`` +(job items), ``dye_material``/``dye_matgloss`` (dyed improvements), +``hardens_with_water_mat``/``hardens_with_water_matg``, and +``syndrome_mat``/``syndrome_matg`` (syndrome tables). + +Working with the encoding in DFHack +=================================== + +``DFHack::MaterialInfo`` (exposed to Lua as ``dfhack.matinfo``) is the +canonical decoder for a pair. Constructing one via ``decode(type, index)``, +``decode(item)``, ``decode(material_vec_ref, idx)``, or +``decode(some_struct_pointer)`` fills in: + +* ``type``/``index``: the raw pair. +* ``material``: the resolved ``df::material`` pointer (``isValid()`` is + false when it is null). +* ``mode``: one of ``None``, ``Builtin``, ``Inorganic``, ``Creature``, or + ``Plant`` -- note that historical figure materials report ``Creature``, + distinguished by a non-null ``figure``. +* ``subtype``: the position within the creature/plant material vector. +* ``inorganic``, ``creature``, ``plant``: the resolved raw object for the + matching mode. +* ``figure``: the ``df::historical_figure`` for ``HIST_FIG_*`` pairs. + +``MaterialInfo::find`` and ``dfhack.matinfo.find`` also parse the +string-token forms produced by ``getToken()``: +``INORGANIC:`` (e.g. ``INORGANIC:IRON``), +``CREATURE::`` (e.g. ``CREATURE:DWARF:BONE``), +``PLANT::``, plain ```` for built-ins, and the +special cases ``COAL:COKE``/``COAL:CHARCOAL`` and ``NONE``. + +For plain integer pairs, ``DFHack::t_matpair`` and +``DFHack::t_matglossPair`` are simple ``{mat_type, mat_index}`` / +``{type, index}`` bundles, and ``isAnyInorganic()``/``isInorganicWildcard()`` +test for ``mat_type == 0`` and the ``(0, -1)`` wildcard respectively. + +See the `lua-api` documentation for the Lua-side ``dfhack.matinfo`` +interface. diff --git a/docs/dev/compile/Compile.rst b/docs/dev/compile/Compile.rst index 3eea95d880..2ad2c52efb 100644 --- a/docs/dev/compile/Compile.rst +++ b/docs/dev/compile/Compile.rst @@ -442,3 +442,14 @@ a command starting with ``cmake .. -G Ninja`` on Linux and macOS, following the instructions in the sections above. CMake should automatically locate files that you placed in ``CMake/downloads``, and use them instead of attempting to download them. + +In addition, some packages used by DFHack are managed using CMake's ``FetchContent`` +feature, which requires an online connection during builds. The simplest way to address +this is to have a connection during the first build (during which CMake will download the +dependencies), and then to use CMake's ``FETCHCONTENT_FULLY_DISCONNECTED`` or +``FETCHCONTENT_UPDATES_DISCONNECTED`` defines to control how CMake manages cached +dependencies. If you need even the first-time build be an offline build, you will need +to provide a CMake dependency provider. We do not provide one, but CMake's own documentation +includes a simple provider. For more information about CMake's ``FetchContent`` feature +and how to use it in offline builds, see the +`CMake documentation `_. diff --git a/docs/dev/compile/Options.rst b/docs/dev/compile/Options.rst index b314e9db27..462fd1f1ca 100644 --- a/docs/dev/compile/Options.rst +++ b/docs/dev/compile/Options.rst @@ -136,16 +136,26 @@ Usage:: Documentation ============= -If you need to build documentation. Documentation can be built as HTML, and PDF, -but there are also plain text files generated for in-game. +If you need to build `documentation `. -Variable: ``BUILD_DOCS`` +.. note:: + + These options are primarily useful for verifying that the end-to-end process + for building and packaging the documentation is working as expected. For + iterating on documentation changes, `faster alternatives ` are + available. + +Variables: + +* ``BUILD_DOCS``: enables the default documentation build +* ``BUILD_DOCS_NO_HTML``: disables the HTML documentation build (only builds the text documentation used in-game) Usage:: cmake .. -DBUILD_DOCS:bool=ON cmake .. -DBUILD_DOCS=1 - + cmake .. -DBUILD_DOCS_NO_HTML:bool=ON + cmake .. -DBUILD_DOCS_NO_HTML=1 The generated documentation is stored in ``docs/html`` and ``docs/text`` (respectively) in the root DFHack folder, and they will both be installed to ``hack/docs`` when you diff --git a/docs/dev/data-identity.rst b/docs/dev/data-identity.rst index 855a6db26c..bfca2388e4 100644 --- a/docs/dev/data-identity.rst +++ b/docs/dev/data-identity.rst @@ -17,6 +17,13 @@ contains both a pointer to the C++ object itself and a pointer to a ``type_ident by that pointer. Note that the userdata object does not own the objects pointed to by these pointers, and the Lua engine is never responsible for managing their lifetimes. +Most identities are instances of the single template class ``type_identity_for``, which wraps the C++ type ``T``. +``type_identity_for`` derives from a runtime base class selected at compile time by inspection of ``T`` +(via ``std::is_enum``, ``std::is_pointer``, ``std::is_arithmetic``, container traits, and similar checks), +and implements the applicable virtual methods with ``if constexpr`` dispatch on the properties of ``T``. +A type can override the selection by defining a ``df_identity_base`` typedef naming the desired base; +the code generator emits this typedef for all generated compound types. + ``type_identity`` defines the following public methods: - ``byte_size``: returns the size, in bytes, of the object held @@ -33,7 +40,7 @@ never responsible for managing their lifetimes. - ``is_primitive``: indicates that ``lua_read`` will store a *copy* of the object on the Lua stack instead of a non-owning reference to it. Used for types that have direct representations in Lua: numbers, booleans, simple strings -- ``is_constructed``: indicates that creating a C++ instance of this type requires the use of a possibly nontrivial constructor. A type identity that is both primitive and constructed cannot be inserted into a container. At the moment the only type identity that is both primitive and constructed is ``stl_string_identity``, which wraps the C++ ``std::string`` type. +- ``is_constructed``: indicates that creating a C++ instance of this type requires the use of a possibly nontrivial constructor. A type identity that is both primitive and constructed cannot be inserted into a container. At the moment the only type identity that is both primitive and constructed is ``type_identity_for``, which wraps the C++ ``std::string`` type. - ``is_container``: indicates that the type is a container and thus implements the methods specific to ``container_identity`` @@ -41,7 +48,9 @@ never responsible for managing their lifetimes. - ``copy``: copy the object at ``src`` onto ``tgt``. This uses ``memmove`` for primitive types, and C++ copy-assignment (when possible) for other types -There are plethora of subclasses of ``type_identity``: +The identity class tree is divided into two layers: the *runtime base classes*, which are ordinary +(non-template) classes used for polymorphic dispatch, and the single leaf template ``type_identity_for``, +which derives from the appropriate runtime base for the C++ type ``T`` that it wraps: * ``type_identity`` the abstract base class of all type identities @@ -55,15 +64,13 @@ There are plethora of subclasses of ``type_identity``: * ``struct_identity`` C++ ``class`` or ``structure`` - * ``global_identity`` holds, as a quasiobject, handles for all of the known Dwarf Fortress program-scope static objects as if they were fields of an object called ``global`` - * ``union_identity`` C++ ``union`` * ``other_vectors_identity`` special-case identity for the categorized subvectors of objects that appears in many of Dwarf Fortress's "handler" classes * ``virtual_identity`` polymorphic C++ ``class`` or ``structure`` having a virtual table to handle virtual dispatch - * ``stl_string_identity`` ``std::string`` + * ``global_identity_base`` special-case identity for the global quasiobject * ``xlsx_file_handle_identity`` special case @@ -71,70 +78,48 @@ There are plethora of subclasses of ``type_identity``: * ``container_identity`` "containers" generally. note that all container types are homogeneous (that is, the elements of the container must all be of the same type). abstract base class - * ``bit_container_identity`` for containers that contain bools stored one element per *bit* (rather than per byte) - - * ``bit_array_identity`` Dwarf Fortress's ``BitArray`` type - - * ``stl_bit_vector_identity`` ``std::vector`` + * ``bit_container_identity`` for containers that contain bools stored one element per *bit* (rather than per byte); also provides the ``get_item``/``set_item`` virtual interface * ``buffer_container_identity`` C++ static arrays and raw C++ pointers acting as arrays of unspecified bound - * ``enum_list_attr_identity`` (template) metaobject with metadata about a C++ enumeration; may also include additional metadata - * ``ptr_container_identity`` containers that contain pointers - * ``stl_ptr_container_identity`` containers that are of the form ``std::vector`` for some ``T`` - - * ``ro_stl_container_identity`` (template) "read only containers" - - * ``ro_stl_assoc_container_identity`` (template) ``std::map`` and ``std::unordered_map`` - - * ``stl_container_identity`` (template) ``std::vector`` where ``T`` is *not* a pointer (and not ``bool``) - * ``opaque_identity`` opaque wrapper around any type, provides no functionality - * ``stl_string_identity`` ``std::string`` - * ``function_identity_base`` abstract base class for ``function_identity`` * ``function_identity`` (template) wrapper around a C++ function that can be invoked from Lua - * ``primitive_identity`` wrapper around a primitive type. primitive types are fixed-length objects with no internal structure - - * ``bool_identity`` ``bool`` - - * ``number_identity_base`` abstract base for numeric types - - * ``float_identity_base`` abstract base for floating point types - - * ``float_identity`` (template) ``double`` and ``float`` - - * ``integer_identity_base`` abstract base for integral types + * ``primitive_identity_base`` abstract base class for primitive types. primitive types are fixed-length objects with no internal structure - * ``integer_identity`` (template) ``int8_t``, ``int16_t``, ``int32_t``, ``size_t``, etc. lots of these + * ``number_identity_base`` abstract base for numeric types (and ``bool``); provides the ``isInteger`` discriminator - * ``pointer_identity`` any arbitrary C++ pointer (other than ``char*``) + * ``pointer_identity_base`` abstract base class for pointer identities; provides ``getTarget`` - * ``ptr_string_identity`` C-style (``char *``) string +* ``type_identity_for`` the leaf template that provides the identity of the C++ type ``T``. Its runtime + base class is selected by compile-time inspection of ``T`` (or by a ``df_identity_base`` typedef in ``T``), + covering all of the categories above: enums, bitfields, structs, unions, virtual classes, numbers, + pointers, C strings, ``std::string``, ``std::filesystem::path``, static arrays, containers, and opaque + types. Container categories are detected by capability rather than by type name: any type that provides + index-addressable elements (``t[i]`` yielding a ``T&``), bit-element access (a bool proxy or + ``is_set``/``set`` accessors), mapped iteration (``mapped_type`` plus ``begin``/``end``), read-only + iteration, or a ``{ size, items[] }`` table (``enum_list_attr``) is treated as the corresponding kind of + container without being listed explicitly. -Types marked with "(template)" are C++ template types, all parameterized by a single typename. + ``global_identity`` is an alias for ``type_identity_for``, where ``global_object`` is an + empty placeholder type: it holds, as a quasiobject, handles for all of the known Dwarf Fortress + program-scope static objects as if they were fields of an object called ``global``. Type identity object lifetime and mutability ============================================ *Most* instances of ``type_identity`` are statically constructed and immutable and are thus ``const static`` when constructed. -All ``type_identity`` pointers should be declared ``const``. -Due to the use of lazy initialization, ``compound_identity`` and its subclasses have a few fields that are marked as ``mutable`` -(so that the ``parent`` and ``child`` members can be updated as additional instances are constructed). -In addition, ``virtual_identity`` has two fields that are ``mutable`` due to the dual use of this type to implement -DFHack's vmethod interpose system. -Due to this dual purpose, it is important that there be at most one ``virtual_identity`` object per virtual class, -although this is not enforced at present. +All ``type_identity`` pointers should be declared ``const``. Due to ``virtual_identity``'s role in implementing +DFHack's vmethod interpose system, it is important that there be at most one ``virtual_identity`` object per virtual class. Having more than one ``struct_identity`` object for the same type might also potentially lead to misoperation. In general, there should be a one to one correspondence between ``type_identity`` objects and C++ types -(with the special case that ``global_identity`` has no corresponding type). -As far as we know, for any type other than ``virtual_identity``, +(with the special case that ``global_identity`` has no corresponding type). As far as we know, for any type other than ``virtual_identity``, violations of this constraint will not lead to misoperation, but this constraint should not be lightly violated. The Lua/C++ interface does, in a handful of places, assume that it can compare ``type_identity`` pointers to determine if they reference the same type, but as far as we know all of these instances will fall diff --git a/docs/dev/g_src-notes.rst b/docs/dev/g_src-notes.rst new file mode 100644 index 0000000000..05de2a62e2 --- /dev/null +++ b/docs/dev/g_src-notes.rst @@ -0,0 +1,804 @@ +.. _g_src-notes: + +######################################### +Notes on the Bay12 ``g_src`` sources +######################################### + +This document describes the open-source portion of Dwarf Fortress that Bay12 +ships in its Linux packages (``g_src``), and how that code maps onto the +symbols and types defined in `df-structures`_ (the ``df.g_src.*.xml`` files and +related globals such as ``enabler``, ``gps``, ``gview``, ``init``, +``musicsound``). + +.. _df-structures: https://github.com/DFHack/df-structures + +Bay12 releases this code so that Linux packagers can rebuild the parts of DF +that link against distribution-specific libraries (SDL2, OpenGL, curses, etc.). + +The reference tree used here is the ``g_src`` folder from the Linux Classic build of DF 53.16 +as available for `download from Bay12 `__. Line numbers are approximate. + +.. contents:: Contents + :local: + +Source inventory and XML correspondence +======================================= + +.. list-table:: + :header-rows: 1 + + * - g_src file + - Role + - df-structures file + * - ``enabler.h``, ``enabler.cpp`` + - ``enablerst`` — SDL main loop, frame scheduler, thread coordination, + ``renderer`` base class, ``textures``, ``Either``, ``zoom_commands``, + ``texture_fullid`` + - ``df.g_src.enabler.xml`` + * - ``enabler_input.h``, ``enabler_input.cpp`` + - ``enabler_inputst`` (base of ``enablerst``), key translation, repeat + timing, macros + - ``df.g_src.enabler.xml`` (parts), ``df.g_src.keybindings.xml`` + * - ``graphics.h``, ``graphics.cpp`` + - ``graphicst`` (``gps``), ``graphic_viewportst``, + ``graphic_map_portst``, ``interface_setst``, ``texblitst``, + screen/tile buffers + - ``df.g_src.graphics.xml`` + * - ``renderer_2d.hpp`` + - ``renderer_2d_base``, ``renderer_2d`` — SDL_Renderer based tile + renderer, texture cache + - ``df.g_src.renderer_2d.xml`` + * - ``renderer_curses.cpp`` + - ``renderer_curses`` (PRINT_MODE:TEXT) + - ``df.g_src.renderer_2d.xml`` + * - ``renderer_offscreen.cpp`` + - offscreen renderer used for map renders (in olders releases, functionality currently not available) + - ``df.g_src.renderer_2d.xml`` + * - ``renderer_opengl.hpp`` + - legacy OpenGL renderer (not compiled — ``#include`` commented out in + ``enabler.cpp:194``) + - n/a + * - ``interface.h``, ``interface.cpp`` + - ``interfacest`` (``gview``), scrolling/string-entry helpers + - ``df.g_src.interface.xml``, ``df.interface.xml`` + * - ``ViewBase.h``, ``ViewBase.cpp`` + - ``viewscreenst``, ``abstract_interfacest``, ``widgets::*`` widget tree + - ``df.g_src.ViewBase.xml`` + * - ``keybindings.h``, ``keybindings.cpp`` + - ``InterfaceKey`` enum, key binding names bimaps + - ``df.g_src.keybindings.xml`` + * - ``KeybindingScreen.*`` + - keybinding-editing UI (viewscreen) + - ``df.g_src.KeybindingScreen.xml`` + * - ``init.h``, ``init.cpp`` + - ``initst`` (``init``) — init.txt/colors.txt parsing, font texpos + tables + - ``df.g_src.init.xml`` + * - ``files.h``, ``files.cpp``, ``find_files.h`` + - ``filest``, ``stringvectst``, path handling, compressed file I/O + - ``df.g_src.files.xml``, ``df.g_src.basics.xml`` + * - ``basics.h``, ``basics.cpp`` + - misc helpers, ``gamelog``/``errorlog``, ``flagarrayst`` + - ``df.g_src.basics.xml`` + * - ``texture_handler.h``, ``textures.cpp`` + - ``texture_handlerst``, ``palettest``, tile-page loading + - ``df.g_src.texture_handler.xml`` + * - ``platform.h``, ``win32_compat.cpp`` + - Win32 API shims for Linux (``GetTickCount``, ``MessageBox``, + ``DWORD``, ...) + - ``df.g_src.platform.xml`` + * - ``mail.hpp`` + - ``Chan`` channel, ``Either`` + - modeled inline in ``df.g_src.enabler.xml`` + * - ``music_and_sound_g.h``, ``music_and_sound*.cpp`` + - ``musicsoundst`` (``musicsound``), ``Song``/``MusicEvent``/ + ``MusicContext`` enums + - ``df.g_src.music_and_sound_g.xml`` + * - ``random.h``, ``random.cpp`` + - Mersenne Twister wrapper, ``trandom*``, seed stack + - ``df.random_objects.xml`` + * - ``command_line.h``, ``command_line.cpp`` + - ``command_linest`` + - ``df.g_src.command_line.xml`` + * - ``lua.h``, ``lua.cpp`` + - ``LuaState`` RAII wrapper, DF Lua globals + - not mapped (see "Other modules" below) + * - ``textlines.h`` + - ``textlinesst`` raw-file line loader + - ``df.g_src.basics.xml`` + * - ``svector.h``, ``bimap.h``, ``pool.h``, ``array_vec.hpp``, + ``template.h``, ``endian.h``, ``resize++.h`` + - internal utility containers/algorithms + - mostly not mapped (STL-internal) + * - ``dfhooks.h``, ``dfhooks.cpp`` + - the DF ↔ DFHack plugin boundary + - n/a — see "DFHack hook placement" below + +Process architecture — the two threads +====================================== + +``enabler.cpp`` implements a two-thread design. Understanding which thread +runs what is the single most important thing about ``g_src`` for DFHack. + +Boot sequence (``main``, ``enabler.cpp:1270``) +---------------------------------------------- + +1. ``SDL_Init(SDL_INIT_TIMER)`` on the **main thread**. +2. ``enabler.renderer_threadid = SDL_ThreadID()`` — the **main thread is the + renderer/event thread** (``enabler.cpp:1282``). +3. ``SDL_CreateThread(call_loop, ...)`` spawns the **simulation thread**, + which runs ``enablerst::async_loop()`` (``enabler.cpp:1265-1268``, + ``746``). +4. ``init.begin()`` parses ``init_default.txt``, ``prefs/init.txt``, + ``colors.txt`` into ``init``/``gps`` (``init.cpp:93``). +5. ``SDL_InitSubSystem(SDL_INIT_VIDEO)``, ``keybinding_init()``, keybindings + loaded (``prefs/interface.txt`` then ``data/init/interface.txt`` — prefs + first because ``load_keybindings`` does not overwrite). +6. ``enabler.loop(cmdline)`` → ``beginroutine()`` (closed source) → renderer + creation (``renderer_2d`` unless PRINT_MODE:TEXT → ``renderer_curses``) → + ``hooks_init()`` → ``eventLoop_SDL()`` (``enabler.cpp:1073-1125``). + +So: the thread that runs ``main()`` owns SDL events, the SDL window/renderer, +and presentation. The created thread owns the simulation and the +screen-buffer writes. + +The channels (``mail.hpp``, ``enabler.h:891-899``) +-------------------------------------------------- + +Three ``Chan`` queues plus one semaphore, all members of ``enablerst``: + +- ``async_tobox : Chan`` — **main → sim** commands: ``pause``, + ``start``, ``render``, ``inc`` (grant N sim frames), ``set_fps``. +- ``async_frombox : Chan`` — **sim → main** messages: ``quit``, + ``complete``, ``set_fps``, ``set_gfps``, ``push_resize``, ``pop_resize``, + ``reset_textures``, ``show_message``. +- ``async_zoom : Chan`` — **sim → main** zoom/fullscreen + requests (``zoom_display()`` writes here; the event loop drains it). +- ``async_fromcomplete : std::binary_semaphore`` — released by the main + thread after it has serviced an ``async_frombox`` request; callers block + on it (see ``override_grid_size``, ``set_fps``, ``set_gfps``, + ``show_message_box``). + +``async_msg`` uses a ``union`` for payloads: ``fps``, ``{x,y}``, or +``{text, caption, type}`` — the wide-string pointers for ``show_message`` +are raw borrowed pointers whose lifetime is guarded by the caller blocking +on ``async_fromcomplete`` until the main thread has shown the box +(``enabler.cpp:733-738``; ``enabler.h`` ``show_message_box``). + +The sim thread — ``enablerst::async_loop`` (``enabler.cpp:746``) +---------------------------------------------------------------- + +The sim thread spins: + +1. Drain ``async_tobox`` (blocking ``read`` when paused or out of frame + budget, ``try_read`` otherwise): + + - ``pause`` → set ``async_paused``, ack with ``async_msg::complete``. + - ``start`` → unpause, reset ``async_frames``. + - ``render`` → if ``ENABLERFLAG_RENDER``: ``renderer->swap_arrays()``, + ``render_things()`` (fills ``gps`` buffers), clear flag, + ``update_gfps()``; then ack ``complete``. **This is the screen-render + tick, on the sim thread.** + - ``inc`` → add to ``async_frames`` budget (clamped to ``fps*3``). + - ``set_fps`` → update local fps. + +2. If unpaused and budget remains (or ``ENABLERFLAG_MAXFPS``): call + ``mainloop()`` — the closed-source simulation + UI step — then + ``simticks++``, ``clear_text_input()``. +3. ``hooks_update()`` **every iteration**, on the sim thread + (``enabler.cpp:812``). This is where ``DFHack::Core::Update()`` runs. + +If ``mainloop()`` returns true, it posts ``async_msg::quit`` and exits; the +main thread sees ``quit`` in ``async_wait``, sets ``loopvar=0``, and both +loops end. + +The main thread — ``eventLoop_SDL`` + ``do_frame`` (``enabler.cpp:892``, ``816``) +--------------------------------------------------------------------------------- + +Each iteration: + +1. Drain ``async_zoom``; if any zooms arrive, **pause the sim first** + (``pause_async_loop`` writes ``pause`` to ``async_tobox`` and blocks in + ``async_wait`` until ``complete``). +2. ``SDL_PollEvent`` loop — again, the first event triggers + ``pause_async_loop``, so **all input processing happens while the sim + thread is parked**. For each event: ``hooks_sdl_event(&event)`` (main + thread, can consume the event entirely), then the switch: keys/wheel/ + quit → ``enabler.add_input`` (input state machine); buttons → + ``enabler.mouse_*``; window events → resize/cursor-focus/ + ``force_full_display_count``/``ENABLERFLAG_RENDER``. +3. Mouse state → ``gps.precise_mouse_x/y``, ``gps.mouse_x/y``, + ``enabler.tracking_on`` (written while sim is paused). +4. ``unpause_async_loop()``. +5. ``hooks_sdl_loop_fn()`` — main thread, just before ``do_frame``. +6. ``do_frame()``: + + - accumulate wall-clock time into ``outstanding_frames`` (sim) and + ``outstanding_gframes`` (graphics), capped at 3 gframes; + - ``inc`` command grants that many sim ticks; + - each gframe: send ``render``, ``async_wait()`` for ``complete`` + (servicing ``set_fps``/``set_gfps``/``push_resize``/``pop_resize``/ + ``reset_textures``/``show_message`` while it waits), then + ``gps.main_thread_requesting_reshape`` handling, + ``tidy_tile_cache()``, ``display()``, ``render()`` → + ``SDL_RenderPresent``. + - ``emit_logs()`` when it sleeps. + +7. ``musicsound.update()``. + +What the pause protocol actually protects +----------------------------------------- + +``pause_async_loop`` only stops ``mainloop()`` and ``render_things`` — it +does **not** lock ``gps``. The buffers are protected by *protocol*, not +mutexes: + +- ``gps.screen``/``screentexpos*`` (the "front" buffers) are written by the + sim thread inside the ``render`` command handler, *before* it posts + ``complete``. +- ``renderer::screen``/``screentexpos*`` (the "back" buffers) are read by + the main thread in ``display()``, *after* ``complete`` arrives. +- ``renderer::swap_arrays()`` (``enabler.cpp:513``) exchanges the + front/back pointer sets, so the sim thread always draws into the buffer + the renderer is *not* currently scanning. The swap runs on the sim + thread, inside the ``render`` command, and completes before ``complete`` + is acked — so there is a happens-before edge via the channel. +- The per-viewport ``screentexpos_*_old`` arrays are likewise rotated in + ``swap_arrays`` so ``display()`` can diff them against the fresh arrays + for incremental redraws (``enabler.cpp:264-398``). + +Things that are **not** safe by protocol (see "Problematic code" below): +``enabler.flag`` (``ENABLERFLAG_RENDER`` written by main, read by sim), +``enabler.async_paused`` (written by ``set_fps`` on whichever thread calls +it), ``simticks``/``gputicks``/``async_frames`` (plain ints read +cross-thread), ``gps.mouse_x``/``mouse_y`` (written by main while +"paused", read by sim at arbitrary times from viewscreen code — races are +possible because the sim thread resumes after ``unpause_async_loop``), and +input statics in ``enabler_input.cpp`` (``timeline``, ``pressed_keys``, +``modState``). + +DFHack hook placement (``dfhooks.cpp``) +--------------------------------------- + +``g_src/dfhooks.cpp`` is a tiny trampoline: ``hooks_init()`` ``dlopen``\ s +``./libdfhooks.so`` (or ``dfhooks.dll``/``libdfhooks.dylib``) and resolves +``dfhooks_init``, ``dfhooks_shutdown``, ``dfhooks_update``, +``dfhooks_prerender``, ``dfhooks_sdl_event``, ``dfhooks_ncurses_key``, +``dfhooks_sdl_loop`` by name. + +``libdfhooks`` itself is the **chainloader** (built from +``depends/dfhooks`` in the DFHack repo): it discovers ``dfhooks_*``/ +``libdfhooks_*.so`` libraries, reads each one's optional +``dfhooks_priority`` int32 (lower runs earlier; DFHack exports 100), calls +each library's optional ``dfhooks_preinit`` at load time, and fans the +other callbacks out in priority order (shutdown in reverse). Note: despite +DFHack's comment in ``library/Hooks.cpp``, ``dfhooks_init``/ +``dfhooks_preinit`` run *after* the sim thread is spawned — +``SDL_CreateThread`` precedes ``enabler.loop`` → ``hooks_init`` in +``main`` (``enabler.cpp:1289``, ``1119``). + +.. list-table:: + :header-rows: 1 + + * - g_src call site + - Exported symbol + - Runs on + - DFHack endpoint (``library/Hooks.cpp``) + * - ``enabler.loop``, after renderer creation, before event loop + - ``dfhooks_init`` + - main + - ``Core::InitMainThread`` + console/command-line checks + * - after ``eventLoop_SDL`` exits + - ``dfhooks_shutdown`` + - main + - ``Core::Shutdown`` + * - every ``async_loop`` iteration + - ``dfhooks_update`` + - **sim** + - ``Core::Update`` (plugin ``onupdate``, ``Gui::*``, etc.) + * - ``render_things``, before top viewscreen ``render()`` + - ``dfhooks_prerender`` + - **sim** + - (currently a stub) + * - every SDL event + - ``dfhooks_sdl_event`` + - **main** + - ``Core::DFH_SDL_Event`` → SDL events for tools + * - every event-loop iteration, before ``do_frame`` + - ``dfhooks_sdl_loop`` + - **main** + - ``Core::DFH_SDL_Loop`` (mouse/display side work) + * - every ncurses key (PRINT_MODE:TEXT only) + - ``dfhooks_ncurses_key`` + - main + - ``Core::DFH_ncurses_key`` + +Practical consequences: + +- Code in ``dfhooks_update`` runs on the sim thread, concurrent with the + main thread's ``display()`` — it must not touch ``renderer.*`` buffers + or SDL. ``CoreSuspender`` exists because even *this* does not stop the + main thread from scanning buffers; it suspends via the DFHack-managed + mutex. +- ``dfhooks_sdl_event``/``dfhooks_sdl_loop`` run while the sim thread is + paused **only for the duration of the event drain** — ``unpause`` + happens before ``hooks_sdl_loop_fn``/``do_frame``, so + ``dfhooks_sdl_loop`` runs concurrent with the sim thread. It is safe for + SDL calls and for reading ``gps`` display-side state, not for mutating + simulation data. +- A ``dfhooks_sdl_event`` handler that consumes an event stops DF's own + processing of it entirely (the ``continue`` at ``enabler.cpp:932``). + +``enablerst`` structure notes (``enabler.h:840``, ``df.g_src.enabler.xml``) +=========================================================================== + +``enablerst`` publicly inherits ``enabler_inputst`` — so +``enabler.add_input``, ``enabler.get_input``, keybinding and macro methods +all live on ``enabler``. + +Field-by-field: + +- ``renderer`` (``renderer*``) — the active renderer; ``renderer_2d`` in + normal SDL builds. Virtual interface in ``enabler.h:733-852``; XML + models all vmethods (``df.g_src.enabler.xml`` ````). +- Timing: ``fps``/``gfps`` are **floats** (target rates), + ``outstanding_frames``/``outstanding_gframes`` are float accumulators + drained by ``do_frame``. ``frame_timings``/``gframe_timings`` + sums + implement the 50-frame rolling average behind + ``calculate_fps``/``calculate_gfps`` (``enabler.cpp:1185-1213``). +- ``simticks``, ``gputicks`` — plain ``int``, incremented on sim and main + threads respectively; also read by the *other* thread (e.g. + ``Event::tick`` in input code) → benign-looking but technically racy. +- ``flag`` (``ENABLERFLAG_*``): ``RENDER`` = "a gframe is due; sim should + run ``render_things``"; ``MAXFPS`` = uncapped sim; ``BASIC_TEXT`` = use + the basic (non-antialiased) font texposes. +- Input: ``mouse_lbut``/``mouse_rbut``/``mouse_mbut`` (+``_down``/ + ``_lift`` edge flags), ``tracking_on`` (mouse inside a valid grid + cell), ``listening_to_text``/``last_text_input`` (32-byte UTF-8 + text-input buffer, filled by ``SDL_TEXTINPUT`` on the main thread, + cleared by sim thread each tick, read via ``get_text_input()`` by + viewscreens that want text). +- ``clock`` — approx. ``SDL_GetTicks`` refreshed per ``do_frame``; cheap + tick for GC-ish logic. +- ``command_line`` — the reconstructed command line string; the actual + ``command_linest`` is ``game->command_line`` (``df.game_g.xml`` / + ``df.g_src.command_line.xml``). +- ``fullscreen_state`` (``FullscreenState`` bitfield), + ``overridden_grid_sizes`` (deque used by + ``override_grid_size``/``release_grid_size``; note the FIXME at + ``enabler.cpp:1157`` about unmatched release calls). +- ``last_message_result`` — ``std::atomic_int``; result of the last + ``MessageBox`` call, set on the main thread in + ``win32_compat.cpp:142``. +- ``must_do_render_things_before_display`` — when a viewport reshape is + pending (``gps.main_thread_requesting_reshape``), ``render_things`` + leaves this true so ``do_frame`` performs the reshape on the main + thread before the next ``display()`` (``enabler.cpp:849-858``, + ``1253``). +- ``textures`` (``enabler_textures``/``textures`` class, + ``enabler.h:557``) — raw ``SDL_Surface`` catalog backing ``texpos`` + values: ``raws`` vector, ``free_spaces``, ``init_texture_size``, + ``uploaded`` flag. ``add_texture``, ``clone_texture``, + ``grayscale_texture``, ``delete_all_post_init_textures`` etc. in + ``textures.cpp``. +- Methods of interest: ``set_fps``/``set_gfps`` (thread-agnostic — they + forward via ``async_frombox`` when called off the renderer thread), + ``override_grid_size``/``release_grid_size``, ``zoom_display``, + ``show_message_box``, ``clean_cached_tile``, + ``get_current_interface_tile_dims``, ``main_renderer()``. + +``graphicst`` and the tile pipeline (``graphics.h:5436``, ``df.g_src.graphics.xml``) +==================================================================================== + +``gps`` is the screen-state singleton. Everything a viewscreen draws lands +in its arrays. + +The screen cell +--------------- + +- Indexing is **column-major**: cell ``(x, y)`` is at ``x*dimy + y`` + (confirmed throughout, e.g. ``addchar``, ``enabler.cpp:38``). +- ``screen`` is ``dimx*dimy*8`` bytes; 8 bytes per cell: ``s[0]`` = + character byte; ``s[1..3]`` = foreground RGB; ``s[4..6]`` = background + RGB; ``s[7]`` = type byte (``GRAPHICSTYPE_TTF``/``_TTFCONT`` — + 0xff/0xfe, currently vestigial since TTF paths are commented out). +- ``screenf``/``screenb``/``screenbright`` + ``use_old_16_colors`` are the + "pen". ``changecolor(f,b,br)`` selects the 16-color palette + (``uccolor[16][3]``); ``color3ub``/``bcolor3ub`` set true RGB + (``screen_color_*`` fields). Both are written into the cell's RGB bytes. +- ``locate(y, x)`` sets the cursor ``screenx``/``screeny`` (note **y + first**); ``addchar`` writes a cell then ``screenx += advance``. + Clipping via ``setclipping``/``clipx``/``clipy``. + +The texpos layer set +-------------------- + +For each cell there is a parallel set of ``long`` texpos arrays: + +- ``screentexpos`` — the base graphical tile (overrides the char when + ``INIT_DISPLAY_FLAG_USE_GRAPHICS``). +- ``screentexpos_lower`` — underlay (beneath text/tiles). +- ``screentexpos_anchored`` + ``_x``/``_y`` — multi-cell anchored tiles + (creatures/buildings bigger than one tile). Subordinate cells get + ``SCREENTEXPOS_FLAG_ANCHOR_SUBORDINATE`` plus packed 6-bit offsets + (``ANCHOR_X_COORD>>6``, ``ANCHOR_Y_COORD>>12``, 64x64 limit, + ``graphics.cpp:983-991``). +- ``screentexpos_flag`` per cell: ``GRAYSCALE``, ``ADDCOLOR``, + ``ANCHOR_SUBORDINATE``, ``TOP_OF_TEXT``, ``BOTTOM_OF_TEXT``, + ``ANCHOR_USE_SCREEN_COLOR`` (``graphics.h:2668-2677``). +- The same five arrays again for the **top layer** (``screen_top``, + ``screentexpos_top*``), composited over the base layer when + ``gps.top_in_use`` (e.g. announcements/tutorial overlays). +- Write order in ``update_all``: background → ``update_tile`` per cell → + ``update_anchor_tile`` → top-layer ``update_top_tile``/ + ``update_top_anchor_tile``. + +``texture_fullid`` (``enabler.h:646``/``671``) = ``{texpos, r,g,b, +br,bg,bb, flag}`` — the SDL renderer's cache key; ``flag`` is +``texture_fullid_flag`` (``TRANSPARENT_BACKGROUND``, ``DO_RECOLOR``, +``CONVERT``). + +Viewports and map ports +----------------------- + +- ``graphic_viewportst`` (``graphics.h:3319``) — a layered representation + of the map portion of the screen: ~23 parallel ``int32_t`` arrays + (``screentexpos``, ``_background``, ``_background_two``, + ``_floor_flag``, ``_liquid_flag``, ``_spatter_flag``, ``_spatter``, + ``_ramp_flag``, ``_shadow_flag``, ``_building_one``/``_two``, + ``_top_shadow``, ``_item``, ``_vehicle``, ``_projectile``, + ``_high_flow``, ``_vermin``, ``_signpost``, five ``*_creature`` + directions, ``_designation``, ``_interface``) plus a matching ``*_old`` + set for diffing. ``screen_x``/``screen_y`` are its private cursor; + ``clipx``/``clipy`` clip writes; ``flag`` has + ``GRAPHIC_VIEWPORT_FLAG_ACTIVE``. +- ``gps.main_viewport`` + ``gps.lower_viewport[LOWER_VIEWPORT_MAX]`` (=8) + — the multilevel z-view: lower viewports draw beneath the main one. + ``multilevel_depth_texpos[8]`` picks the depth shading tiles. +- ``graphic_map_portst`` (world-map viewports): base + 8 ``edge`` + 8 + ``edge2`` + detail/tunnel/river/road/site/army/interface + + ``detail_to_*`` (8 dirs) + ``site_to_s`` + ``cloud_bits`` (u64 bitmask + per cell) — plus ``_old`` twins, ``pixel_perc_x/y``, + ``top_left_corner_*``. +- Resizing flows through ``reshape_viewports(zoom_factor)`` → + ``shape_viewport_according_by_pixel_size`` (32px or + ``zoom_factor*32/128`` per tile; map ports use 16px). + +Per-frame lifecycle +------------------- + +1. Main thread ``do_frame``: ``render`` cmd → sim thread. +2. Sim thread: ``swap_arrays()`` — ``gps.*`` ↔ ``renderer.*`` pointer + exchange for every buffer, including each viewport's ``*_old`` arrays. +3. Sim thread: ``render_things()`` (``graphics.cpp:1199``): + + - walk ``gview.view`` ``child`` chain to topmost viewscreen; + - ``hooks_prerender()``; + - ``currentscreen->render(curtick)`` then + ``currentscreen->widgets.render(curtick)`` — **widgets render last, + deliberately "mostly DFHack"** (comment at line 1218). This is the + ordering DFHack's overlay relies on. + - REC/PLAY/prefix/FPS indicators. + +4. Sim acks ``complete``; main thread ``async_wait`` returns. +5. Main thread ``display()`` (``enabler.cpp:218``): if + ``force_full_display_count>0``, ``do_blank_screen_fill`` + full + viewport/map_port rebuilds; else per-cell diff between + ``screentexpos_*`` and ``*_old`` → ``update_viewport_tile``/ + ``update_map_port_tile`` only for changed cells. Then + ``update_all()`` → ``render()`` → ``SDL_RenderPresent``. + +The SDL renderer (``renderer_2d.hpp``) +-------------------------------------- + +``renderer_2d_base`` implements the ``renderer`` interface with an +``SDL_Renderer``: a ``texture_fullid → SDL_Texture*`` cache, +``update_tile`` / ``update_anchor_tile`` per-cell compositing (reading +``screen`` bytes + ``screentexpos`` + flags into a ``texture_fullid``, +looking it up in the cache, ``SDL_RenderCopy``), ``update_all`` (full +traversal), ``tidy_tile_cache`` (LRU-ish eviction), ``clean_tile_cache`` +(full flush — triggered via ``gps.do_clean_tile_cache``), ``zoom``/ +``resize``/``grid_resize``/``set_viewport_zoom_factor``/ +``set_fullscreen``. + +``screen_to_texid`` (``enabler.cpp:37``) is the canonical cell→texture +translation: char byte → font texpos (basic/large/small, or ``_top``/ +``_bot`` for half-height text rows), or ``screentexpos`` + +GRAYSCALE/ADDCOLOR color mixing. + +``init.font.*_font_texpos[256]`` (``init.h``, ``df.g_src.init.xml``) map +glyph bytes → font-page texposes, derived from ``curses_*.png`` font +sheets by ``prepare_graphics``. + +Input pipeline (``enabler_input.cpp``, ``keybindings.h``) +========================================================= + +SDL event → ``enabler.add_input`` (main thread, sim paused) → +``KeyEvent{release, EventMatch{type, mod, scancode, key|button|y}}`` → +``key_translation`` looks up ``EventMatch`` in the static ``keymap`` +(``multimap``) → ``Event``\ s go into the +``timeline`` (``set`` ordered by ``time`` then ``serial`` — one +physical press = one serial, possibly several ``InterfaceKey``\ s). + +Sim thread consumes via ``enabler_inputst::get_input(now)`` → +``std::set`` for one physical press; repeats are rescheduled +onto the timeline per ``repeatmap`` (``REPEAT_NOT``/``SLOW``/``FAST``, +tunable via ``init.input.hold_time``/``repeat_time``/``repeat_accel_*``). +``REPEAT_MWHEEL`` uses ``repeats`` as a countdown so one wheel notch +generates exactly N events. + +``InterfaceKey`` (``keybindings.h``, ~1800 constants, +``df.g_src.keybindings.xml``) is the binding vocabulary; +``bindingNames``/``displayNames``/``sdlNames``/``sdlUnicode`` bimaps +translate between keys and human strings. Macros record +``set`` lists (``prefs/macros/*.mak``), played back onto the +timeline. + +``interface.h`` adds ``standardscrolling``/``secondaryscrolling``/ +``standardstringentry`` helpers, ``INTERFACEFLAG_RETAIN_NONZERO_INPUT``, +and ``interfacest gview`` — the viewscreen stack root. + +Interface layer (``ViewBase.h``, ``interface.h``) +================================================= + +- ``viewscreenst`` (``ViewBase.h:1050``): ``child``/``parent`` links form + the screen stack (``gview.view`` is the root *member*, not a pointer). + ``breakdownlevel`` (``INTERFACE_BREAKDOWN_*``) requests teardown; + ``feed(set)``, ``logic()``, ``render(curtick)`` are the + three virtuals the loop calls; ``set_port_flags()`` lets a screen claim + viewports. +- ``interfacest::addscreen``/``removescreen`` splice the list + (``INTERFACE_PUSH_*``); ``interfacest::loop`` (``interface.cpp:870``) is + invoked from inside ``mainloop()`` and drives feed/logic/render per + screen (inferred — ``mainloop`` itself is closed source). +- ``viewscreenst::widgets`` (a ``widgets::container``) is the per-screen + widget tree — the DFHack overlay attaches widgets here. Container + types: ``container``, ``widget_stack``, ``rows_container``, + ``columns_container``, ``tabs``, ``scroll_rows``, ``radio_rows``, + ``table``, ``folder``, ``filter``, ``multifilter``, + ``params_container``; display widgets: ``text``, ``text_truncated``, + ``text_multiline``, ``nineslice``, ``anchored_tile``, + ``keybinding_display``, ``graphics_switcher``, ``character``; inputs: + ``interface_*_button``, ``better_button``, ``textbox``, ``dropdown``. + Anchoring is Godot-``Control``-style (``anchor_*`` fractions + + ``offset_*`` + ``LayoutPreset``\ s). + +Other modules DFHack touches +============================ + +- ``initst init`` — all init.txt state; ``init.display.grid_x``/ + ``grid_y`` are the *tile-grid* dimensions; ``init.display.flag`` has + ``USE_GRAPHICS``, ``TEXT``, ``NOT_RESIZABLE``, ``SOFTWARE``; + ``init.input.zoom_speed``, ``pause_zoom_no_interface_ms``. +- ``musicsoundst musicsound`` (``music_and_sound_g.h:422``) — song/ + ambience state machine (``Song``/``MusicEvent``/``MusicContext`` enums, + ``MUSICSOUND_FLAG_*``), up to ``SOUND_CHANNELNUM`` =32 channels; + ``update()`` is called from the main event loop; ``loading_files`` are + async futures. +- ``command_linest`` — ``game->command_line.original``, ``arg_vect``, + ``gen_id``/``world_seed``/``world_param``. +- ``random.h`` — ``trandom(limit)``, + ``push_trandom_uniform_seed``/``pop_trandom_uniform_seed`` (seed stack), + ``trandom_twist``. +- ``filest``/``stringvectst``/``textlinesst`` — path resolution across DF + dirs (``canon_location``/``both_locations``/ + ``with_flags(FILE_FLAG_*)``), manual ``std::string *`` vector (implemented via the ``pstringst`` class), raw-file + loader. +- ``LuaState`` (``lua.h``) — DF embeds Lua for the closed side (worldgen, + utterances); ``add_lua_df_globals`` registers ``trandom``, ``lua_log``, + ``capitalize_string_*``; ``data/init/globals.lua`` is run at state + init. Not mapped in df-structures. +- ``platform.h``/``win32_compat.cpp`` — Win32 names on Linux: + ``GetTickCount`` ≈ ``SDL_GetTicks``, ``MessageBox`` → + ``SDL_ShowMessageBox`` (with cross-thread marshaling via + ``show_message``), ``Sleep``, ``QueryPerformance*``, ``DWORD``, + ``MB_*``, ``VK_*``. Values are locally defined and may differ from real + Windows constants. + +Problematic / outdated / unsafe code +==================================== + +Confirmed defects and latent hazards. Severity tags: **[bug]** real +defect; **[ub]** undefined/unspecified behavior risk; **[fragile]** works +today but easily broken; **[stale]** dead/outdated code. + +Channels and threading +---------------------- + +- **[ub]** ``Chan::read`` waits ``cv.wait(lk)`` with no predicate; a + spurious wakeup (or a wakeup for a message another consumer took) makes + ``vals.front()``/``pop()`` run on an empty queue — UB + (``mail.hpp:34-40``). +- **[ub]** ``Chan::try_read_for_millis`` treats *any* ``wait_for`` return + of ``no_timeout`` as "queue non-empty" and pops unconditionally + (``mail.hpp:23-33``). Also only one consumer can safely read a channel; + ``try_read`` races multiple consumers by design of ``try_to_lock``. +- **[fragile]** ``Chan::write(const T &w)`` calls + ``vals.push(std::move(w))`` — moving from a const ref copies; the API + looks like it moves but doesn't (``mail.hpp:41-45``). +- **[fragile]** ``async_cmd``/``async_msg`` have a union + tag and their + ctors don't initialize the union (``enabler.h:875-899``); a reader using + the wrong member reads garbage. ``async_wait`` dispatches on ``msg`` so + this is currently consistent — but ``async_msg(show_message)`` leaves + ``fps``/``x``/``y`` uninitialized and vice versa. +- **[bug/ub]** ``enabler.flag``, ``enabler.async_paused``, ``simticks``, + ``gputicks``, ``async_frames``, ``gps.mouse_*`` are non-atomic and + cross-thread (e.g. ``flag`` write ``enabler.cpp:1005`` vs read ``784``; + ``async_paused`` write ``enabler.cpp:1219`` vs read ``757``/``800``; + ``simticks`` read on main in ``enabler_input.cpp:703``/``741``). Mostly + benign on x86, formally racy. +- **[fragile]** ``async_msg::show_message`` ships raw ``const wchar_t*`` + cross-thread; lifetime is guarded only by the sender blocking on + ``async_fromcomplete``. If anything drains ``async_frombox`` without + servicing ``show_message``, or releases the semaphore early, UAF. +- **[fragile]** ``release_grid_size`` drains *all* + ``overridden_grid_sizes`` then sends one ``zoom_resetgrid``; the + in-source FIXME notes a known push/pop mismatch ("overriding once per + movie but apparently only releasing for the last one", + ``enabler.cpp:1157``). +- **[stale]** ``enabler_input.cpp:677`` calls ``enabler.simticks.load()`` + — but ``simticks`` is a plain ``int``. This is inside ``#ifdef CURSES``; + the curses input path cannot compile as written. Indicates the curses + port bit-rotted after ``simticks`` stopped being atomic. + +Screen-buffer writes +-------------------- + +- **[bug]** ``addchar_flag(x, y, ...)`` and ``top_addchar_flag(x, y, + ...)`` write ``screentexpos_flag[screenx*dimy + screeny]`` — the + *cursor* cell, not the ``(x,y)`` argument cell (``graphics.h:11183``, + ``11309``). +- **[bug]** ``addchar(x, y, c, f, b, bright)`` (and ``top_addchar``, + ``addchar_flag``, ``top_addchar_flag`` ``(x,y)`` overloads) compute + ``fg = f + (bright ? 8 : 0)`` and then ignore it, indexing + ``uccolor[f]`` — the bright flag is dropped on these overloads + (``graphics.h:11112-11118``). +- **[ub]** the ``(x,y)`` ``addchar``/``top_addchar``/``addchar_flag``/ + ``top_addchar_flag`` overloads index ``uccolor[f]``/``uccolor[b]`` with + raw u8 parameters — any color index >15 is an out-of-bounds read + (``graphics.h:11113-11118`` etc.). +- **[fragile]** ``locate``+``addchar`` bounds-safety relies on the clip + rect: the pointer check ``s < screen_limit`` has no lower bound in the + cursor-based overloads; a negative ``screenx`` passes the check and is + only saved because ``clipx[0] >= 0``. Calling ``setclipping`` with + negative values breaks that invariant. +- **[bug]** ``gps_locator::is_free`` reads ``gps.screen[x*dimy*8 + + y*8]`` with no bounds check at all (``graphics.h:11433+``). + Over/underflow reads if called with out-of-range coordinates. +- **[fragile]** ``graphicst::dim_colors``, ``rain/snow/color_square`` + operate on the 8-byte cell layout with magic offsets; several are only + correct for the 16-color palette path. +- **[stale]** ``graphicst::gray_out_rect`` is fully commented out with a + note that ``*4`` should be ``*8`` — the 4-byte cell era ended long ago + (``graphics.h:11351-11367``). The TTF path in ``addst``/``top_addst`` + (``GRAPHICSTYPE_TTF``, ``ttf_manager``, ``ttfstr``) is similarly dead — + commented out wholesale (``graphics.cpp:482-530``, ``562-610``). + +Renderer +-------- + +- **[fragile]** ``renderer_2d_base::update_all`` uses hard-coded display + numbers (1920/1080/907) and ``SDL_GetDisplayBounds(0, ...)`` on display + 0 for title-screen/background sizing (``renderer_2d.hpp`` + ``update_all``). +- **[fragile]** ``renderer_2d_base::zoom``: ``case zoom_reset:`` sets + ``zoom_steps = 0`` then **falls through** into ``zoom_resetgrid`` → + ``compute_forced_zoom()`` (``renderer_2d.hpp:4478-4481``). Probably + intended — reset implies recompute — but there is no + ``[[fallthrough]]`` or comment. +- **[bug]** ``renderer_2d_base`` zoom/reshape math has in-source FIXMEs: + "perc introduces some error so at certain zooms it is way off (the -16 + is a cludge)" (``graphics.cpp:306-311``) — map-port origin math is + approximate and known-wrong at some zoom levels. +- **[fragile]** ``clean_cached_tile``/``clean_tile_cache``/ + ``tidy_tile_cache`` call ``SDL_DestroyTexture`` on cache entries; + entries can be null — tolerated by SDL2 but unchecked. +- **[stale]** ``renderer`` declares ``directtexcopy``/ + ``directtexcopy_old`` buffers that ``gps_allocate`` never allocates — + dead fields still mapped in ``df.g_src.enabler.xml`` + (``enabler.h:750``/``772``). +- **[stale]** ``renderer_opengl.hpp`` is ``#include``\ d out + (``enabler.cpp:194``) and ``loop()`` has a block of commented + ``renderer_opengl``/``partial``/… selection logic + (``enabler.cpp:1101-1116``); ``renderer_curses`` is ``#ifdef CURSES`` + -only. ``ttf_manager`` exists but its users are commented out. +- **[fragile]** ``renderer_2d`` ctor exits the process on init failure + (window/renderer creation) rather than reporting. + +Input +----- + +- **[fragile]** ``add_input`` synthesizes release/re-press ``KeyEvent``\ s + for all pressed keys on every modifier change + (``enabler_input.cpp:537-555``) — quirky by design; DFH code that + injects ``KeyEvent``\ s should use ``add_input_refined`` ("Use this one. + It's much nicer.", ``enabler_input.h:96``). +- **[fragile]** ``load_macro_from_file`` uses fixed 100-char ``getline`` + buffers and hand-rolled parsing; malformed files print to cout and can + partially populate ``macros`` (``enabler_input.cpp:968-1043``). +- **[bug-ish]** ``get_input``'s "3 simticks before first repeat" guard + can drop a repeat event silently (``enabler_input.cpp:782-785``). +- **[stale]** ``GetKeyDisplay`` prints to ``cout`` on missing binding and + returns "?" — fine for dev, useless in production. + +Platform layer and misc +----------------------- + +- **[bug]** ``win32_compat.cpp::MessageBox``: ``new char[64]``/ + ``new char[128]`` for title/text are **leaked on every call** — the + heap allocation is unnecessary (``SDL_ShowMessageBox`` is synchronous + and copies the strings; the comment claiming stack pointers are unsafe + is wrong) (``win32_compat.cpp:125-129``). ``wcstombs`` also truncates + at fixed sizes and loses non-ASCII. +- **[fragile]** ``platform.h`` redefines Win32 constants (``MB_*``, + ``VK_*``, ``DWORD``, etc.) by hand; several values are explicitly noted + not to match Windows. Code compiled against real Win32 values may + behave differently. +- **[stale]** ``endian.h`` byteswaps only under ``__ppc__``/ + ``__ppc64__`` — dead on all modern targets. +- **[bug-ish]** ``stringvectst`` (``files.h``/``basics.h``) manually owns + C++ STL strings, erases from the front, and can leak if a consumer directly accesses its public + data element or if ``read_file`` is used on a non-empty list. Several methods ignore allocation failure. +- **[fragile]** file/decompression helpers (``files.cpp``, + ``basics.cpp``) read sizes and resize buffers from on-disk values + without validation — malformed data can trigger huge allocations or + overruns. +- **[fragile]** ``texture_handlerst``/``palettest``/``textures`` do + manual ``new[]``/``delete[]`` and pointer bookkeeping; error paths leak + or leave dangling pointers (e.g. ``clone_texture`` pushes a NULL + surface into ``raws``, ``textures.cpp:59-71``). +- **[fragile]** ``lua.cpp``: ``lua_push_flags_member`` calls + ``std::tolower`` on possibly-signed ``char`` (UB for negative chars); + several raw ``lua_*`` stack/index manipulations assume fixed stack + positions. +- **[stale]** ``enabler.h`` still carries OpenGL-era members/comments + (``reset_textures`` semantics predate SDL_Renderer; "OpenGL state + (wrappers)" comment on ``textures``), and ``enabler.cpp``/ + ``graphics.cpp`` have extensive commented-out TTF/OpenGL/partial-print + code. +- **[fragile]** ``musicsoundst`` mixes raw ints/``DWORD``\ s, + ``svector``\ s, and ``std::future``\ s; ``SOUND_CHANNELNUM``-indexed + arrays in ``interfacest`` movie code are fixed at 200 entries with no + bounds checks (``interface.h:115``). +- **[fragile]** ``emit_logs``/``errorlog_string``/``gamelog_string`` + open/close file streams per call plus keep static ``ofstream``\ s — the + comment admits "opening/closing these too often seems to cause crashes + on linux" (``basics.cpp:54-56``). + +Quick map: g_src symbol → DFHack +================================= + +.. list-table:: + :header-rows: 1 + + * - g_src + - df-structures / DFHack + * - ``enabler`` (global ``enablerst``) + - ``df.global.enabler`` (``df/enabler.h``) + * - ``gps`` (``graphicst``) + - ``df.global.gps`` (``df/graphic.h``) + * - ``gview`` (``interfacest``) + - ``df.global.gview`` (``df/interface.h``); + ``Gui::getCurViewscreen`` + * - ``init`` (``initst``) + - ``df.global.init`` + * - ``musicsound`` + - ``df.global.musicsound`` + * - ``mainloop()`` + - not mapped — closed side; reached via ``Core::Update`` inside + ``dfhooks_update`` + * - ``hooks_*`` trampolines + - ``dfhooks_*`` exports in ``library/Hooks.cpp`` + * - ``renderer`` vtable + - ``df/renderer.h`` vmethods — interposable via ``VTableInterpose``; + the old ``Renderer::renderer_wrap`` chaining code in + ``library/modules/Renderer.cpp`` is entirely commented out + * - ``InterfaceKey`` + - ``df.interface_key`` enum; ``Screen``/``Gui`` modules + * - ``enabler.async_tobox``/``async_frombox``/``async_zoom`` + - modeled inline in ``df.g_src.enabler.xml`` (mutex+condvar+deque + members) + * - ``screentexpos*`` arrays + - ``df.gps``/``df.renderer`` pointer fields; + ``Screen::paintScreen``/``df.screen`` diff --git a/docs/dev/github-workflows.rst b/docs/dev/github-workflows.rst new file mode 100644 index 0000000000..ea71e1a2b6 --- /dev/null +++ b/docs/dev/github-workflows.rst @@ -0,0 +1,204 @@ +GitHub workflows +================ + +We run our continuous integration (CI) validation and our release automation +via GitHub workflows. This allows us to merge PRs with confidence that they +won't catastrophically break DFHack functionality. GitHub workflows also allow +us to quickly produce stable release builds with fewer manual steps. Reducing +manual steps for releases is important since it is easy for a person to forget +a small but impactful step and therefore produce a bad release that causes +trouble for our users. + +Background +---------- + +`GitHub workflows `_ run +on provisioned VMs in the cloud with stable environments that we specify. They +are free to use since DFHack is an open source project. They have proven to be +reliably available within a few seconds when our workflows are triggered. The +logic for the workflows is written in yaml, and the files that control our +workflows are stored in the :file:`.github/workflows/` directory in each of our +repos. Example: :source:`.github/workflows`. + +Each workflow contains metadata that specifies: +- when it `triggers `_ +- what `base environment `_ it uses (OS, pre-installed dependencies, etc.) +- what additional dependencies should be installed (if any) +- custom business logic + +Workflows run in the context of a single repo, but workflows defined in one +repo can inherit logic from workflows in other repos. All our common CI logic +is in the main DFHack/dfhack repo, but our submodules, like our ``scripts`` and +``df-structures`` repos, have CI workflows defined that inherit from the logic +in DFHack/dfhack. That way we can fix bugs and extend functionality in one +place and have it benefit the entire org tree. + +Caches +~~~~~~ + +GitHub also provides 10GB per repository for `caches `_. +We utilize the cache system to keep state between workflow runs, cache +downloads, and keep compiler output to speed up subsequent builds. Efficient +use of the cache system is a critical part of our workflow design. It allows us +to iterate on test failures in PRs in one minute instead of 20. It allows us to +put out an entire emergency release build in 5 minutes instead of 45. We have +tuned our build and test workflows to minimize spurious cache misses and keep +the fast path fast. + +Caches are namespaced by key prefixes, and we have one key prefix per build +context. For example, release builds on gcc-11 are kept in one cache namespace, +whereas test builds on gcc-11 are kept separate. MSVC release and test builds +similarly have their own namespaces. Each cache has a maximum size that is +enforced by the business logic that writes the cache data. + +In order to maintain consistency in a distributed environment, caches are +versioned. A workflow will read the latest version of the cache with its key +prefix, maybe modify the cache with new data, then write back a new version. +Caches that are not used for 2 weeks are purged from GitHub storage, but if a +repo goes over the 10GB limit, caches are deleted in LRU order until the repo +is under the storage limit again. + +CI workflows +------------ + +Build +~~~~~ + +The Build workflow is the main CI workflow. It runs on every PR and push to a +branch. The ``build.yml`` file is essentially an orchestration layer for the +logic in several other .yml files: + +- ``test.yml`` builds DFHack with the test suite enabled (but stonesense and + windows pdb files disabled) and runs the test suite. It is optimized for + speed and is intended to give PR authors quick feedback on their changes. + The test suite is executed in a real running DF game on both Linux and + Windows. The ``test`` job populates the ``test`` cache, which is used by many + other workflows for non-distributed builds. +- ``package.yml`` builds DFHack as it would be released: test suite disabled + but stonesense and windows pdb files enabled. The ``package`` job populates the ``release`` cache, which is used to build all distributed binaries. +- The ``docs`` target does a docs-only build of DFHack and reports any errors. + Doc errors would show up in the ``test`` and ``package`` builds anyway, but + the ``docs`` target runs very fast and can identify doc errors in less than + 1m. +- ``lint.yml`` runs the verification scripts in the ``ci`` directory. These + scripts check for common errors in the codebase that are not caught by the + compiler. The lint scripts are written in Python and shell script and are + intended to be run quickly and catch common errors. + +Check type sizes +~~~~~~~~~~~~~~~~ + +``check-type-sizes.yml`` is a df-structures-only workflow that checks for +changes in the sizes of types in the xml structures. It builds the +``xml-dump-type-sizes`` binary on both Linux and Windows for both the +structures in this PR and for the structures in the target merge branch. It +then runs the built binary on its native OS and compares the output. If any +type sizes have changed, the workflow generates a PR comment (via the +``comment-pr.yml`` workflow) with details. + +.. _workflows-release-automation: + +Release automation workflows +---------------------------- + +Watch DF Releases +~~~~~~~~~~~~~~~~~ + +This workflow runs every 8 minutes and checks the Steam metadata, the Itch +website, and the Bay 12 website for evidence of new releases. If a new release +is found, it generates an announcement in a private channel on the DFHack +Discord server. + +Inside the ``watch-df-releases.yml`` workflow, there are separate jobs for +watching Steam branches and watching the websites. For the Steam watcher, it +takes configuration for: + +- which branches to watch +- whether to kick off the Generate symbols workflow when a new release is found +- whether to autodeploy to Steam when the Generate symbols workflow completes + +The workflow has protections against concurrent runs, so if you suspect a new +release is out, you can manually trigger the workflow to check. If the cron +trigger happens to run the workflow at the same time, the second run will be +paused while the first run completes. + +Generate symbols +~~~~~~~~~~~~~~~~ + +This workflow can be triggered manually or by the Watch DF Releases workflow. +It downloads the specified DF version for the selected distribution platform(s) +and OS target(s), then updates the ``symbol-table`` entries in ``symbols.xml``. +If the distribution platform is Steam, it can also autodetect the DF version by +extracting the version string from the DF title screen data. + +For Linux, it always builds DFHack -- just the core library (no plugins) -- and +generates symbols via the `devel/dump-offsets` and `devel/scan-vtables` scripts. + +For Windows, we extract symbol data via static analysis, so the workflow only +builds DFHack if it needs to autodetect the DF version. + +Once the symbols.xml file is updated, the workflow commits the changes to the +specified df-structures branch and updates the xml submodule ref in the +specified DFHack/dfhack branch. If a deploy Steam branch is specified, it also +launches the Deploy to Steam workflow. + +Deploy to GitHub +~~~~~~~~~~~~~~~~ + +`github-release.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for GitHub, creates a new GitHub release, and uploads the packages +to the GitHub release page. + +It uses text in :source:`.github/release_template.md` to generate the release +notes, and appends the changelog contents for the tagged version. + +If you need to re-tag the release to fix a mistake, it will automatically run +again and replace the binaries attached to the GitHub release for the tagged +version. It will not overwrite the release notes, though, to preserve any edits +you may have made in the GitHub UI. If you *want* it to completely regenerate +the release notes, you can delete the release before you re-tag the version. + +GitHub releases end up here: https://github.com/DFHack/dfhack/releases. + +Deploy to Steam +~~~~~~~~~~~~~~~ + +`steam-deploy.yml `_ +can be triggered manually or automatically by creating a new release version +tag in git. It builds DFHack with the release configuration, packages the +aritifacts for Steam, and uploads them to the specified Steam branch. + +The workflow caches steamcmd to speed the deployment up by 30s or so. +Otherwise, steamcmd would have to be downloaded and updated every time the +workflow runs. + +Steam releases end up here: +https://partner.steamgames.com/apps/builds/2346660. The "version" you +specified for the workflow is used as the "description" for the build. + +Maintenance workflows +--------------------- + +Update submodules +~~~~~~~~~~~~~~~~~ + +`update-submodules.yml `_ +runs daily, or can be run manually as needed. It checks DFHack submodules for +new commits on the main branches and updates the submodule refs in the DFHack +develop branch. + +You generally should not run this workflow for anything other than the develop +branch, as it will overwrite any changes you have made to the submodule refs in +other branches. + +Clean up PR caches +~~~~~~~~~~~~~~~~~~ + +This workflow runs automatically whenever a PR is closed or merged. It removes +caches created for the PR so they don't take up quota. + +Note that if you merge a PR before all the workflows have completed, the caches +may be created after this workflow runs. In that case, the caches will be +orphaned and will be purged by GitHub's cache eviction policy after 2 weeks. diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 4992015d86..98a1fcfc59 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -17,8 +17,14 @@ These are pages relevant to people developing for DFHack. /docs/dev/Lua API /docs/dev/overlay-dev-guide /docs/dev/Structures-intro + /docs/dev/g_src-notes + /docs/dev/Material-encoding + /docs/dev/CoreSuspender /docs/dev/data-identity + /docs/dev/github-workflows + /docs/dev/release-process /docs/dev/Memory-research + /docs/dev/performance-plan /docs/dev/Binpatches /docs/dev/Remote /docs/NEWS-dev diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 7b8198c78a..d87178bfff 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -86,6 +86,11 @@ beyond your everyday `widgets.Widget `: end This allows for dynamic updates to UI overlays directly from the CLI. +- If an ``overlay_onenable()`` function is defined, it is called when the + overlay is enabled (including when the persisted state is reloaded at DF + startup). +- If an ``overlay_ondisable()`` function is defined, it is called when the + overlay is disabled. If the widget can take up a variable amount of space on the screen, and you want the widget to adjust its position according to the size of its contents, you can @@ -228,6 +233,23 @@ If you need to improve performance, here are some potential options: 3. Move hotspots into C++ code, either in a new core library function or in a dedicated plugin +Overlay framework API +--------------------- + +The overlay plugin Lua interface provides a few functions for interacting with +the framework. You can get a reference to the API via:: + + local overlay = require('plugins.overlay') + +* ``overlay.rescan()`` + + Rescans all module-loadable Lua scripts for registered overlays and loads + updated widget definitions. + +* ``overlay.isOverlayEnabled(name)`` + + Returns whether the overlay with the given name is enabled. + Development workflows --------------------- diff --git a/docs/dev/performance-plan.rst b/docs/dev/performance-plan.rst new file mode 100644 index 0000000000..4d5309635f --- /dev/null +++ b/docs/dev/performance-plan.rst @@ -0,0 +1,291 @@ +========================= +Performance plan +========================= + +Working plan for improving DFHack's per-tick CPU budget. Based on +microarchitecture analysis (Intel VTune capture ``r120ue``, Jan 2026) +and code review of the Lua integration. Not user-facing documentation. + +.. contents:: + +Baseline findings +================= + +From ``r120ue`` (uarch exploration, whole-system sampling; ~24k +``CPU_CLK_UNHALTED.THREAD`` samples): + +- DFHack-side code is ~21% of sampled CPU: ``lua53.dll`` 3322, + ``dfhooks_dfhack.dll`` 1750, plugins ~110. +- DFHack owns ~11% of ``STALLS_L3_MISS`` and ~11% of DTLB + ``WALK_ACTIVE`` samples. +- Top lua53 stallers are data-layout problems, not dispatch: + ``luaH_getshortstr`` (hash-chain walk, 17 stall samples), + ``luaD_precall``, ``lua_geti``, ``luaH_next`` (``pairs`` iteration), + ``lua_getmetatable``, ``mainposition``, ``luaC_checkfinalizer``. +- Top CPU consumers are the C<->Lua transition layer: + ``luaV_execute`` 759, ``luaD_precall`` 260, ``index2addr`` 115, + ``luaV_finishget`` (metamethod resolution) 90, ``luaG_traceexec`` + 82 (the always-armed ``LUA_MASKCOUNT`` interrupt hook), + ``match``/``singlematch`` ~88 (Lua pattern matching per frame), + plus ``lua_tolstring``/``luaS_hash`` churn. +- C++ side: MSVC ``std::_Hash``/``std::_Tree`` internals, + ``std::string`` construct/destroy churn, ``matchFocusString``, + ``Units::isActive`` — chained-node STL containers have the same + miss problem as Lua's tables. + +Working hypothesis (operator model): DF's own memory traffic evicts +essentially all DFHack data from L3 between simulation ticks, so every +tick starts cold and each pointer-chase (hash chain, GC list, +metatable walk) pays full miss latency. Goal: make the per-tick cold +walk *sequential* (prefetcher-trackable) rather than pointer-chasing, +and reduce the number of misses by shortening dependent-load chains. + +Overlay processing is believed to dominate the per-tick budget; this +is consistent with the data but not yet proven — see the attribution +step below. + +Constraints and rejected directions +=================================== + +- **No PGO/LTO (deferred).** DFHack does questionable things to DF's + vtables and binary; whole-program optimization changes codegen + assumptions the interpose/binpatch machinery relies on. Would need + a dedicated validation effort before it is safe to enable. Revisit + only if cheaper items are exhausted. +- **Hugepages: optional-only.** ``VirtualAlloc(MEM_LARGE_PAGES)`` + needs ``SeLockMemoryPrivilege`` and often fails for normal users; + Linux needs THP/hugetlbfs. Steam Deck (Linux) is a significant + user population and we have no Linux profiling. Any allocator work + must function with ordinary pages; hugepages may be an opt-in + bonus path (``madvise(MADV_HUGEPAGE)`` on Linux, + ``MEM_LARGE_PAGES`` on Windows) with mandatory fallback. +- **No LLVM-JIT Lua (Ravi etc.).** The miss profile is in + ``ltable``/``lgc``/``ldo`` (data layout + transitions), not + ``lvm`` dispatch. A JIT keeps identical object layout and would + also have to reproduce our ``lua_lock`` threading patch, + ``LUA_MASKCOUNT`` interrupt hooks, ``lstate.h`` field access, + yieldable pcall, and debug-hook fidelity. Wrong tool for this + problem. +- **LuaJIT** has the best-in-class data layout but is Lua 5.1 + semantics; incompatible with our 5.3 corpus. + +Action items +============ + +Ordered by leverage-per-effort. All are architecture-independent +unless noted. + +Measurement +----------- + +1. Per-tick attribution. ``perf_counters.update_lua_ms`` / + ``update_plugin_ms`` (``Core.cpp:1668-1682``) already isolate Lua + update time; ``script-manager.lua`` prints them. Baseline a real + fort first. +2. Callsite-stack attribution on VTune captures: ``dd_callsite`` + parent links in ``dicer.db`` allow rebuilding stacks and splitting + Lua-side stalls between overlay widgets, timers, and plugin + events. (Scratch query script: ``%TEMP%/vtune_agg.py``; accepts a + ``dicer.db`` path for other captures.) +3. Lua-level: run ``profiler.lua`` scoped to the update window, or a + count hook aggregating by Proto, to identify which script + functions dominate ticks. +4. Object-level (if needed): we own the Lua source — log + ``luaC_newobj``/``luaM_realloc_`` (addr, type, size, phase) during + a session, dump live objects by walking ``allgc`` at a tick + boundary, cross-reference VTune miss addresses. + +Cheap knobs +----------- + +5. Raise the interrupt-hook interval. ``interrupt_init`` arms + ``LUA_MASKCOUNT, 256`` unconditionally (``LuaTools.cpp:508``) — + ``luaG_traceexec`` shows 82 samples. Raising the count trades + runaway-script interrupt latency for less hook dispatch. Tune and + verify interrupt still fires acceptably. +6. GC tuning on the core state (``lua_gc`` pause/stepmul) — never + configured today; only ``scripts/dwarf-op.lua`` calls + ``collectgarbage``. One-line experiments; watch memory growth. +7. Audit per-frame Lua string work: ``match``/``singlematch`` + samples imply ``string.find``/patterns in a per-frame path; + ``luaS_hash``/``lua_tolstring`` churn implies string building per + frame. Fix at the call site once attribution identifies it. +8. Audit ``pairs()`` iteration over hash-part tables in hot loops + (``luaH_next`` stalls): array-part iteration is contiguous and + prefetchable; hash-part is not. + +Structural: Lua heap layout +--------------------------- + +9. Custom ``lua_Alloc`` with type/lifetime-segregated arenas. + + - All GC objects funnel through ``luaC_newobj(L, tt, sz)`` — one + place to tag by type; other allocs via ``luaM_realloc_`` by + size class. + - Permanent bump region for everything allocated during + ``Lua::Open`` + ``require dfhack`` + script load (protos, + interned strings, metatables, fieldtables, module tables) — + init order approximates tick access order; GC's ``allgc`` walk + becomes quasi-sequential. Phase flag can live in + ``lua_extra_state`` (already used by ``dfhack_llimits.h``). + - Separate churn region for post-init garbage. + - Only 3 state-creation sites: ``Core.cpp:1394``, + ``LuaTools.cpp:1793``, ``LuaTools.test.cpp``. + - Portable: works with plain VirtualAlloc/mmap; hugepages are a + strictly optional overlay on top (see constraints). + - Caveat: ``lua_Alloc`` must implement realloc semantics + (alloc+copy+free is fine); Lua GC never moves objects + (``push_adhoc_pointer`` relies on this already). +10. Software prefetch at tick entry in ``Lua::Core::onUpdate`` + (``LuaTools.cpp:2073``): prefetch ``G(State)``, registry, and + each due coroutine's ``stack``/``ci`` before ``lua_resume`` — + portable via ``_mm_prefetch``/``__builtin_prefetch``. Cheap; + converts serial cold misses into overlapped ones. + +Binding layer (the userdata proxy question) +------------------------------------------- + +11. Kill the double lookup in the field path. + ``meta_struct_index`` → ``find_field`` → ``lookup_field`` + (``LuaTypes.cpp:422-454``) probes a Lua field-table *and* walks + the enum metatable per access. Short strings are interned, so + ``TString*`` pointer equality = name equality: a small + direct-mapped cache keyed ``(fieldtable, TString*)``, or a + per-type dense index assigned to each field name at metatable + build time, collapses this to one load. Highest-confidence fix — + hits ``luaV_finishget``/``luaH_getshortstr``/``lua_getmetatable`` + simultaneously. +12. Reduce per-access userdata churn. ``push_object_ref`` allocates + a fresh userdata (+ ``object_ref_header``: tag_ptr, tag_identity, + tag_attr, field_info) for every nested struct/container access — + ``unit.pos.x`` garbage-per-hop. Options, in increasing scope: + + a) arena allocation makes the churn cheap and local (item 9); + b) memoize refs via weak-valued cache keyed ``(ptr, identity)`` + — adds a hash probe, only worth it for expensive chains; + c) bulk getters for hot access patterns (e.g. one call returning + ``x,y,z`` for ``pos``) — semantic addition, avoids N + allocations and N metamethod hops per vector; + d) pack/shrink ``object_ref_header`` — review which of its four + pointers are needed on the common path; union-tag fields could + live in a side table populated only for unions. +13. Keep the proxy model; make the ref cheaper. A full move away + from userdata proxies isn't practical (scripts rely on + reference identity, ``__index`` polymorphism, ``_field``), but + the identity-layer work in PR #5959 already removes one virtual + dispatch from every primitive field read — that direction + (more ``if constexpr`` static knowledge in the access path) is + the right way to slim the proxy rather than replacing it. + +C++-side containers +------------------- + +14. MSVC STL ``unordered_map``/``unordered_set``/``map`` on per-tick + paths are chained-node structures with the same miss signature + (``_Fnv1a_append_value``, ``_Hash`` loops appear in the stall + list — e.g. inside ``Units::isActive``). Swap hot-path instances + for flat maps / sorted vectors / precomputed indices. +15. ``matchFocusString`` allocates ``std::string`` per call per + frame — make it allocation-free (``string_view``, cached split). + +Overlay / script layer +---------------------- + +16. Once attribution lands (item 2/3): batch the overlay render + path. ``dfhack.penarray`` bulk tile ops already exist + (``LuaApi.cpp:997+``). Per-cell ``Pen`` writes through + metamethods per frame should move to bulk APIs or C++ rendering. + If overlay is indeed the budget owner, this is likely the + largest single user-visible win. + +Verification methodology +======================== + +For each change: measure ``update_lua_ms`` / ``update_plugin_ms`` +before/after on the same save; VTune Hotspots (not uarch exploration +— less noise) filtered to the sim thread; compare +``MEMORY_ACTIVITY.STALLS_L3_MISS`` and ``MEM_LOAD_RETIRED.L3_MISS`` +inside ``lua53.dll``/``dfhooks_dfhack.dll``. Each item above is +independently revertable. + +Lua 5.4/5.5 upgrade assessment +============================== + +The ``__ipairs`` blocker is narrower than it appears: + +- ``__ipairs`` is registered on all generated metatables + (``LuaTypes.cpp`` ``SetPairsMethod`` x4; ``LuaWrapper.cpp`` + ``wtype_ipairs``/``complex_enum_ipairs``). It exists because some + proxy ``__index`` implementations never return nil for integer + keys (enum attrs — DFHack/dfhack#1860), so plain ``ipairs`` would + not terminate. +- Under 5.4, ``__ipairs`` is ignored entirely; ``ipairs`` indexes + ``t[i]`` until nil. Migration path: make every proxy ``__index`` + bounded for integer keys (containers already bound by item_count; + enum attrs need an explicit bound — the enum count is known), then + audit direct ``__ipairs`` users (one script: + ``scripts/test/fix/stuck-written-materials.lua``; plus + ``test/structures/enum_attrs.lua`` semantics). +- Real benefits for our profile: **generational GC** (young-gen + collections over small dense sets directly attack the + cold-sweep problem), ~10-25% faster VM, ``lua_newuserdatauv`` + uservalues (could fold ``object_ref_header`` into a uservalue — + see item 12d). +- Costs: semantic audit of ~500 scripts (integer/string coercion + strictness, ``math.random`` algorithm change breaks seeded + determinism, ``lua_resume`` signature change — we call it + directly in ``LuaTools.cpp:880``), bytecode format change + (``dumper.lua`` ``string.dump`` roundtrips). Moderate project. +- Recommendation: pursue items 9-12 first (larger wins per risk); + revisit 5.4 as a GC-locality lever afterward. 5.5 is not yet a + stable target; track it but don't plan against it. + +PR #5959 review (performance notes) +=================================== + +Net-positive for the hot path, with watch-items: + +- **Win:** ``type_identity_for::lua_read``/``lua_write`` inline + ``*(T*)ptr`` via ``if constexpr`` — removes the second virtual + call (old ``integer_identity_base::lua_read`` → virtual + ``read()``) on every primitive field access. This is exactly the + ``read_field``/``write_field`` hot path. +- **Watch:** ``mapped`` containers (``std::map``/ + ``unordered_map``) now get identities with O(n) ``item_pointer`` + iteration. Any struct field of map type newly exposed to Lua + (check the ``library/xml`` submodule bump) becomes O(n^2) under + ``ipairs``-style iteration — audit which types gain this. +- **Watch:** ``container_storage>`` generalizes the + "vector == vector" layout assumption to *any* + random-access pointer container (deque etc.). The assumption + holds for same-width pointer instantiations in practice, but the + blast radius widened. +- **Note:** removing ``BUILD_DFHACK_LIB`` guards lets plugins + instantiate ``container_impl``/``type_identity_for`` locally, + creating non-canonical identity objects — ``is_type_compatible`` + compares identity *pointers*, so a plugin-side local instance of + an existing identity could yield false type mismatches. Document + that identities must come from ``identity_traits``/canonical + sources only. +- ``field_error``/``get_object_internal`` newly ``DFHACK_EXPORT`` — + required by header-side template instantiation; fine. +- ``LuaTools.h`` now includes ``DataIdentity.h`` — heavier include + in every consumer TU; compile-time only. +- ``bool`` moved to ``NUMBER_IDENTITY_TRAITS`` with correct + ``isInteger()==false`` and bool-specific read/write — semantics + preserved (``type()`` still ``IDTYPE_PRIMITIVE``, name "bool"). +- ``c_string`` → ``primitive_identity_base`` preserves + "raw pointer string" write error and push-string read semantics. +- ``push_adhoc_pointer`` retains the "GC never moves objects" hack — + reinforces that any allocator work must stay non-moving. + +Suggested order +=============== + +1. Callsite attribution (who owns the Lua stalls). +2. Cheap knobs: hook count, GC tuning, string-work audit. +3. Field-lookup inline cache (item 11). +4. Arena allocator (items 9-10). +5. C++ container swaps + ``matchFocusString`` (items 14-15). +6. Overlay batching per attribution (item 16). +7. Lua 5.4 evaluation (generational GC) once 1-6 land. diff --git a/docs/dev/release-process.rst b/docs/dev/release-process.rst new file mode 100644 index 0000000000..bef09be412 --- /dev/null +++ b/docs/dev/release-process.rst @@ -0,0 +1,209 @@ +Release process +=============== + +This page details the process we follow for beta and stable releases. + +For documentation on the related GitHub workflows, see +`workflows-release-automation`. + +Beta release +------------ + +This process pushes a pre-release build to GitHub and Steam. It is intended to +be lower-toil than the stable release process and allows us to facilitate +frequent public testing and feedback without compromising the stability of our +"stable" releases. + +1. Run the `Update submodules `_ GitHub action on the ``develop`` branch to ensure that all submodules are up to date. + +2. Update version strings in :source:`CMakeLists.txt` as appropriate. Set ``DFHACK_RELEASE`` to the *next* stable release version with an "rc#" suffix. For example, if the last stable release was "r1" then set the string to "r2rc1". If we do a second beta release before the final stable "r2" then the string would be "r2rc2". + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``TRUE``. + - Commit and push to ``develop`` + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r2rc1`` for bash) + +3. Tag ``develop`` (no need to tag the submodules) and push: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin`` + + - This will automatically trigger `Deploy to Steam `_ and `Deploy to GitHub `_ build workflows. + +4. Write release notes highlights and any requests for feedback in the `draft GitHub release `_ and publish the draft. + +5. Associate release notes with the build on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Small update / Patch notes" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r2rc1") + - Set the "Subtitle" to "DFHack pre-release (beta channel)" + - Mention release highlights in the "Summary" field + - Transcode release notes from the GitHub release into the "Event description" field + - patch notes must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. By the time you're done with the patch notes the build will likely be ready for you) + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on dfhack_logo.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + - `Promote `_ the build to the "beta" branch (and the "testing" branch if it's newer than what is on the "testing" branch) + +6. Monitor for beta channel subscriber feedback on the Steam `community page `_ + +7. *Maybe* also post to Reddit and other announcement channels if we feel like we need to recruit more beta testers into the pool, but we should avoid posting so often that it is annoying for those who don't use Steam or just want announcements for stable releases. + +Stable release +-------------- + +This process creates a stable DFHack release meant for widespread distribution. +Stable releases come in two forms: straight from ``develop`` or from a point +release branch. + +During "normal" times, we will test out new features in beta releases until we +reach a point of stability. Then, after the ``develop`` branch is feature frozen +while we polish and fix bugs, we tag a release directly from ``develop`` +``HEAD``. + +However, if we have already started committing beta features to ``develop`` and +it becomes necessary to put out a bugfix release for a problem in an +already-released stable release, then we will create a new branch from the +stable tag, cherry-pick fixes from ``develop`` onto that branch, and spin a +release from there. After the point release is published, we'll merge the +branch back into ``develop`` and remove the release branch to clean up. + +1. Triage remaining issues/PRs in the `release project `_ + + - Don't feel pressure to merge anything risky just before a stable release. That's what beta releases are for. + +2. In your local clone of the ``DFHack/develop`` branch, make sure your checkout and all submodules (listed in :source:`.gitmodules`) are up to date with their latest public commits and have no uncommitted/unpushed local changes. + +3. Ensure that CI has not failed unexpectedly on the latest online changes: + + - https://github.com/DFHack/dfhack/commits/develop + - https://github.com/DFHack/scripts/commits/master + - https://github.com/DFHack/df-structures/commits/master + +4. Update version strings in :source:`CMakeLists.txt` as appropriate + + - Ensure the ``DFHACK_PRERELEASE`` flag is set to ``FALSE``. + - Set ``RELEASE`` in your environment for the commands below (e.g. ``RELEASE=51.07-r1``) + +5. Replace "Future" with the version number and clean up changelog entries; add new "Future" section (with headers pre-populated from the template at the top of the file): + + - ``docs/changelog.txt`` + - ``scripts/changelog.txt`` + - ``library/xml/changelog.txt`` + - ``plugins/stonesense/docs/changelog.txt`` + +6. Do a top-level build to ensure the docs build cleanly + +7. Commit/push changes to submodules and tag (``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin master``) + + - ``scripts`` + - ``library/xml`` + - ``plugins/stonesense`` + +8. Commit and push changes to ``develop`` + + - Ensure that any updates you pushed to submodules are tracked in the commit to ``DFHack/develop`` + +9. Tag ``dfhack``: ``git tag -a $RELEASE -m "Bump to $RELEASE"; git push --tags origin develop`` + + - This will automatically trigger a `Deploy to Steam `_ GitHub action to the "staging" Steam branch and a `Deploy to GitHub `_ GitHub action to create a draft `release `_ from a template and attach the built artifacts. + +10. Switch to the Steam ``staging`` release channel in the Steam client (password: ``stagingstagingstaging``) and download/test the update. + + - Ensure DFHack starts DF when run from the Steam client + - Ensure the DFHack version string is accurate on the title page (should just be the release number, e.g. ``DFHack 51.07-r1``, with no git hash or warnings) + - Run `devel/check-release` + - If something goes wrong with this step, fix it, delete the tag (both from `GitHub `_ and locally (``git tag -d $RELEASE``)), re-tag, re-push, and re-test. Note that you do *not* need to remove the GitHub draft release -- the existing one will just get updated with the new tag and binaries. You *can* remove the draft release, though, if you want the release notes to get regenerated. + +11. Prep release on GitHub + + - Go to the draft `release `_ on GitHub + - Add announcements, highlights (with demo videos), etc. to the description + +12. Push develop to master (``git push origin develop:master``) + + - This will start the documentation build process and update the published "stable" docs + - Note that if this is a -r1 release, you won't be able to complete this step until a classic build is available on the Bay 12 website so the DFHack Test workflow can pass, which is a prerequisite for being able to push to ``master``. + +13. Post release notes on Steam + + - Go to the `announcement creation page `_ + - Select "A game update" + - Select "Regular update" + - Set the "Event title" to the release version (e.g. "DFHack 51.07-r1") + - Set the "Subtitle" to "DFHack stable release" + - Add list of highlights (and maybe announcements, if significant) to the "Summary" field + - Upload screenshots and demo videos via the button at the bottom of the "Previously uploaded videos" area + - Add release notes to the "Event description" field (must be in BBcode; see `converting-markdown-to-bbcode` below for how to convert our release notes to BBcode) + - Drag uploaded images/videos into their appropriate places in the announcement text (replace the GitHub URL tags, which won't work from Steam) + - If the generated release notes exceed the announcement length limits, add a link to the GitHub release page at the bottom of the announcement instead + - Click "Link to build" and select the staging branch (be sure the build has been deployed to the branch first. by the time you're done with the patch notes the build will likely be ready for you) + - the release notes will travel with the build when we promote it to other branches + - Go to the Artwork tab, select "Previously uploaded images", and search for and double-click on STABLEannouncement6.png. Click "Upload" (even though it has already been uploaded). + - Switch to the "Publish" tab and publish! + +14. Go to the `Steam builds page `_ and promote the build to the "default" branch + + - For the build that you just pushed to "staging", click the "-- Select an app branch --" drop-down and select "default" + - Click on "Preview Change" + - Commit the change (you may need to verify with 2FA) + - If the release is newer than what's on the ``beta`` and/or ``testing`` branches, set it live on those branches as well + +15. Publish the prepped GitHub release + +16. Send out release announcements + + - Announce new version in r/dwarffortress. Example: https://www.reddit.com/r/dwarffortress/comments/1i3l5xl/dfhack_5015r2_released_highlights_stonesense/ + - Create the post in the Reddit web interface; the mobile app is extremely painful to use for posting + - Do an "Images & Video" post, sample title: "DFHack 51.07-r1 released! Hilights: Open legends mode directly from an active fort, Dig through warm or damp tiles without interruption, Unlink buildings from levers" + - Add the animated gifs to the post (with appropriate captions naming the relevant tool and what is being demonstrated) + - Add the "DFHack Official" flair to the post. If you're not a r/dwarffortress mod, ask Myk to do this after posting. + - After posting, add each section of the release notes as its own comment, splitting out individual announcements and highlights. This gives people the opportunity to respond directly to the portion of the release notes that interests them; it also helps us avoid size limits for comments. You can include a single still shot (.png file) per comment, but you have to switch to "Fancy Pants Editor" to do it. You can only switch editors once, or the image will get messed up (that is, the image will turn into a hyperlink to an image). Suggested procedure is to prepare the comment in markdown, switch to Fancy Pants Editor, and add images just before submitting the comment. + - Announce new version in forum thread. Example: http://www.bay12forums.com/smf/index.php?topic=164123.msg8567134#msg8567134 + - Update latest version text and link in `first post `_ (if you are not Lethosor, ping Lethosor for this) + - Announce in `#announcements `_ on DFHack Discord + - Announce in `#mod-releases `_ on Kitfox Discord + - Change the name of the release thread on Kitfox Discord to match the release version (if you are not Myk, ping Myk for this) + +17. Monitor all announcement channels for feedback and respond to questions/complaints + +18. Create a `project `_ on GitHub in the DFHack org for the next release + + - Open the `project template `_ + - Click "Use this template" + - Name the project according to the version, e.g. "51.07-r2" and click "Use template" + - In the new project, select settings and set the visibility to Public + - Move any remaining To Do or In Progress items from last release project to next release project + - Close project for last release + +19. If this is a -r2 release or later, go to https://readthedocs.org/projects/dfhack/versions/ and "Edit" previous DFHack releases for the same DF version and mark them "Hidden" (keep the "Active" flag set) so they no longer appear on the docs version selector. + +.. _converting-markdown-to-bbcode: + +Converting Markdown to BBcode +----------------------------- + +Hopefully we can `automate `_ this in the future, but for now, here is the procedure: + +1. Get the markdown that you want to convert into some field on GitHub (can be a temporary text field that you then preview without saving) + +2. View the rendered release notes in your browser (these instructions are for Chrome, but other browsers probably have similar capabilities) + +3. Right click on the rendered text and inspect the DOM + +4. Copy the HTML element that contains the release notes + +5. Click on the "Import HTML" button on the Steam announcement form; paste in the HTML and click "Overwrite" + +6. Copy the generated BBCode out from the description field and into a text editor + +7. Fix it up: + + - Remove the "How do I download DFHack?" section -- people on Steam don't need it + - Some ``

`` elements aren't converted properly and need to be rewritten with square brackets + - Any monospaced text gets HTML tags instead of BBCode ``[code]`` tags, but you can't use them either since they force newlines. ``[tt]`` isn't supported. Any ```` tags just need to be removed entirely. + - Any ``
`` and ```` tags need to be removed + +8. Copy it all back into the description field for the announcement + +9. Click on "Preview event" to double check that it renders sanely + +10. You're done. diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 6660dfae51..f47f9565ce 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -10,3 +10,4 @@ These pages are detailed guides covering DFHack tools. /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide + /docs/guides/stonesense-art-guide diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 03b26d56a5..dae03c3aba 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1919,7 +1919,7 @@ Links DF maps. - DFHack's `buildingplan plugin ` sets material and quality constraints for quickfort-placed buildings. -- `Python Quickfort `__ is the previous, +- `Python Quickfort `__ is the previous, Python-based implementation that DFHack's quickfort script was inspired by. .. _quickfort_guide_appendix: @@ -2298,8 +2298,10 @@ Symbol Type Properties ``ek`` kiln ``en`` magma kiln ``ib`` ballista +``it`` bolt thrower ``ic`` catapult ``Cw`` wall +``CW`` reinforced wall ``Cf`` floor ``Cr`` ramp ``Cu`` up stair diff --git a/docs/guides/stonesense-art-guide.rst b/docs/guides/stonesense-art-guide.rst new file mode 100644 index 0000000000..c9e966dba3 --- /dev/null +++ b/docs/guides/stonesense-art-guide.rst @@ -0,0 +1,71 @@ +.. _stonesense-art-guide: + +Stonesense art creation guide +============================= + +Understanding isometric perspective +----------------------------------- + +Stonesense uses an isometric perspective, a form of pseudo-3D projection where objects are displayed at an +angle, typically with a 2:1 pixel ratio for diagonal lines. This perspective allows for a detailed and visually +appealing representation of a 3D world using 2D sprites. Unlike traditional top-down views, isometric projection +simulates depth while maintaining a consistent scale without vanishing points. + +Understanding sprites +--------------------- + +Understanding how Stonesense deals with sprites is central to anyone who wishes to modify the content. The +scheme is not very complicated, and this guide will give a short introduction to how they work. With the +exception of floors, which we will discuss later, all sprites are 32x32 pixels and come in groups known +as sprite sheets. All sprites are loaded and rendered in 32-bit full-color PNGs. The image files should have +a transparent background but pure magenta (RGB: 255,0,255) is also treated as transparent. + + +.. image:: ../images/stonesense-sprite-sample.png + :align: left + +Here's an example of a typical Stonesense sprite. + +When working with Stonesense sprites, it is important to understand how they fit into the isometric grid. +Each sprite is designed to align with the isometric perspective and must fit within a specific bounding area. +To illustrate this, here is a template for the area that should be used by Stonesense sprites: + +.. image:: ../images/stonesense-sprite-template.png + :align: left + +The solid area is the floor space taken up by a sprite, while the dotted box indicates the volume above this +area corresponding to one z-level. + +The way sprites are loaded is fairly generalized: the name of the sprite sheet, and the index of a sprite within that sheet. + +Sprite sheets +------------- +There can be an arbitrary number of sprite sheets for Stonesense, though there are 3 sheets that are +always present as they contain default sprites (see further down). Configuring the XML to use new sheets is +outside the scope of this guide but there may be a guide for such added in the future. + +Sprite index +------------ +The sprite index, or sheet index, is the zero-indexed offset of a sprite on its sprite sheet. +The index starts with the upper left sprite which has index zero. It then increments to the right. Stonesense +is hardcoded to 20 sprite-wide sheets, this means that anything past 20 "sprite slots" is ignored, though less +than 20 slots is fine. The first sprite on the second row always has index 20 (even if there are fewer sprites per row in the sheet), the next row is 40, and so on. This +boundary is hardcoded and changing the size of the sheet will not affect it. + +This image shows how sprites are indexed. Grid added for readability. + +.. figure:: ../images/stonesense-indexed-sprites.png + :align: left + + +Important sprite sheets +----------------------- +`objects.png `_ is the default sheet +for buildings and vegetation. Also used for all hard-coded content, like default plants, the cursor, default +walls and liquid. + +`creatures.png `_ is the default +sprite sheet for creatures. If no file is specified in a creature node, this is the sheet it will use. + +`floors.png `_ holds all the Stonesense +floors. Unlike the other sprite sheet, this sheet is hard-coded with sprite dimensions of 32x20 pixels. diff --git a/docs/images/stonesense-indexed-sprites.png b/docs/images/stonesense-indexed-sprites.png new file mode 100644 index 0000000000..dc749a2196 Binary files /dev/null and b/docs/images/stonesense-indexed-sprites.png differ diff --git a/docs/images/stonesense-sprite-sample.png b/docs/images/stonesense-sprite-sample.png new file mode 100644 index 0000000000..35dcbefd55 Binary files /dev/null and b/docs/images/stonesense-sprite-sample.png differ diff --git a/docs/images/stonesense-sprite-template.png b/docs/images/stonesense-sprite-template.png new file mode 100644 index 0000000000..401a8549bf Binary files /dev/null and b/docs/images/stonesense-sprite-template.png differ diff --git a/docs/images/stonesense-yellowcubes.png b/docs/images/stonesense-yellowcubes.png new file mode 100644 index 0000000000..83ae6ce507 Binary files /dev/null and b/docs/images/stonesense-yellowcubes.png differ diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index b429f57542..f7bea4d6d9 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -15,6 +15,7 @@ Units are protected from being automatically butchered if they are: * Untamed * Named or nicknamed * Caged, if and only if the cage is in a zone (to protect zoos) +* On a restraint * Trained for war or hunting * Females who are pregnant or brooding a clutch of fertile eggs diff --git a/docs/plugins/autoclothing.rst b/docs/plugins/autoclothing.rst index 1ea1a9094f..02d13c83a7 100644 --- a/docs/plugins/autoclothing.rst +++ b/docs/plugins/autoclothing.rst @@ -22,6 +22,7 @@ Usage autoclothing autoclothing autoclothing + autoclothing clear ```` can be "cloth", "silk", "yarn", or "leather". The ```` can be anything your civilization can produce, such as "dress" or "mitten". @@ -43,6 +44,9 @@ Examples long as there is cloth available to make them out of). ``autoclothing cloth dress`` Displays the currently set number of cloth dresses chosen per citizen. +``autoclothing clear cloth "short skirt"`` + Unsets cloth short skirts from being made if previously enabled such as + in the first example Which should I enable: autoclothing or tailor? ---------------------------------------------- diff --git a/docs/plugins/autodump.rst b/docs/plugins/autodump.rst index 298619a62e..8c2110f8f2 100644 --- a/docs/plugins/autodump.rst +++ b/docs/plugins/autodump.rst @@ -8,6 +8,9 @@ autodump .. dfhack-command:: autodump-destroy-here :summary: Destroy items marked for dumping under the keyboard cursor. +.. dfhack-command:: autodump-destroy-item + :summary: Destroy the selected item. + This tool can instantly move all unforbidden items marked for dumping to the tile under the keyboard cursor. After moving the items, the dump flag is unset and the forbid flag is set, just as if it had been dumped normally. See @@ -24,19 +27,28 @@ Usage autodump [] autodump-destroy-here + autodump-destroy-item ``autodump-destroy-here`` is an alias for ``autodump destroy-here`` and is intended for use as a keybinding. +``autodump-destroy-item`` marks the currently viewed item for destruction. +The pending destruction can be canceled if the command is run again before +unpausing, or by running ``autodump undestroy``. + Options ------- ``destroy`` - Destroy instead of dumping. Doesn't require a cursor. If ``autodump`` is - called again with this option before the game is resumed, it cancels - pending destroy actions. + Destroy instead of dumping. Doesn't require a cursor. Items held by units + (e.g. equipped or in a carried container) are skipped, and the contents of + destroyed containers are destroyed along with the container. ``destroy-here`` - Destroy items marked for dumping under the cursor. + Destroy items under the cursor that are marked for dumping. +``undestroy`` + Revert pending destruction of items marked by ``destroy``, + ``destroy-here``, or ``autodump-destroy-item``. Only works while the game + is still paused; once a frame passes, the items may already be gone. ``visible`` Only process items that are not hidden. ``hidden`` @@ -48,8 +60,14 @@ Examples -------- ``autodump`` - Teleports items marked for dumping to the cursor position. + Teleports unforbidden items marked for dumping to the cursor position. ``autodump destroy`` - Destroys all unforbidden items marked for dumping + Destroys all unforbidden items marked for dumping. +``autodump destroy forbidden`` + Destroys all forbidden items marked for dumping, skipping unforbidden items. ``autodump-destroy-here`` - Destroys items on the selected tile that are marked for dumping. + Destroys unforbidden items on the selected tile that are marked for dumping. +``autodump-destroy-item`` + Destroys the item currently being viewed, or reverts its pending destruction. +``autodump undestroy`` + Reverts pending destruction of all marked items. diff --git a/docs/plugins/autofarm.rst b/docs/plugins/autofarm.rst index 8896c3117c..0d070eeedc 100644 --- a/docs/plugins/autofarm.rst +++ b/docs/plugins/autofarm.rst @@ -9,6 +9,14 @@ Periodically scan your plant stocks and assign crops to your farm plots based on which plant stocks are low (as long as you have the appropriate seeds). The target threshold for each crop type is configurable. +Farms that span multiple biomes are handled correctly: any crop that can grow +in any of the biomes covered by the farm's tiles may be assigned to that farm. + +Individual farm plots can be excluded from autofarm management via the controls +that appear on the farm plot information sheet. That sheet also provides a +shortcut to `gui/autofarm`, a convenient UI for setting crop thresholds and +enabling or disabling autofarm. + Usage ----- @@ -24,9 +32,14 @@ Usage Sets thresholds of individual plant types. You can find the identifiers for the crop types in your world by running the -following command:: +following command: ``getplants -f`` - lua "for _,plant in ipairs(df.global.world.raws.plants.all) do if plant.flags.SEED then print(plant.id) end end" +Some plants produce usable growth items (such as fruits, leaves, or bulbs) +when processed or gathered. These products normally count toward the plant's +threshold together with the plant items themselves, but you can also assign a +threshold to a specific growth product with the ``:`` syntax. +A plant is grown while either its pooled stock or any of its assigned growth +products is below the corresponding threshold. Examples -------- @@ -35,3 +48,6 @@ Examples Set the default threshold to 30. ``autofarm threshold 150 MUSHROOM_HELMET_PLUMP GRASS_TAIL_PIG`` Set the threshold for Plump Helmets and Pig Tails to 150 +``autofarm threshold 30 BUSH_QUARRY:LEAVES`` + Keep at least 30 Quarry Bush Leaves in stock, growing Quarry Bushes until + you have enough of them to process diff --git a/docs/plugins/autolabor.rst b/docs/plugins/autolabor.rst index 73d5feac53..34c997f2ff 100644 --- a/docs/plugins/autolabor.rst +++ b/docs/plugins/autolabor.rst @@ -5,6 +5,16 @@ autolabor :summary: Automatically manage dwarf labors. :tags: fort auto labors +The ``autolabor`` command runs the plugin's *legacy* engine, which writes the +labor matrix directly and bypasses the work detail system. The same plugin +also provides the `labormanager` command, a modern engine that works through +the work detail system instead. A third *monitor* mode watches the job +board and shows task starvation warnings without managing labors at all. +Only one mode can be active at a time; +``autolabor mode [legacy|modern|monitor]`` or the overlay on the work +details screen switches between them, and the selected mode persists +across save/load. + Autolabor attempts to keep as many dwarves as possible busy while allowing dwarves to specialize in specific skills. @@ -32,24 +42,21 @@ untouched by autolabor. logging level using the `debugfilter` command (setting either ``debug`` or ``trace`` level for the ``cycle`` mode) but be warned that this may generate a large amount of console spam, especially in a large fort. - When it is enabled, autolabor automatically disables the work detail system. You cannot - use autolabor and work details at the same time. If you attempt to open the work detail screen while - autolabor is active, a warning box should appear advising you that autolabor is managing labors and preventing - you from making any changes on that screen. + When it is enabled in legacy mode, autolabor automatically disables the work detail system. You cannot + use legacy autolabor and work details at the same time. The DFHack overlay panel on the work details + screen (the same panel that provides the `gui/settings-manager` save/load buttons) shows the active + mode and warns you when legacy mode is preventing changes on that screen from taking effect. - Finally, should you disable autolabor, autolabor will automatically reenable the vanilla work detail system. - However, the work detail system only updates labors when the work detail screen is open and some change is - made on that screen. Therefore, if you choose to disable autolabor, you should probably immediately - thereafter open the work details screen and make some change to force the game to recompute all labor - assignments based on the vanilla algorithm. At this time, it is not possible for autolabor to do this - automatically. + Should you disable autolabor or switch to modern mode, the vanilla work + detail system is automatically reenabled and all citizens' labors are + recomputed from the work details. Usage ----- :: - enable autolabor + autolabor enable Anything beyond this is optional - autolabor works well with the default settings. Once you have enabled it in a fortress, it stays enabled until you @@ -114,5 +121,9 @@ Advanced usage Turn off autolabor for a specific labor. ``autolabor reset-all| reset`` Return a labor (or all labors) to the default handling. +``autolabor mode [legacy|modern|monitor]`` + Show or change which engine runs when the plugin is enabled. Monitor + mode performs no labor management; it only watches the job board and + reports starving job postings via the DFHack notification panel. See `autolabor-artisans` for a differently-tuned setup. diff --git a/docs/plugins/blueprint.rst b/docs/plugins/blueprint.rst index 1a5071b6be..3ec8d9c435 100644 --- a/docs/plugins/blueprint.rst +++ b/docs/plugins/blueprint.rst @@ -3,7 +3,7 @@ blueprint .. dfhack-tool:: :summary: Record a live game map in a quickfort blueprint. - :tags: fort design buildings map stockpiles + :tags: fort design buildings map stockpile With ``blueprint``, you can export the structure of a portion of your fortress in a blueprint file that you (or anyone else) can later play back with @@ -15,13 +15,6 @@ selected interactively with the `gui/blueprint` command or, if the GUI is not used, starts at the active cursor location and extends right and down for the requested width and height. -.. admonition:: Note - - blueprint is still in the process of being updated for the new version of - DF. Stockpiles (the "place" phase), zones (the "zone" phase), building - configuration (the "query" phase), and game configuration (the "config" - phase) are not yet supported. - Usage ----- @@ -83,11 +76,6 @@ phases; just separate them with a space. Generate quickfort ``#place`` blueprints for placing stockpiles. ``zone`` Generate quickfort ``#zone`` blueprints for designating zones. -``query`` - Generate quickfort ``#query`` blueprints for configuring stockpiles and - naming buildings. -``rooms`` - Generate quickfort ``#query`` blueprints for defining rooms. If no phases are specified, phases are autodetected. For example, a ``#place`` blueprint will be created only if there are stockpiles in the blueprint area. diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index c00569365c..e06b3586a8 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -146,6 +146,12 @@ Some building types will have other options available as well, such as a selector for how many weapons you want in weapon traps or whether you want to only build engraved slabs. +If you want placed buildings to be built as soon as possible, you can toggle +the "Do now" option (or hit :kbd:`n`). While it is on, the job for each placed +building is flagged as high priority, so dwarves will prefer it over their +regular work once the building is unsuspended. The option stays on until you +toggle it off again, and it is saved with your configuration. + Setting quality and material filters ++++++++++++++++++++++++++++++++++++ @@ -204,6 +210,18 @@ other available items (or from items produced in the future if not all items are available yet). If there are multiple item types to choose for the current building, one dialog will appear per item type. +Queueing work orders +-------------------- + +If you are planning a building but do not have the required items in stock, you can +automatically queue a manager work order to produce the missing quantity. After +selecting your desired item types and filters, press :kbd:`Ctrl`:kbd:`q` (or click +"Queue order") to generate a work order. + +`buildingplan` will attempt to automatically determine the correct job (e.g. making +a wooden bed if you are planning a bed) and will respect the material categories +you have selected in your filters. + Building status --------------- @@ -215,6 +233,9 @@ to the building. If there is a particular building that you need built ASAP, you can click on the "make top priority" button (or hit :kbd:`Ctrl`:kbd:`T`) to bump the items for this building to the front of their respective queues. +To rush the construction job itself once the building is underway, use the +"Make top priority" toggle in the `prioritize` overlay. + Note that each item type and filter configuration has its own queue, so even if an item is in queue position 1, there may be other queues that snag the needed item first. @@ -237,3 +258,8 @@ usual) unless freed via the ``Free`` buttons on the ``Show items`` tab on both buildings. This will remove the mechanism from the building and drop it onto the ground, allowing it to be reused elsewhere. There is an option to auto-free mechanisms when unlinking to perform this step automatically. + +For any linked building that is a lever, a ``Pull`` button also appears next to it +on the ``Show linked buildings`` tab, with a glyph showing the lever's current +position. Clicking it queues a high-priority ("do now") pull-lever job without +having to navigate to the lever itself; click it again to cancel the job. diff --git a/docs/plugins/changeitem.rst b/docs/plugins/changeitem.rst index f452f6a742..cf3fb83d94 100644 --- a/docs/plugins/changeitem.rst +++ b/docs/plugins/changeitem.rst @@ -18,9 +18,10 @@ Usage Show details about the selected item. Does not change the item. You can use this command to discover RAW ids for existing items. ``changeitem []`` - Change the item selected in the ``k`` list or inside a container/inventory. + Change the item that is selected in the UI. ``changeitem here []`` - Change all items at the cursor position. Requires in-game cursor. + Change all items at the keyboard cursor position. Requires the vanilla + keyboard cursor to be enabled. Examples -------- diff --git a/docs/plugins/changelayer.rst b/docs/plugins/changelayer.rst index f986a1e3d1..44fa1a3279 100644 --- a/docs/plugins/changelayer.rst +++ b/docs/plugins/changelayer.rst @@ -22,8 +22,8 @@ Usage When run without options, ``changelayer`` will: -- only affect the geology layer at the current cursor position -- only affect the biome that covers the current cursor position +- only affect the geology layer at the current keyboard cursor position +- only affect the biome that covers the current keyboard cursor position - not allow changing stone to soil and vice versa You can use the `probe` command on various tiles around your map to find valid @@ -34,8 +34,9 @@ Examples ``changelayer GRANITE`` Convert the layer at the cursor position into granite. -``changelayer SILTY_CLAY force`` - Convert the layer at the cursor position into clay, even if it's stone. +``changelayer SAND_RED force`` + Convert the layer at the cursor position into red sand, even if it's + currently stone. ``changelayer MARBLE all_biomes all_layers`` Convert all layers of all biomes which are not soil into marble. diff --git a/docs/plugins/dig.rst b/docs/plugins/dig.rst index 0a6007f598..d8d9c4ff40 100644 --- a/docs/plugins/dig.rst +++ b/docs/plugins/dig.rst @@ -197,13 +197,13 @@ Overlay This tool also provides three overlays that are managed by the `overlay` framework. -asciicarve -~~~~~~~~~~ +asciidesignated +~~~~~~~~~~~~~~~ -The ``dig.asciicarve`` overlay makes carving designations visible in ASCII -mode. It highlights tiles that are designated for smoothing, engraving, track -carving, or fortification carving. The designations blink (slowly) so you can -still see what is underneath them. +The ``dig.asciidesignated`` overlay makes designations visible in ASCII mode. +It highlights tiles that are designated for digging, smoothing, engraving, +track carving, or fortification carving. The designations blink (slowly) so you +can still see what is underneath them. Due to the limitations of the ASCII mode screen buffer, the designation highlights may show through other interface elements that overlap the diff --git a/docs/plugins/edgescroll.rst b/docs/plugins/edgescroll.rst new file mode 100644 index 0000000000..4522ff36bf --- /dev/null +++ b/docs/plugins/edgescroll.rst @@ -0,0 +1,13 @@ +edgescroll +========== + +.. dfhack-tool:: + :summary: Scroll the game world and region maps when the mouse reaches the window border. + :tags: interface + +Usage +----- + +:: + + enable edgescroll diff --git a/docs/plugins/export-world-map.rst b/docs/plugins/export-world-map.rst new file mode 100644 index 0000000000..33307df5df --- /dev/null +++ b/docs/plugins/export-world-map.rst @@ -0,0 +1,96 @@ +export-world-map +================ + +.. dfhack-tool:: + :summary: Export world map data for GIS and other external tools. + :tags: inspection embark map + +This plugin exports world-map information to files in the DFHack configuration +folder under the ``map-export`` directory. It must be run from the +embark-selection screen. The exports are generated at the granularity of +individual region tiles (16 x 16 per world tile). Those tiles are only generated +by DF as you scroll around on the zoomed-in embark map, which thus needs to be +done prior to running this export tool. + +The supported export targets are: + +- ``regions``: writes world tile regions and their biome information to ``regions.csv``. +- ``sites``: writes site extents and ownership/civilization information to ``sites.csv``. +- ``rivers``: writes rivers to ``rivers.csv``. +- ``elevation``: writes elevation data to ``elevation.dat`` and ``elevation.vrt``. + +Unless you have software such as ArcGIS or QGIS/GDAL available, these exports +will be of little use. + +Usage +----- + +:: + + export-world-map + export-world-map + export-world-map ... + export-world-map all + +If no topic is given, or if ``all`` is supplied, every supported export target +is run. + +Options +------- + +``--group-by-world`` + Create a folder named after the world in the ``map-export`` directory. + +``--group-by-date`` + Create a folder with name ``year-month`` inside the world folder. Implies + ``--group-by-world``. + +Examples +-------- + +``export-world-map`` + Export all supported datasets. + +``export-world-map regions`` + Export only region boundary geometries. + +``export-world-map sites rivers`` + Export site and river geometries only. + + +Notes on file formats +--------------------- + +To avoid depending on external libraries such as GDAL, the export formats are +kept as simple as possible to generate. This means raw bitmap data for the +elevation export and CSV files for the others. Before these files can be used +reasonably in software such as QGIS, they need to be assigned a coordinate +reference system (CRS) and converted into formats that can be accessed more +efficiently than CSV. While any CRS will do, WGS 84 / Pseudo-Mercator +(EPSG:3857) is the preferred choice. + +For detailed map data (``regions`` and ``rivers``), Parquet is by far the +preferred format. The elevation export provides a VRT file to facilitate the +transformation into a GeoTIFF with the aforementioned CRS. + +Assuming a recent version of GDAL built with Parquet support and with all +command-line tools in your PATH, this can be achieved with the following +commands: + +:: + + ogr2ogr -a_srs 'EPSG:3857' -of Parquet regions.parquet -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' regions.csv + ogr2ogr -a_srs 'EPSG:3857' -of Parquet rivers.parquet -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' rivers.csv + gdal raster convert elevation.vrt elevation.tif + +As an alternative to separate Parquet files, all vector data exports might also be imported into a single Spatialite (SQLite) file: + +:: + + ogr2ogr -a_srs 'EPSG:3857' -of SQLite world-map.db -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' -dsco 'SPATIALITE=YES' -update -overwrite regions.csv + +Or into a single Geopackage file: + +:: + + ogr2ogr -a_srs 'EPSG:3857' -of GPKG world-map.gpkg -oo 'GEOM_POSSIBLE_NAMES=*wkt' -oo 'KEEP_GEOM_COLUMNS=NO' -oo 'AUTODETECT_TYPE=YES' -update -overwrite regions.csv diff --git a/docs/plugins/getplants.rst b/docs/plugins/getplants.rst index 82616b2358..3ba5a6e386 100644 --- a/docs/plugins/getplants.rst +++ b/docs/plugins/getplants.rst @@ -11,8 +11,9 @@ names. Usage ----- -``getplants [-t|-s|-f]`` - List valid tree/shrub ids, optionally restricted to the specified type. +``getplants [-t|-s|-f] []`` + List valid tree/shrub ids, optionally restricted to the specified type and + traits. ``getplants [ ...] []`` Designate trees/shrubs of the specified types for chopping/gathering. @@ -25,6 +26,10 @@ Examples Gather all plants on the map that yield seeds for farming. ``getplants NETHER_CAP -n 10`` Designate 10 nether cap trees for chopping. +``getplants --brewable -a`` + Designate all shrubs that can be brewed into alcohol. +``getplants --brewable`` + List all valid brewable plant IDs. Options ------- @@ -46,3 +51,22 @@ Options Verbose: Lists the number of (un)designations per plant. ``-n `` Number: Designate up to the specified number of plants of each species. +``-d``, ``--dry-run`` + Dry run: report how many designations would be (un)set without changing + anything. +``--brewable`` + Restrict the selection to plants that can be brewed into alcohol. +``--edible`` + Restrict the selection to plants with parts that can be eaten raw. +``--oil`` + Restrict the selection to plants with seeds, nuts, or fruits that can be + pressed into oil. +``--cloth`` + Restrict the selection to plants that yield thread for cloth. +``--dye`` + Restrict the selection to plants that yield dyes. + +Trait filters can be combined, in which case a plant that matches any of the +specified traits qualifies. They apply to the whole selection, so +``getplants --brewable -x QUARRY_BUSH`` designates every brewable plant except +quarry bushes. diff --git a/docs/plugins/labormanager.rst b/docs/plugins/labormanager.rst index 255de05c46..560057ecad 100644 --- a/docs/plugins/labormanager.rst +++ b/docs/plugins/labormanager.rst @@ -2,131 +2,193 @@ labormanager ============ .. dfhack-tool:: - :summary: Automatically manage dwarf labors. - :tags: unavailable - -Labormanager is derived from `autolabor` but uses a completely different -approach to assigning jobs to dwarves. While autolabor tries to keep as many -dwarves busy as possible, labormanager instead strives to get jobs done as -quickly as possible. - -Labormanager frequently scans the current job list, current list of dwarves, and -the map to determine how many dwarves need to be assigned to what labors in -order to meet all current labor needs without starving any particular type of -job. - -Dwarves on active military duty or dwarves assigned to burrows are left -untouched. + :summary: Automatically manage dwarf labors via work details. + :tags: fort auto labors + +``labormanager`` is the modern mode of the `autolabor` plugin. Instead of +writing the labor matrix directly, it expresses labor policy through Dwarf +Fortress's work detail system, so the game's job auction chooses which +dwarves take which jobs. The plugin's managed work details are visible on +the work details screen (``u`` -> Labor -> Work Details) with names +beginning with ``auto:``. The DFHack overlay panel on that screen (the +same panel that provides work detail save/load buttons from +`gui/settings-manager`) lets you switch modes and tune the plugin's +priorities. + +Since the job auction already prefers skilled dwarves for skilled jobs, +labormanager focuses on the things the auction does poorly: + +* **Unskilled job starvation.** The auction tends to starve jobs that + require no skill (hauling, cleaning, lever pulling, and so on) in favor of + skilled work. Labormanager maintains a laborer pool, assigned to the + ``auto:Laborers`` work detail and restricted to unskilled labors, so there + are always dwarves that *can only* take unskilled work. The pool grows and + shrinks based on how many unclaimed unskilled job postings are piling up. + +* **Skill growth and specialization.** Citizens with a useful skill are + assigned to a specialized ``auto:`` work detail covering the labors that + train that skill, so they spend their time practicing rather than hauling. + If the fortress has a guild hall dedicated to the dwarf's profession, + labormanager also protects some of that specialist's time so they can + participate in guild activities (see ``idle-reserve`` below). + +Labormanager keeps statistics on which skills the fortress actually +exercises, based on the job postings that resolve on the job board. That +history shapes specialization: + +* **No dead skills.** Skills the fort never uses (animal dissection and + gelding in most forts, for example) get no reserved work detail at all -- + a dwarf whose only talent is an unused skill stays a generalist instead + of being pinned to irrelevant work. +* **Demand-weighted specialization.** Frequently-used skills are + prioritized over rarely-used ones when choosing which specialty to + assign, and units skilled in them are protected harder from laborer + duty. +* **Quality skills.** Skills whose products carry skill-scaled quality + (smithing, crafting, cooking, mechanics, engraving, and so on) are + preferred for higher-skilled workers -- both when choosing a specialty + and when deciding who to keep out of the laborer pool. +* **Assignment stickiness.** A unit assigned to a skill detail keeps the + assignment while any work for that skill remains -- including claimed + jobs, the fetch-materials phase, and breaks -- so jobs aren't aborted + by reassignment churn. For tool-carrying labors (mining, woodcutting, + hunting) this also prevents dropping and re-equipping tools between + cycles. Assignments are recorded in the save data and restored on load. +* **Builtin tool details.** DF v50 only equips tools for members of the + game's predefined work details, so mining, woodcutting, and hunting + specialists are assigned to the builtin Miners, Woodcutters, and + Hunters details instead of plugin-created ones. Memberships added by + the plugin are tracked and removed cleanly on disable; members you + assigned yourself are never touched. +* **Strange-mood shaping.** Dwarves that can still have a strange mood + (their race has the ``STRANGE_MOODS`` token and they haven't had one + yet) and that have already started a moodable craft are steered toward + skills a strange mood can advance -- preferably valuable ones like + weaponsmithing or armorsmithing -- so a mood pays out in a useful + legendary skill. + +Workshop restrictions are honored as well: when a job is posted at a +workshop that has a permitted-worker list, the engine ensures at least +one listed worker is assigned the job's labor, and when the workshop +restricts jobs by skill level it ensures at least one worker within the +allowed range holds that labor. Units on any workshop's permitted list +are treated as reserved for that shop's work -- they are preferentially +assigned the labors its jobs require and deprioritized for the laborer +pool and unrelated specialties. + +Labormanager also reads each citizen's unmet personal needs and biases +assignments toward work that satisfies them: + +* Citizens desperate to stay occupied, acquire things, or help others are + preferentially drafted into the laborer pool, where hauling, stockpiling, + depot runs, and patient care satisfy those needs. +* Citizens needing social time (gregarious, family-oriented, or just + wanting to take it easy) are kept out of the restrictive pools so they + can idle and socialize; guilded specialists with these needs earn guild + rest sooner. +* Citizens who crave wandering are steered toward fishing, hunting, and + plant gathering; those craving excitement toward trapping and hunting; + those craving creativity toward crafts. A completely unskilled citizen + can be assigned a specialty purely on inclination. + +Dwarves on active military duty, in meetings, or assigned to burrows are +left untouched. Children are unaffected: their chores are governed by the +vanilla chores settings. .. warning:: - As with autolabor, labormanager will override any manual changes you make to - labors while it is enabled, including through other tools such as Dwarf - Therapist. Do not run both autolabor and labormanager at the same time! + While labormanager is enabled it will overwrite the membership of its + ``auto:`` work details on every cycle. Player-created work details are + left alone, but manual edits to the managed details will be lost. Usage ----- :: - enable labormanager - -Anything beyond this is optional - labormanager works well with the default -settings. Once you have enabled it in a fortress, it stays enabled until you -explicitly disable it, even if you save and reload your game. - -The default priorities for each labor vary (some labors are higher priority by -default than others). The way the plugin works is that, once it determines how -many jobs of each labor are needed, it then sorts them by adjusted priority. -(Labors other than hauling have a bias added to them based on how long it's been -since they were last used to prevent job starvation.) The labor with the highest -priority is selected, the "best fit" dwarf for that labor is assigned to that -labor, and then its priority is *halved*. This process is repeated until either -dwarves or labors run out. - -Because there is no easy way to detect how many haulers are actually needed at -any moment, the plugin always ensures that at least one dwarf is assigned to -each of the hauling labors, even if no hauling jobs are detected. At least one -dwarf is always assigned to construction removing and cleaning because these -jobs also cannot be easily detected. Lever pulling is always assigned to -everyone. Any dwarves for which there are no jobs will be assigned hauling, -lever pulling, and cleaning labors. If you use animal trainers, note that -labormanager will misbehave if you assign specific trainers to specific animals; -results are only guaranteed if you use "any trainer". - -Labormanager also sometimes assigns extra labors to currently busy dwarfs so -that when they finish their current job, they will go off and do something -useful instead of standing around waiting for a job. - -There is special handling to ensure that at least one dwarf is assigned to haul -food whenever food is detected left in a place where it will rot if not stored. -This will cause a dwarf to go idle if you have no stockpiles to haul food to. - -Dwarves who are unable to work (child, in the military, wounded, handless, -asleep, in a meeting) are entirely excluded from labor assignment. Any dwarf -explicitly assigned to a burrow will also be completely ignored by labormanager. - -The fitness algorithm for assigning jobs to dwarves generally attempts to favor -dwarves who are more skilled over those who are less skilled. It also tries to -avoid assigning female dwarfs with children to jobs that are "outside", favors -assigning "outside" jobs to dwarfs who are carrying a tool that could be used as -a weapon, and tries to minimize how often dwarves have to reequip. - -Labormanager automatically determines medical needs and reserves health care -providers as needed. Note that this may cause idling if you have injured dwarves -but no or inadequate hospital facilities. - -Hunting is never assigned without a butchery, and fishing is never assigned -without a fishery, and neither of these labors is assigned unless specifically -enabled (see below). - -The method by which labormanager determines what labor is needed for a -particular job is complicated and, in places, incomplete. In some situations, -labormanager will detect that it cannot determine what labor is required. It -will, by default, pause and print an error message on the dfhack console, -followed by the message "LABORMANAGER: Game paused so you can investigate the -above message.". If this happens, please open an :issue:`` on GitHub, -reporting the lines that immediately preceded this message. You can tell -labormanager to ignore this error and carry on by running -``labormanager pause-on-error no``, but be warned that some job may go undone in -this situation. + labormanager enable + +Anything beyond this is optional. Once enabled in a fortress, it stays +enabled until you explicitly disable it, even if you save and reload your +game. + +The same plugin binary also provides the legacy `autolabor` command, which +uses the classic labor-matrix algorithm, and a *monitor* mode that does no +labor management at all -- it only watches the job board and reports +starving job postings (see the end of this document). Only one mode can be +active at a time; use ``labormanager mode`` (or the overlay on the work +details screen) to see or change which mode will run. Examples -------- -``labormanager priority BREWER 500`` - Boost the priority of brewing jobs. -``labormanager max FISH 1`` - Only assign fishing to one dwarf at a time. Note that you also have to run - ``labormanager allow-fishing`` for any dwarves to be assigned fishing at - all. +``labormanager balance staffing`` + Prioritize preventing unskilled jobs from starving. Specialists will be + pulled into the laborer pool more aggressively when hauling backs up. +``labormanager balance skills`` + Prioritize skill growth. Specialists keep their specialized labors even + when unskilled work is piling up. +``labormanager labor MINE unmanaged`` + Leave the mining labor to the vanilla work details and any manual + assignments, while managing everything else. Advanced usage -------------- ``labormanager list`` - Show current priorities and current allocation stats. Use this command to - see the IDs for all labors. + List every labor, whether labormanager is managing it, and which labors + currently have starving (long-unclaimed) job postings. ``labormanager status`` - Show basic status information. -``labormanager priority `` - Set the priority value for labor to . -``labormanager max `` - Set the maximum number of dwarves that can be assigned to a labor. -``labormanager max none`` - Unrestrict the number of dwarves that can be assigned to a labor. -``labormanager max disable`` - Don't manage the specified labor. Dwarves who you have manually enabled this - labor on will be less likely to have managed labors assigned to them. -``labormanager reset-all|reset `` - Return a labor (or all labors) to the default priority. + Show the current balance setting, role counts, job board statistics + (how many postings resolved, their average and longest waits, and how + many are currently starving), and the most-used skills observed on the + job board. +``labormanager balance <0-4|staffing|lean-staffing|balanced|lean-skills|skills>`` + Set where the balance slider sits. Lower values grow the laborer pool + more aggressively to keep unskilled jobs staffed; higher values protect + skill specialization. This is the same control shown as a slider on the + work details overlay. +``labormanager idle-reserve <0-100>`` + Percentage of time to keep free for specialists whose profession has a + matching guild hall, so they can take part in guild demonstrations and + other off-duty activities. Defaults to 30. +``labormanager labor managed|unmanaged`` + Include or exclude a specific labor from labormanager's work details. + Unmanaged labors are entirely governed by the player's own work detail + configuration. ``labormanager allow-fishing|forbid-fishing`` - Allow/disallow fisherdwarves. *Warning* Allowing fishing tends to result in - most of the fort going fishing. Fishing is forbidden by default. + Allow/disallow fisherdwarf specialists. Fishing is forbidden by default + and also requires a fishery. ``labormanager allow-hunting|forbid-hunting`` - Allow/disallow hunterdwarves. *Warning* Allowing hunting tends to result in - as many dwarves going hunting as you have crossbows. Hunting is forbidden by - default. -``labormanager pause-on-error yes|no`` - Make labormanager pause/continue if the labor inference engine fails. See - the above section for details. + Allow/disallow hunter specialists. Hunting is forbidden by default and + also requires a butchery. +``labormanager mode [legacy|modern|monitor]`` + Show or change which engine runs when the plugin is enabled. The same + command exists under `autolabor`. In monitor mode the plugin performs + no labor management -- all work details are left to the player -- but + still tracks the job board and shows the task starvation warning. +``labormanager dump`` + Print a diagnostic snapshot of everything the engine can see: every + job and posting on the board with its flags and resolved labor, the + per-labor demand and starvation streaks, skill usage, tool counts, + work detail memberships, and each citizen's state, pins, and enabled + labors. Intended for bug reports; also available as + ``autolabor dump``, and meaningful in monitor mode. + +Monitor mode (``labormanager mode monitor`` or the work details overlay) +runs the same tracking without any labor management, for players who want +the starvation warning while keeping full manual control. + +The tracking works as follows: job postings that sit unclaimed on the job +board are timed, and if any posting goes more than a full game day (1200 +ticks) without being +claimed, a "task starvation" warning appears in the DFHack notification +panel (alongside the other fort warnings), naming the longest-starving +job and how long it has been waiting; clicking the warning zooms the map +to that job's location. The warning can be toggled in `gui/notify` under +``labor_starvation``. + +All of labormanager's accumulated state -- job posting ages, skill usage +history, starvation streaks, and the guild rest rotation -- is saved with +the fortress and restored when the save is loaded, so statistics and +rotation carry over between play sessions. diff --git a/docs/plugins/logistics.rst b/docs/plugins/logistics.rst index 5a6fc9254c..4b3605f428 100644 --- a/docs/plugins/logistics.rst +++ b/docs/plugins/logistics.rst @@ -3,7 +3,7 @@ logistics .. dfhack-tool:: :summary: Automatically mark and route items in monitored stockpiles. - :tags: fort auto animals items stockpiles + :tags: fort auto animals items stockpile Commands act upon the stockpile selected in the UI unless another stockpile identifier is specified on the commandline. diff --git a/docs/plugins/nestboxes.rst b/docs/plugins/nestboxes.rst index 3c07cc30ca..a8c67dda33 100644 --- a/docs/plugins/nestboxes.rst +++ b/docs/plugins/nestboxes.rst @@ -10,9 +10,16 @@ This plugin will automatically scan for and forbid fertile eggs incubating in a nestbox so that dwarves won't come to collect them for eating. The eggs will hatch normally, even when forbidden. +You can control which eggs are collected and which eggs are protected by placing +the nestboxes whose contents should be protected inside a burrow and running +``nestboxes burrow ``. The default behavior of protecting all +fertile eggs in nest boxes can be reestablished by running ``nestboxes all``. + Usage ----- :: enable nestboxes + nestboxes burrow + nestbox all diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 9ac3009af8..d89475a328 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -145,7 +145,14 @@ This collection of orders handles basic fort necessities: You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. -Note that the jugs are specifically made out of wood. This is so, as long as you don't may any other "Tools" out of wood, you can have a stockpile just for jugs by restricting a finished goods stockpile to only take wooden tools. +These orders do not contain milking, shearing, or cheesemaking jobs since the +game does not provide sufficient order conditions. Please enable ``automilk``, +``autoshear``, and `autocheese` on the DFHack `gui/control-panel` for these +types of jobs. + +Note that the jugs are specifically made out of wood. This is so, as long as +you don't make any other "Tools" out of wood, you can have a stockpile just for +jugs by restricting a finished goods stockpile to only take wooden tools. Armok's additional note: "shleggings? Yes, `shleggings `__." @@ -155,7 +162,7 @@ Armok's additional note: "shleggings? Yes, This collection creates basic items that require heat. It is separated out from ``library/basic`` to give players the opportunity to set up magma furnaces first -in order to save resources. It handles: +(if desired) in order to save resources. It handles: - charcoal (including smelting of bituminous coal and lignite) - pearlash diff --git a/docs/plugins/plant.rst b/docs/plugins/plant.rst index 51eedaf999..2a2d6327e0 100644 --- a/docs/plugins/plant.rst +++ b/docs/plugins/plant.rst @@ -9,7 +9,8 @@ Grow and remove shrubs or trees. Modes are ``list``, ``create``, ``grow``, and ``remove``. ``list`` prints a list of all valid shrub and sapling raw IDs. ``create`` allows the creation of new shrubs and saplings. ``grow`` adjusts the age of saplings and trees, allowing them to grow instantly. ``remove`` can -remove existing shrubs and saplings. +remove existing shrubs and saplings. For operating on grasses (such as bamboo) +use `regrass` instead. Usage ----- diff --git a/docs/plugins/smooth-movement.rst b/docs/plugins/smooth-movement.rst new file mode 100644 index 0000000000..cdee603b89 --- /dev/null +++ b/docs/plugins/smooth-movement.rst @@ -0,0 +1,50 @@ +smooth-movement +=============== + +.. dfhack-tool:: + :summary: Smoothly animate movement in the fortress viewport. + :tags: fort interface + +This plugin makes creatures, hauled items, vehicles, and associated status +icons glide between tiles. It only changes rendering; gameplay, simulation +timing, and save data are unaffected. + +The plugin requires the SDL 2D renderer. + +Usage +----- + +:: + + enable smooth-movement + smooth-movement + smooth-movement flip on|off + smooth-movement zlevel on|off + +Running ``smooth-movement`` without arguments reports the plugin status and +current settings. Optional features are reset to their defaults whenever the +plugin is enabled. + +Examples +-------- + +``enable smooth-movement`` + Enable smooth movement on the main z-level. + +``smooth-movement flip on`` + Horizontally flip creatures so they face their direction of travel. + +``smooth-movement zlevel on`` + Also animate movement on visible z-levels below the main level. + +Settings +-------- + +``flip`` (default: off) + Flip creature sprites horizontally according to their last horizontal + movement. Items, vehicles, and designation icons keep their normal + orientation. + +``zlevel`` (default: off) + Animate movement on visible lower z-levels. Sprite flipping remains + independent of this setting. diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 70a11a3bcf..9ef6f34a53 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -185,3 +185,10 @@ World overlay Searching is supported for the Artifacts list when viewing the world map (where you can initiate raids). + +Stress icons overlay +-------------------- + +The stress/happiness icons in the top bar are clickable. Clicking an icon opens +a searchable list of the citizens in that stress category, and selecting a +citizen from the list centers the map on them. diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1e400a06e4..dd0174f320 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -2,8 +2,31 @@ spectate ======== .. dfhack-tool:: - :summary: Automatically follow productive dwarves. - :tags: fort interface + :summary: Automated spectator mode. + :tags: fort inspection interface + +This tool is for those who like to watch their dwarves go about their business. + +When enabled, `spectate` will lock the camera to following the dwarves +scurrying around your fort. Every once in a while, it will automatically switch +to following a different dwarf. It can also switch to following animals, +hostiles, or visiting units. You can switch to the next target (or a previous +target) immediately with the left/right arrow keys. + +`spectate` will automatically disengage and turn itself off when you move the +map, just like the vanilla follow mechanic. It will also disengage immediately +if you open the squads menu for military action. + +It can also annotate your dwarves on the map with their name, job, and other +information, either as floating tooltips or in a panel that comes up when you +hover the mouse over a target. + +Run `gui/spectate` to configure the plugin's settings. + +Settings are saved globally, so your preferences for `spectate` and its +overlays will apply to all forts, not just the currently loaded one. Follow +mode is automatically disabled when you load a fort so you can get your +bearings before re-enabling. Usage ----- @@ -11,52 +34,210 @@ Usage :: enable spectate - spectate - spectate set - spectate enable|disable - -When enabled, the plugin will lock the camera to following the dwarves -scurrying around your fort. Every once in a while, it will automatically switch -to following a different dwarf, preferring dwarves on z-levels with the highest -job activity. - -If you have the ``auto-disengage`` feature disabled, you can switch to a new -dwarf immediately by hitting one of the map movement keys (``wasd`` by -default). To stop following dwarves, bring up `gui/launcher` and run -``disable spectate``. - -Changes to settings will be saved with your fort, but if `spectate` is enabled -when you save the fort, it will disenable itself when you load so you can get -your bearings before re-enabling follow mode with ``enable spectate`` again. + spectate [status] + spectate toggle [] + spectate set [] + spectate overlay enable|disable Examples -------- ``enable spectate`` - Starting following dwarves and observing life in your fort. + Start following dwarves and observing life in your fort. + +``spectate toggle`` + Toggle the plugin on or off. Intended for use with a keybinding. The + default is Ctrl-Shift-S. ``spectate`` The plugin reports its configured status. -``spectate enable auto-unpause`` - Enable the spectate plugin to automatically dismiss pause events caused - by the game. Siege events are one example of such a game event. +``spectate set auto-unpause true`` + Configure `spectate` to automatically dismiss popups and pause events, like + siege announcements. -``spectate set tick-threshold 1000`` - Set the tick interval between camera changes back to its default value. +``spectate set follow-seconds 30`` + Configure `spectate` to switch targets every 30 seconds when in follow mode. -Features --------- -:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) -:auto-disengage: Toggle auto-disengagement of plugin through player - intervention while unpaused. (default: disabled) -:animals: Toggle whether to sometimes follow animals. (default: disabled) -:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, - titans, invaders, etc.) (default: disabled) -:visiting: Toggle whether to sometimes follow visiting units (eg. - diplomats) +``spectate overlay enable`` + Show informative tooltips that follow each unit on the map. Note that this + can be enabled independently of `spectate` itself. + +``spectate set tooltip-follow-job-shortenings "Store item in stockpile" "Store"`` + Abbreviate the names of "Store item in stockpile" jobs to just "Store" when the + job is displayed in the `spectate` tooltips. See the + ``tooltip-follow-job-shortenings`` setting below for details. + +``spectate toggle tooltip-follow`` + Toggle the follow tooltips on or off. Settings -------- -:tick-threshold: Set the plugin's tick interval for changing the followed - dwarf. (default: 1000) + +``auto-unpause`` (default: disabled) + Toggle auto-dismissal of announcements that pause the game, like sieges, + forgotten beasts, etc. + +``cinematic-action`` (default: enabled) + Toggle whether to switch targets more rapidly when there is conflict. + +``follow-seconds`` (default: 10) + Set the time interval for changing the followed unit. The interval does not + include time that the game is paused. + +``include-animals`` (default: disabled) + Toggle whether to sometimes follow fort animals and wildlife. + +``include-hostiles`` (default: disabled) + Toggle whether to sometimes follow hostiles (eg. undead, titans, invaders, + etc.) + +``include-visitors`` (default: disabled) + Toggle whether to sometimes follow visiting units, like diplomats. + +``include-wildlife`` (default: disabled) + Toggle whether to sometimes follow wildlife. + +``prefer-conflict`` (default: enabled) + Toggle whether to prefer following units in active conflict. + +``prefer-new-arrivals`` (default: enabled) + Toggle whether to prefer following (non-siege) units that have newly + arrived on the map. + +``prefer-nicknamed`` (default: enabled) + Toggle whether to prefer following nicknamed units. + +``tooltip-follow`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + tooltips that follow onscreen dwarves around the map. + +``tooltip-follow-blink-milliseconds`` (default: 3000) + If the ``spectate.tooltip`` overlay is enabled, set the tooltip's blink + duration in milliseconds. Set to 0 to always show. + +``tooltip-follow-hold-to-show`` (default: none) + If the ``spectate.tooltip`` overlay is enabled, specifies a modifier key + (one of none, ctrl, alt, or shift) that has to be held to show the tooltips + that follow onscreen dwarves around the map. This supersedes the + ``tooltip-follow-blink-milliseconds`` option. + +``tooltip-follow-job`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + job of the dwarf in the tooltip. + +``tooltip-follow-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the tooltip. + +``tooltip-follow-job-shortenings`` (default: "Store item in stockpile" -> "Store item") + If the ``spectate.tooltip`` overlay is enabled, this dictionary is used to + shorten some job names, f.e. "Store item in stockpile" becomes "Store item". + You can pass two parameters to ``spectate set tooltip-follow-job-shortenings`` to + add or change elements in the dictionary. See the Examples section for an example. + +``tooltip-follow-name`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + name of the dwarf in the tooltip. + +``tooltip-follow-stress`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the tooltip. + +``tooltip-follow-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the tooltip. F.e. + ``tooltip-follow-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` below for details. + +``tooltip-hover`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + hover popup panel when your mouse cursor is over a unit. + +``tooltip-hover-job`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + job of the dwarf in the hover panel. + +``tooltip-hover-activity`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + activity of the dwarf in the hover panel. + +``tooltip-hover-name`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + name of the dwarf in the hover panel. + +``tooltip-hover-stress`` (default: enabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + happiness level (stress) of the dwarf in the hover panel. + +``tooltip-hover-stress-levels`` (default: Displeased, Content, Pleased are disabled) + If the ``spectate.tooltip`` overlay is enabled, toggle whether to show the + specific happiness level (stress) of the dwarf in the hover panel. F.e. + ``tooltip-hover-stress-levels 2 true`` would show the Displeased emoticon. + See ``tooltip-stress-levels`` below for details. + +``tooltip-stress-levels`` + Controls how happiness levels (stress) are displayed (emoticon and color). + F.e. ``tooltip-stress-levels 6 text XD`` will change the emoticon for + Ecstatic dwarves to ``XD``. + Default values are: + +.. list-table:: + :widths: 25 25 25 25 + :header-rows: 1 + + * - Level + - name + - text + - pen + * - 0 + - Miserable + - ``=C`` + - COLOR_RED + * - 1 + - Unhappy + - ``:C`` + - COLOR_LIGHTRED + * - 2 + - Displeased + - ``:(`` + - COLOR_YELLOW + * - 3 + - Content + - ``:]`` + - COLOR_GREY + * - 4 + - Pleased + - ``:)`` + - COLOR_GREEN + * - 5 + - Happy + - ``:D`` + - COLOR_LIGHTGREEN + * - 6 + - Ecstatic + - ``=D`` + - COLOR_LIGHTCYAN + +Keep in mind that the text may look different when rendered in the game's font. + +Overlays +-------- + +``spectate.tooltip`` + +``spectate`` can show informative tooltips that follow each unit on the map +and/or a popup panel with information when your mouse cursor hovers over a unit. + +This overlay is managed via the `overlay` framework. It can be controlled via +the ``spectate overlay`` command or the ``Overlays`` tab in `gui/control-panel`. + +``spectate.followpanel`` + +This overlay adds widgets to the vanilla follow panel -- the one that appears +in the lower left corner of the screen when you are following a unit. When you +are following a unit, regardless of whether the `spectate` plugin is enabled, +you can use the keyboard cursor left/right keys to switch which unit you are +following. There is also an indicator for whether spectate mode is enabled +(that is, whether the `spectate` plugin is enabled), and there is a button for +launching the `gui/spectate` configuration UI. diff --git a/docs/plugins/stockflow.rst b/docs/plugins/stockflow.rst index 95eba213c1..a32591d63f 100644 --- a/docs/plugins/stockflow.rst +++ b/docs/plugins/stockflow.rst @@ -3,21 +3,21 @@ stockflow .. dfhack-tool:: :summary: Queue manager jobs based on free space in stockpiles. - :tags: unavailable + :tags: fort auto workorders stockpile -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 +With this plugin, you can tally up free space in specific stockpiles and +automatically queue jobs through the manager to produce items to fill the free space. -When the plugin is enabled, the :kbd:`q` menu of each stockpile will have two -new options: +When the plugin is enabled, selecting a stockpile in the UI will show the +stockflow overlay panel with two options: * :kbd:`j`: Select a job to order, from an interface like the manager's screen. * :kbd:`J`: Cycle between several options for how many such jobs to order. -Whenever the bookkeeper updates stockpile records, new work orders will -be placed on the manager's queue for each such selection, reduced by the -number of identical orders already in the queue. +About once per in-game day, new work orders will be placed on the manager's +queue for each such selection, reduced by the number of identical orders +already in the queue. This plugin is similar to `workflow`, but uses stockpiles to manage job triggers instead of abstract stock quantities. @@ -31,5 +31,3 @@ Usage Display whether the plugin is enabled. ``stockflow list`` List any work order settings for your stockpiles. -``stockflow fast`` - Enqueue orders once per day instead of waiting for the bookkeeper. diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 739cac5b8d..c9b5f96bd4 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -3,7 +3,7 @@ stockpiles .. dfhack-tool:: :summary: Import, export, or modify stockpile settings. - :tags: fort design productivity stockpiles + :tags: fort design productivity stockpile Commands act upon the stockpile selected in the UI unless another stockpile identifier is specified on the commandline. @@ -378,6 +378,12 @@ Flags and subcategory prefixes:: paste/ pressed/ +Properties:: + + brewable + millable + processable + Settings files:: preparedmeals diff --git a/docs/plugins/stonesense.rst b/docs/plugins/stonesense.rst index 5d8dbb6964..7127dc71b4 100644 --- a/docs/plugins/stonesense.rst +++ b/docs/plugins/stonesense.rst @@ -15,12 +15,8 @@ Usage Open the visualiser in a new window. The viewer window has read-only access to the game, and can follow the game view -or be moved independently. Configuration for stonesense can be set in the -``dfhack-config/stonesense/init.txt`` file in your DF game directory. If the window refresh -rate is too low, change ``SEGMENTSIZE_Z`` to ``2`` in this file, and if you are -unable to see the edges of the map with the overlay active, try decreasing the -value for ``SEGMENTSIZE_XY`` -- normal values are ``50`` to ``80``, depending -on your screen resolution. +or be moved independently. Configuration for Stonesense can be set in the +``dfhack-config/stonesense/init.txt`` file in your DF game directory. .. figure:: ../images/stonesense-roadtruss.jpg :align: center @@ -37,10 +33,9 @@ Mouse controls are hard-coded and cannot be changed. :Scrollwheel: Move up and down :Ctrl-Scroll: Increase/decrease Z depth shown -Follow mode makes the Stonesense view follow the location of the DF -window. The offset can be adjusted by holding :kbd:`Ctrl` while using the -keyboard window movement keys. When you turn on cursor follow mode, the -Stonesense debug cursor will follow the DF cursor when the latter exists. +Follow mode makes the Stonesense view follow the location and zoom level of the DF +window. The offset can be adjusted by holding :kbd:`Alt` while using the +keyboard window movement keys. You can take screenshots with :kbd:`F5`, larger screenshots with :kbd:`Ctrl`:kbd:`F5`, and screenshot the whole map at full resolution with @@ -57,14 +52,14 @@ views, fog, and rotation. Here's the important section: :end-before: VALID ACTIONS: -Streaming stonesense on Windows +Streaming Stonesense on Windows ------------------------------- -If you wish to stream stonesense thru a broadcasting software such as `OBS `_ -then you may find that opening stonesense causes your main DF window to flicker -between DF and stonesense. While it is unknown exactly what causes this, a fix -does exist. Simply make sure that both DF and stonesense are using ``Window Capture`` +If you wish to stream Stonesense thru a broadcasting software such as `OBS `_ +then you may find that opening Stonesense causes your main DF window to flicker +between DF and Stonesense. While it is unknown exactly what causes this, a fix +does exist. Simply make sure that both DF and Stonesense are using ``Window Capture`` and NOT ``Game Capture``. This will stop the flickering from happening and enable -you to stream stonesense for all to enjoy. This has been tested in OBS on Windows 10 but +you to stream Stonesense for all to enjoy. This has been tested in OBS on Windows 10 but should work on Windows 11 and in `Streamlabs `_. Linux, having no ``Game Capture`` option should be unaffected by this issue. @@ -75,13 +70,37 @@ If Stonesense gives an error saying that it can't load detail sprites used. Either open :file:`creatures/init.txt` and remove the line containing that folder, or :dffd:`use these smaller sprites <6096>`. +Sometimes if you have opened Stonesense and then resize the DF window, DF will appear to be +unresponsive. This bug is graphical only and if you hit :kbd:`Ctrl`:kbd:`Alt`:kbd:`S` and wait +a minute or so (since you can't see when the game finishes saving) the game should quicksave. + +If you have Stonesense open in a fort and want to load a new fort, you MUST close Stonesense before +loading the new fort or the game will crash. + Stonesense requires working graphics acceleration, and we recommend at least a dual core CPU to avoid slowing down your game of DF. +Yellow cubes and missing sprites +-------------------------------- +If you are seeing yellow cubes in Stonesense, then there is something on the map that +Stonesense does not have a sprite for. + +.. figure:: ../images/stonesense-yellowcubes.png + :align: center + + An example of the yellow cubes. + +If you would like to help us in fixing this, there are two things you can do: + +* Make an issue on `GitHub `_ with what + item is missing and pictures of what it looks like in DF. +* Create the art yourself. For help with this, please see the `stonesense-art-guide`. + Useful links ------------ - Report issues on `Github `_ - `support` +- `Stonesense Subreddit `_ - :forums:`Official Stonesense thread <106497>` - :forums:`Screenshots thread <48172>` - :wiki:`Main wiki page ` diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index b7d87196e2..e9f0033783 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -29,6 +29,10 @@ Options Make the strange mood strike the selected unit instead of picking one randomly. Unit eligibility is still enforced (unless ``--force`` is also specified). +``--id`` + Make the strange mood strike the unit with the given id instead of picking one + randomly. Unit eligibility is still enforced (unless ``--force`` is also + specified). ``--type `` Force the mood to be of a particular type instead of choosing randomly based on happiness. Valid values are "fey", "secretive", "possessed", "fell", and diff --git a/docs/plugins/suspendmanager.rst b/docs/plugins/suspendmanager.rst index 6015e3dc11..b92c50e2f0 100644 --- a/docs/plugins/suspendmanager.rst +++ b/docs/plugins/suspendmanager.rst @@ -19,6 +19,9 @@ When enabled, ``suspendmanager`` will watch your active jobs and: which would cause the designation to be lost. - suspend construction jobs that would cave in immediately on completion, such as when building walls or floors next to grates/bars. +- suspend deconstruction jobs on machine components (gears, axles, + waterwheels, etc.) whose removal would leave part of the machine without + support and collapse it. See `gui/suspendmanager` for a graphical configuration interface. diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index c1732a741d..9e59957810 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -28,6 +28,7 @@ Usage tailor now tailor materials [ ...] tailor confiscate [true|false] + tailor dye [true|false] By default, ``tailor`` will prefer using materials in this order:: @@ -48,6 +49,11 @@ too precious to routinely make into cloth. ``tailor`` does not support modded "cloth" types which utilize custom reactions for making clothing out of those cloth types. +If dye management is enabled, ``tailor`` will also issue orders to dye cloth +as needed to fulfill fortress clothing requirements, and will issue orders to +manufacture dyes required to fulfill the dye cloth orders. Dye management is +disabled by default. + Examples -------- @@ -69,3 +75,11 @@ Caveats Modded cloth-like materials are not supported because custom reactions do not support being sized for non-dwarf races. The game only supports sizing the built-in default make-clothing or make-armor reactions. + +Dye automation will not not issue orders to mill dyes that are made at a +millstone or quern because Dwarf Fortress does not currently support +"mill plant" orders for a specified material. + +At present, dye automation uses any available dye and cloth will be dyed +with whatever color dye can be found or made. The ability to specify color +preferences may be added in the future. diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 87361ef670..c369960344 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -38,10 +38,16 @@ Commands ``adamantine-cloth-wear`` Prevents adamantine clothing from wearing out while being worn (:bug:`6481`). +``animaltrap-reuse`` + Makes built animal traps unload caught vermin into stockpiled animal traps + so that they can be automatically re-baited and reused. ``craft-age-wear`` Fixes crafted items not wearing out over time (:bug:`6003`). With this tweak, items made from cloth and leather will gain a level of wear every 20 in-game years. +``drawbridge-tiles`` + Makes raising bridges in ASCII mode render with different tiles when they + are raised. ``eggs-fertile`` Displays an indicator on fertile eggs. ``fast-heat`` diff --git a/docs/sphinx_extensions/dfhack/tool_docs.py b/docs/sphinx_extensions/dfhack/tool_docs.py index 6c65e5918d..3d385d8656 100644 --- a/docs/sphinx_extensions/dfhack/tool_docs.py +++ b/docs/sphinx_extensions/dfhack/tool_docs.py @@ -143,7 +143,12 @@ def add_index_entries(self, name) -> None: if 'unavailable' not in tags: self.env.domaindata['all']['objects'].append(indexdata) for tag in tags: - self.env.domaindata[tag]['objects'].append(indexdata) + # tolerate tags missing from Tags.rst: the scripts submodule pin + # can lag behind tag renames in either repo + if tag in self.env.domaindata: + self.env.domaindata[tag]['objects'].append(indexdata) + else: + logger.info('%s: ignoring unregistered tag: %s', docname, tag) @staticmethod def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition: @@ -173,16 +178,22 @@ def render_content(self) -> List[nodes.Node]: tags = self.options.get('tags', []) self.env.domaindata['tag-repo']['doctags'][self.env.docname] = tags for tag in tags: - tag_paragraph += [ - addnodes.pending_xref(tag, nodes.inline(text=tag), **{ - 'reftype': 'ref', - 'refdomain': 'std', - 'reftarget': tag + '-tag-index', - 'refexplicit': True, - 'refwarn': True, - }), - nodes.inline(text=' | '), - ] + # link only tags registered in Tags.rst; the scripts submodule + # pin can lag behind tag renames in either repo + if tag in self.env.domaindata: + tag_paragraph += addnodes.pending_xref(tag, + nodes.inline(text=tag), **{ + 'reftype': 'ref', + 'refdomain': 'std', + 'reftarget': tag + '-tag-index', + 'refexplicit': True, + 'refwarn': True, + }) + else: + logger.info('%s: ignoring unregistered tag: %s', + self.env.docname, tag) + tag_paragraph += nodes.inline(text=tag) + tag_paragraph += nodes.inline(text=' | ') tag_paragraph.pop() ret_nodes = [tag_paragraph] diff --git a/docs/styles/dfhack.css b/docs/styles/dfhack.css index c1a00ed908..a8a1e3237c 100644 --- a/docs/styles/dfhack.css +++ b/docs/styles/dfhack.css @@ -79,3 +79,12 @@ div.dfhack-tool-summary p:last-child, aside.dfhack-tool-summary p:last-child { margin-bottom: 0; } + +table.dfhack-param-table { + table-layout: fixed; + width: 110%; +} + +table.dfhack-param-table span.pre { + white-space: wrap; +} diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 37f5b12d5e..117c0f5e8e 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -48,9 +48,12 @@ set(MAIN_HEADERS include/DFHackVersion.h include/BitArray.h include/ColorText.h + include/Commands.h include/Console.h + include/CoordTemplate.h include/Core.h include/CoreDefs.h + include/Crashlog.h include/DataDefs.h include/DataFuncs.h include/DataIdentity.h @@ -58,15 +61,16 @@ set(MAIN_HEADERS include/DebugManager.h include/Error.h include/Export.h + include/Format.h + include/HashUtil.h include/Hooks.h include/LuaTools.h include/LuaWrapper.h include/MemAccess.h include/Memory.h include/MiscUtils.h - include/Module.h include/MemAccess.h - include/ModuleFactory.h + include/MemoryPatcher.h include/PluginLua.h include/PluginManager.h include/PluginStatics.h @@ -88,6 +92,7 @@ set(MAIN_HEADERS_WINDOWS set(MAIN_SOURCES Core.cpp ColorText.cpp + Commands.cpp CompilerWorkAround.cpp DataDefs.cpp DataIdentity.cpp @@ -100,11 +105,13 @@ set(MAIN_SOURCES LuaApi.cpp DataStatics.cpp DataStaticsCtor.cpp + MemoryPatcher.cpp MiscUtils.cpp Types.cpp PluginManager.cpp PluginStatics.cpp PlugLoad.cpp + Process.cpp TileTypes.cpp VersionInfoFactory.cpp RemoteClient.cpp @@ -125,8 +132,7 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} - Hooks.cpp - Process-windows.cpp + Crashlog-windows.cpp ) if(WIN32) @@ -136,13 +142,12 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} - Process-linux.cpp + Crashlog.cpp ) set(MAIN_SOURCES_DARWIN ${CONSOLE_SOURCES} PlugLoad-posix.cpp - Process-darwin.cpp ) set(MODULE_HEADERS @@ -154,9 +159,9 @@ set(MODULE_HEADERS include/modules/Designations.h include/modules/EventManager.h include/modules/Filesystem.h - include/modules/Graphic.h include/modules/Gui.h include/modules/GuiHooks.h + include/modules/Hotkey.h include/modules/Items.h include/modules/Job.h include/modules/Kitchen.h @@ -185,8 +190,8 @@ set(MODULE_SOURCES modules/Designations.cpp modules/EventManager.cpp modules/Filesystem.cpp - modules/Graphic.cpp modules/Gui.cpp + modules/Hotkey.cpp modules/Items.cpp modules/Job.cpp modules/Kitchen.cpp @@ -312,8 +317,6 @@ endif() # Compilation -add_definitions(-DBUILD_DFHACK_LIB) - if(UNIX) if(CONSOLE_NO_CATCH) add_definitions(-DCONSOLE_NO_CATCH) @@ -321,12 +324,12 @@ if(UNIX) endif() if(APPLE) - set(PROJECT_LIBS dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) elseif(UNIX) - set(PROJECT_LIBS rt dl dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS rt dl dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) else(WIN32) # FIXME: do we really need psapi? - set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${DFHACK_TINYXML}) + set(PROJECT_LIBS psapi dbghelp dfhack-md5 ${FMTLIB} ${DFHACK_TINYXML}) endif() set(VERSION_SRCS DFHackVersion.cpp) @@ -352,21 +355,55 @@ if(DFHACK_PRERELEASE) endif() configure_file(git-describe.cmake.in ${CMAKE_CURRENT_SOURCE_DIR}/git-describe.cmake @ONLY) -if(EXISTS ${dfhack_SOURCE_DIR}/.git/index AND EXISTS ${dfhack_SOURCE_DIR}/.git/modules/library/xml/index) - add_custom_command(OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/include/git-describe.h - COMMAND ${CMAKE_COMMAND} - -D dfhack_SOURCE_DIR:STRING=${dfhack_SOURCE_DIR} - -D GIT_EXECUTABLE:STRING=${GIT_EXECUTABLE} - -P ${CMAKE_CURRENT_SOURCE_DIR}/git-describe.cmake - COMMENT "Obtaining git commit information" - DEPENDS ${dfhack_SOURCE_DIR}/.git/index - ${dfhack_SOURCE_DIR}/.git/modules/library/xml/index - ${CMAKE_CURRENT_SOURCE_DIR}/git-describe.cmake - include/git-describe.h.in - ) -endif() + +# .git is a directory in a normal repository, but a file containing +# "gitdir: " in git worktrees and submodules; resolve both. +function(dfhack_find_git_dir repo_dir out_var) + set(${out_var} "" PARENT_SCOPE) + if(IS_DIRECTORY "${repo_dir}/.git") + set(${out_var} "${repo_dir}/.git" PARENT_SCOPE) + elseif(EXISTS "${repo_dir}/.git") + file(STRINGS "${repo_dir}/.git" gitdir_line REGEX "^gitdir:" LIMIT_COUNT 1) + if(gitdir_line MATCHES "^gitdir:[ \t]*(.+)") + string(STRIP "${CMAKE_MATCH_1}" gitdir) + if(NOT IS_ABSOLUTE "${gitdir}") + get_filename_component(gitdir "${repo_dir}/${gitdir}" ABSOLUTE) + endif() + set(${out_var} "${gitdir}" PARENT_SCOPE) + endif() + endif() +endfunction() + +# Re-run git describe when HEAD or the index changes in the main repository or +# in the library/xml submodule, when they are present. The header is generated +# even outside a git checkout so the build works on plain source trees too. +set(GIT_DESCRIBE_DEPS + ${CMAKE_CURRENT_SOURCE_DIR}/git-describe.cmake + ${CMAKE_CURRENT_SOURCE_DIR}/include/git-describe.h.in +) +dfhack_find_git_dir("${dfhack_SOURCE_DIR}" DFHACK_GIT_DIR) +dfhack_find_git_dir("${dfhack_SOURCE_DIR}/library/xml" DFHACK_XML_GIT_DIR) +foreach(git_dir "${DFHACK_GIT_DIR}" "${DFHACK_XML_GIT_DIR}") + if(git_dir) + foreach(name HEAD index) + if(EXISTS "${git_dir}/${name}") + list(APPEND GIT_DESCRIBE_DEPS "${git_dir}/${name}") + endif() + endforeach() + endif() +endforeach() + +add_custom_command(OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/include/git-describe.h +COMMAND ${CMAKE_COMMAND} + -D dfhack_SOURCE_DIR:STRING=${dfhack_SOURCE_DIR} + -D GIT_EXECUTABLE:STRING=${GIT_EXECUTABLE} + -P ${CMAKE_CURRENT_SOURCE_DIR}/git-describe.cmake +COMMENT "Obtaining git commit information" +DEPENDS ${GIT_DESCRIBE_DEPS} +) add_library(dfhack SHARED ${PROJECT_SOURCES}) +target_compile_definitions(dfhack PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) get_target_property(xlsxio_INCLUDES xlsxio_read_STATIC INTERFACE_INCLUDE_DIRECTORIES) @@ -375,6 +412,7 @@ add_dependencies(dfhack generate_proto_core) add_dependencies(dfhack generate_headers) add_library(dfhack-client SHARED RemoteClient.cpp ColorText.cpp MiscUtils.cpp Error.cpp ${PROJECT_PROTO_SRCS} ${CONSOLE_SOURCES}) +target_compile_definitions(dfhack-client PRIVATE BUILD_DFHACK_LIB) target_include_directories(dfhack-client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/proto) add_dependencies(dfhack-client dfhack) @@ -385,16 +423,16 @@ add_executable(binpatch binpatch.cpp) target_link_libraries(binpatch dfhack-md5) if(WIN32) - set_target_properties(dfhack PROPERTIES OUTPUT_NAME "dfhooks_dfhack" ) set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) else() set_target_properties(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) set_target_properties(dfhack-client PROPERTIES COMPILE_FLAGS "-include Export.h" ) - add_library(dfhooks_dfhack SHARED Hooks.cpp) - target_link_libraries(dfhooks_dfhack dfhack) endif() +add_library(dfhooks_dfhack SHARED Hooks.cpp) +target_link_libraries(dfhooks_dfhack PUBLIC dfhack lua ${FMTLIB}) + # effectively disables debug builds... set_target_properties(dfhack PROPERTIES DEBUG_POSTFIX "-debug" ) @@ -421,7 +459,7 @@ endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static ${FMTLIB}) if(WIN32) target_link_libraries(dfhack-client dbghelp) endif() @@ -444,11 +482,13 @@ if(UNIX) install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack-run DESTINATION .) endif() - install(TARGETS dfhooks_dfhack - LIBRARY DESTINATION . - RUNTIME DESTINATION .) endif() +install(TARGETS dfhooks_dfhack + LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} + RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) + + # install the main lib install(TARGETS dfhack LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} @@ -458,6 +498,12 @@ install(TARGETS dfhack-run dfhack-client binpatch LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + CONTENT "${DFHACK_DATA_DESTINATION}/$") + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dfhooks_dfhack.ini + DESTINATION .) + endif(BUILD_LIBRARY) # install the offset file diff --git a/library/ColorText.cpp b/library/ColorText.cpp index 8dc5cc5dcf..a101d795a9 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -35,24 +35,12 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include #include +#include +#include #include "ColorText.h" -#include "MiscUtils.h" - -#include -#include -#include -#include -using namespace std; using namespace DFHack; bool color_ostream::log_errors_to_stderr = false; @@ -81,7 +69,7 @@ void color_ostream::end_batch() flush_proxy(); } -color_ostream::color_ostream() : ostream(new buffer(this)), cur_color(COLOR_RESET) +color_ostream::color_ostream() : std::ostream(new buffer(this)), cur_color(COLOR_RESET) { // } @@ -91,54 +79,6 @@ color_ostream::~color_ostream() delete buf(); } -void color_ostream::print(const char *format, ...) -{ - va_list args; - va_start(args, format); - vprint(format, args); - va_end(args); -} - -void color_ostream::vprint(const char *format, va_list args) -{ - std::string str = stl_vsprintf(format, args); - - if (!str.empty()) { - flush_buffer(false); - add_text(cur_color, str); - if (str[str.size()-1] == '\n') - flush_proxy(); - } -} - -void color_ostream::printerr(const char * format, ...) -{ - va_list args; - va_start(args, format); - vprinterr(format, args); - va_end(args); -} - -void color_ostream::vprinterr(const char *format, va_list args) -{ - color_value save = cur_color; - - if (log_errors_to_stderr) - { - va_list args1; - va_copy(args1, args); - vfprintf(stderr, format, args1); - va_end(args1); - } - - color(COLOR_LIGHTRED); - va_list args2; - va_copy(args2, args); - vprint(format, args2); - va_end(args2); - color(save); -} - void color_ostream::color(color_value c) { if (c == cur_color) diff --git a/library/Commands.cpp b/library/Commands.cpp new file mode 100644 index 0000000000..b19ed68bb4 --- /dev/null +++ b/library/Commands.cpp @@ -0,0 +1,589 @@ + +#include "Commands.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "RemoteTools.h" + +#include "modules/Gui.h" +#include "modules/Hotkey.h" +#include "modules/World.h" + +#include "df/viewscreen_new_regionst.h" + +#include +#include +#include + + +namespace DFHack +{ + command_result Commands::help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!parts.size()) + { + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Here are some basic commands to get you started:\n" + " help|?|man - This text.\n" + " help - Usage help for the given plugin, command, or script.\n" + " tags - List the tags that the DFHack tools are grouped by.\n" + " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" + " Optional parameters:\n" + " --notags: skip printing tags for each command.\n" + " --dev: include commands intended for developers and modders.\n" + " cls|clear - Clear the console.\n" + " fpause - Force DF to pause.\n" + " die - Force DF to close immediately, without saving.\n" + " keybinding - Modify bindings of commands to in-game key shortcuts.\n" + "\n" + "See more commands by running 'ls'.\n\n" + ); + + con.print("DFHack version {}\n", dfhack_version_desc()); + } + else + { + DFHack::help_helper(con, parts[0]); + } + return CR_OK; + } + + command_result Commands::load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool all = false; + bool load = (first == "load"); + bool unload = (first == "unload"); + bool reload = (first == "reload"); + auto plug_mgr = core.getPluginManager(); + if (parts.size()) + { + for (const auto& p : parts) + { + if (p.size() && p[0] == '-') + { + if (p.find('a') != std::string::npos) + all = true; + } + } + auto ret = CR_OK; + if (all) + { + if (load && !plug_mgr->loadAll()) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unloadAll()) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reloadAll()) + ret = CR_FAILURE; + } + else + { + for (auto& p : parts) + { + if (p.empty() || p[0] == '-') + continue; + if (load && !plug_mgr->load(p)) + ret = CR_FAILURE; + else if (unload && !plug_mgr->unload(p)) + ret = CR_FAILURE; + else if (reload && !plug_mgr->reload(p)) + ret = CR_FAILURE; + } + } + if (ret != CR_OK) + con.printerr("{} failed\n", first.c_str()); + return ret; + } + else + { + con.printerr("{}: no arguments\n", first.c_str()); + return CR_FAILURE; + } + + } + + command_result Commands::enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + CoreSuspender suspend; + bool enable = (first == "enable"); + auto plug_mgr = core.getPluginManager(); + + if (parts.size()) + { + command_result res{CR_FAILURE}; + + for (auto& part_ : parts) + { + // have to copy to modify as passed argument is const + std::string part(part_); + + if (has_backslashes(part)) + { + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", part); + replace_backslashes_with_forwardslashes(part); + } + + auto alias = core.GetAliasCommand(part, true); + + Plugin* plug = (*plug_mgr)[alias]; + + if (!plug) + { + std::filesystem::path lua = core.findScript(part + ".lua"); + if (!lua.empty()) + { + res = core.enableLuaScript(con, part, enable); + } + else + { + res = CR_NOT_FOUND; + con.printerr("No such plugin or Lua script: {}\n", part); + } + } + else if (!plug->can_set_enabled()) + { + res = CR_NOT_IMPLEMENTED; + con.printerr("Cannot {} plugin: {}\n", first, part); + } + else + { + res = plug->set_enabled(con, enable); + + if (res != CR_OK || plug->is_enabled() != enable) + con.printerr("Could not {} plugin: {}\n", first, part); + } + } + + return res; + } + else + { + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + if (!plug->can_be_enabled()) continue; + + con.print( + "{:>21} {:<3}{}\n", + (key + ":").c_str(), + plug->is_enabled() ? "on" : "off", + plug->can_set_enabled() ? "" : " (controlled internally)" + ); + } + + Lua::CallLuaModuleFunction(con, "script-manager", "list"); + + return CR_OK; + } + } + + command_result Commands::plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + constexpr auto header_format = "{:30} {:10} {:4} {:8}\n"; + constexpr auto row_format = header_format; + + con.print(header_format, "Name", "State", "Cmds", "Enabled"); + + auto plug_mgr = core.getPluginManager(); + + plug_mgr->refresh(); + for (auto& [key, plug] : *plug_mgr) + { + if (!plug) + continue; + + if (parts.size() && std::ranges::find(parts, key) == parts.end()) + continue; + + color_value color; + switch (plug->getState()) + { + case Plugin::PS_LOADED: + color = COLOR_RESET; + break; + case Plugin::PS_UNLOADED: + case Plugin::PS_UNLOADING: + color = COLOR_YELLOW; + break; + case Plugin::PS_LOADING: + color = COLOR_LIGHTBLUE; + break; + case Plugin::PS_BROKEN: + color = COLOR_LIGHTRED; + break; + default: + color = COLOR_LIGHTMAGENTA; + break; + } + con.color(color); + con.print(row_format, + plug->getName(), + Plugin::getStateDescription(plug->getState()), + plug->size(), + (plug->can_be_enabled() + ? (plug->is_enabled() ? "enabled" : "disabled") + : "n/a") + ); + con.color(COLOR_RESET); + } + + return CR_OK; + } + + command_result Commands::type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + auto plug_mgr = core.getPluginManager(); + + if (!parts.size()) + { + con.printerr("type: no argument\n"); + return CR_WRONG_USAGE; + } + con << parts[0]; + bool builtin = is_builtin(con, parts[0]); + std::filesystem::path lua_path = core.findScript(parts[0] + ".lua"); + Plugin* plug = plug_mgr->getPluginByCommand(parts[0]); + if (builtin) + { + con << " is a built-in command"; + con << std::endl; + } + else if (core.IsAlias(parts[0])) + { + con << " is an alias: " << core.GetAliasCommand(parts[0]) << std::endl; + } + else if (plug) + { + con << " is a command implemented by the plugin " << plug->getName() << std::endl; + } + else if (!lua_path.empty()) + { + con << " is a Lua script: " << lua_path << std::endl; + } + else + { + con << " is not a recognized command." << std::endl; + plug = plug_mgr->getPluginByName(parts[0]); + if (plug) + con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + using Hotkey::KeySpec; + auto hotkey_mgr = core.getHotkeyManager(); + std::string parse_error; + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) { + const std::string& keystr = parts[1]; + if (parts[0] == "set") + hotkey_mgr->removeKeybind(keystr); + for (const auto& part : parts | std::views::drop(2) | std::views::reverse) { + auto spec = KeySpec::parse(keystr, &parse_error); + if (!spec.has_value()) { + con.printerr("{}\n", parse_error); + break; + } + if (!hotkey_mgr->addKeybind(spec.value(), part)) { + con.printerr("Invalid command: '{}'\n", part); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") { + for (const auto& part : parts | std::views::drop(1)) { + auto spec = KeySpec::parse(part, &parse_error); + if (!spec.has_value()) { + con.printerr("{}\n", parse_error); + } + if (!hotkey_mgr->removeKeybind(spec.value())) { + con.printerr("No matching keybinds to remove\n"); + break; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") { + auto spec = KeySpec::parse(parts[1], &parse_error); + if (!spec.has_value()) { + con.printerr("{}\n", parse_error); + return CR_FAILURE; + } + std::vector list = hotkey_mgr->listKeybinds(spec.value()); + if (list.empty()) + con << "No bindings." << std::endl; + for (const auto& kb : list) + con << " " << kb << std::endl; + } + else + { + con << "Usage:\n" + << " keybinding list \n" + << " keybinding clear [@context]...\n" + << " keybinding set [@context] \"cmdline\" \"cmdline\"...\n" + << " keybinding add [@context] \"cmdline\" \"cmdline\"...\n" + << "Later adds, and earlier items within one command have priority.\n" + << "Key format: [Ctrl-][Alt-][Super-][Shift-](A-Z, 0-9, F1-F12, `, etc.).\n" + << "Context may be used to limit the scope of the binding, by\n" + << "requiring the current context to have a certain prefix.\n" + << "Current UI context is: \n" + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; + } + return CR_OK; + } + + command_result Commands::alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) + { + const std::string& name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); + if (!core.AddAlias(name, cmd, parts[0] == "replace")) + { + con.printerr("Could not add alias {} - already exists\n", name); + return CR_FAILURE; + } + } + else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) + { + if (!core.RemoveAlias(parts[1])) + { + con.printerr("Could not remove alias {}\n", parts[1]); + return CR_FAILURE; + } + } + else if (parts.size() >= 1 && (parts[0] == "list")) + { + auto aliases = core.ListAliases(); + for (auto p : aliases) + { + con << p.first << ": " << join_strings(" ", p.second) << std::endl; + } + } + else + { + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; + } + + return CR_OK; + } + + command_result Commands::fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (auto scr = Gui::getViewscreenByType()) + { + if (scr->doing_mods || scr->doing_simple_params || scr->doing_params) + { + con.printerr("Cannot pause now.\n"); + return CR_FAILURE; + } + scr->abort_world_gen_dialogue = true; + } + else + { + World::SetPauseState(true); + } + con.print("The game was forced to pause!\n"); + return CR_OK; + } + + command_result Commands::clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (con.can_clear()) + { + con.clear(); + return CR_OK; + } + else + { + con.printerr("No console to clear, or this console does not support clearing.\n"); + return CR_NEEDS_CONSOLE; + } + } + + command_result Commands::kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + bool force = std::ranges::any_of(parts, [] (const std::string& part) { return part == "force"; }); + if (!Lua::Interrupt(force)) + { + con.printerr( + "Failed to register hook. This can happen if you have" + " lua profiling or coverage monitoring enabled. Use" + " 'kill-lua force' to force, but this may disable" + " profiling and coverage monitoring.\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + core.loadScriptFile(con, std::filesystem::weakly_canonical(std::filesystem::path{parts[0]}), false); + return CR_OK; + } + else + { + con << "Usage:" << std::endl + << " script " << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().show()) + { + con.printerr("Could not show console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (!core.getConsole().hide()) + { + con.printerr("Could not hide console\n"); + return CR_FAILURE; + } + return CR_OK; + } + + command_result Commands::sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.empty() || parts[0] == "help" || parts[0] == "?") + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; + for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) + { + std::string name = sc_event_name((state_change_event)i); + if (name != "SC_UNKNOWN") + con << " " << name << std::endl; + } + return CR_OK; + } + else if (parts[0] == "list") + { + std::string event_name = parts.size() >= 2 ? parts[1] : ""; + if (event_name.size() && sc_event_id(event_name) == SC_UNKNOWN) + { + con << "Unrecognized event name: " << parts[1] << std::endl; + return CR_WRONG_USAGE; + } + for (const auto& state_script : core.getStateChangeScripts()) + { + if (!parts[1].size() || (state_script.event == sc_event_id(parts[1]))) + { + con.print("{} ({}): {}{}\n", sc_event_name(state_script.event), + state_script.save_specific ? "save-specific" : "global", + state_script.save_specific ? "/raw/" : "/", + state_script.path); + } + } + return CR_OK; + } + else if (parts[0] == "add") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript script(evt, parts[2], save_specific); + for (const auto& state_script : core.getStateChangeScripts()) + { + if (script == state_script) + { + con << "Script already registered" << std::endl; + return CR_FAILURE; + } + } + core.addStateChangeScript(script); + return CR_OK; + } + else if (parts[0] == "remove") + { + if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) + { + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; + return CR_WRONG_USAGE; + } + state_change_event evt = sc_event_id(parts[1]); + if (evt == SC_UNKNOWN) + { + con << "Unrecognized event: " << parts[1] << std::endl; + return CR_FAILURE; + } + bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); + StateChangeScript tmp(evt, parts[2], save_specific); + if (core.removeStateChangeScript(tmp)) + { + return CR_OK; + } + else + { + con << "Unrecognized script" << std::endl; + return CR_FAILURE; + } + } + else + { + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + return CR_WRONG_USAGE; + } + } + + command_result Commands::dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts) + { + if (parts.size() == 1) + { + std::ofstream file(parts[0]); + CoreService coreSvc; + coreSvc.dumpMethods(file); + + for (auto& it : *core.getPluginManager()) + { + Plugin* plug = it.second; + if (!plug) + continue; + + std::unique_ptr svc(plug->rpc_connect(con)); + if (!svc) + continue; + + file << "// Plugin: " << plug->getName() << std::endl; + svc->dumpMethods(file); + } + } + else + { + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; + return CR_WRONG_USAGE; + } + return CR_OK; + } +} diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 85934b2f37..908508bb3b 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -36,6 +36,7 @@ POSSIBILITY OF SUCH DAMAGE. */ +#define NOMINMAX #include #include #include @@ -45,6 +46,7 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -72,11 +74,15 @@ namespace DFHack rawmode = 0; console_in = 0; console_out = 0; + exiting = false; ConsoleWindow = 0; default_attributes = 0; state = con_unclaimed; in_batch = false; raw_cursor = 0; + console_owned = false; + saved_cp = 0; + saved_output_cp = 0; }; virtual ~Private() { @@ -218,10 +224,7 @@ namespace DFHack { COORD pos = { (SHORT)x, (SHORT)y }; DWORD count = 0; - CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; - GetConsoleScreenBufferInfo(console_out, &inf); - SetConsoleCursorPosition(console_out, pos); - WriteConsoleA(console_out, str, len, &count, NULL); + WriteConsoleOutputCharacterA(console_out, str, len, pos, &count); } void prompt_refresh() @@ -232,17 +235,17 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - while ((plen + cooked_cursor) >= cols) + if (plen + cooked_cursor > (cols-1)) { - buf++; - len--; - cooked_cursor--; - } - while (plen + len > cols) - { - len--; + int adj = std::min(plen + cooked_cursor - (cols-1), len); + buf += adj; + len -= adj; + cooked_cursor -= adj; } + if (len + plen > cols) + len = cols - plen; + CONSOLE_SCREEN_BUFFER_INFO inf = { 0 }; GetConsoleScreenBufferInfo(console_out, &inf); output(prompt.c_str(), plen, 0, inf.dwCursorPosition.Y); @@ -282,11 +285,12 @@ namespace DFHack INPUT_RECORD rec; DWORD count; lock->unlock(); - if (ReadConsoleInputA(console_in, &rec, 1, &count) == 0) { - lock->lock(); - return Console::SHUTDOWN; - } + // shutdown() queues a dummy input record to wake this read; + // the exiting flag distinguishes the wake from real input. + BOOL success = ReadConsoleInputA(console_in, &rec, 1, &count); lock->lock(); + if (exiting || !success) + return Console::SHUTDOWN; if (rec.EventType != KEY_EVENT || !rec.Event.KeyEvent.bKeyDown) continue; switch (rec.Event.KeyEvent.wVirtualKeyCode) @@ -392,9 +396,13 @@ namespace DFHack int rawmode; HANDLE console_in; HANDLE console_out; + std::atomic exiting; HWND ConsoleWindow; HWND MainWindow; WORD default_attributes; + bool console_owned; // false if the console was inherited from a parent process + UINT saved_cp; // code pages to restore on shutdown if the console is borrowed + UINT saved_output_cp; // current state enum console_state { @@ -466,18 +474,32 @@ bool Console::init(bool) } d->MainWindow = h; - // Allocate a console! - AllocConsole(); + // Allocate a console! If the process already has one (e.g. dfhack-run + // launched from an existing terminal), the console is borrowed and must + // not be mutated or freed. + d->console_owned = AllocConsole() != FALSE; d->ConsoleWindow = GetConsoleWindow(); wlock = new std::recursive_mutex(); - HMENU hm = GetSystemMenu(d->ConsoleWindow,false); - DeleteMenu(hm, SC_CLOSE, MF_BYCOMMAND); + if (d->console_owned) + { + HMENU hm = GetSystemMenu(d->ConsoleWindow,false); + DeleteMenu(hm, SC_CLOSE, MF_BYCOMMAND); + } + + // force console code pages to utf-8 + d->saved_cp = GetConsoleCP(); + d->saved_output_cp = GetConsoleOutputCP(); + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); // set the screen buffer to be big enough to let us scroll text GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); d->default_attributes = coninfo.wAttributes; - coninfo.dwSize.Y = MAX_CONSOLE_LINES; // How many lines do you want to have in the console buffer - SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); + if (d->console_owned) + { + coninfo.dwSize.Y = MAX_CONSOLE_LINES; // How many lines do you want to have in the console buffer + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); + } // redirect unbuffered STDOUT to the console d->console_out = GetStdHandle(STD_OUTPUT_HANDLE); @@ -501,7 +523,10 @@ bool Console::init(bool) // make our own weird streams so our IO isn't redirected std::cin.tie(this); - clear(); + if (d->console_owned) + { + clear(); + } inited = true; // DOESN'T WORK - locks up DF! // ForceForegroundWindow(d->MainWindow); @@ -511,8 +536,27 @@ bool Console::init(bool) // FIXME: looks awfully empty, doesn't it? bool Console::shutdown(void) { + assert(inited); + d->exiting = true; std::lock_guard lock{*wlock}; - FreeConsole(); + // Wake any thread blocked in ReadConsoleInputA by queueing a dummy + // input record, so it is no longer inside a console call and can + // observe the exiting flag before we detach the console below. + INPUT_RECORD rec = { 0 }; + rec.EventType = FOCUS_EVENT; + DWORD written; + WriteConsoleInputA(d->console_in, &rec, 1, &written); + if (d->console_owned) + { + FreeConsole(); + } + else + { + // the console is borrowed from a parent process; restore the state + // we changed and leave it attached + SetConsoleCP(d->saved_cp); + SetConsoleOutputCP(d->saved_output_cp); + } inited = false; return true; } @@ -605,12 +649,16 @@ void Console::msleep (unsigned int msec) bool Console::hide() { + if (!inited || !d->console_owned) + return false; ShowWindow( GetConsoleWindow(), SW_HIDE ); return true; } bool Console::show() { + if (!inited || !d->console_owned) + return false; ShowWindow( GetConsoleWindow(), SW_RESTORE ); return true; } diff --git a/library/Core.cpp b/library/Core.cpp index 9dc2d2e68a..09b9660345 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -22,38 +22,45 @@ must not be misrepresented as being the original software. distribution. */ +#include "Core.h" +#include "Crashlog.h" + #include "Internal.h" -#include "Error.h" -#include "MemAccess.h" -#include "Core.h" +#include "ColorText.h" +#include "Commands.h" +#include "Console.h" +#include "CoreDefs.h" #include "DataDefs.h" #include "Debug.h" -#include "Console.h" +#include "DFHackVersion.h" +#include "Error.h" +#include "Format.h" +#include "LuaTools.h" +#include "MemAccess.h" +#include "MemoryPatcher.h" +#include "MiscUtils.h" #include "MiscUtils.h" -#include "Module.h" -#include "VersionInfoFactory.h" -#include "VersionInfo.h" #include "PluginManager.h" -#include "ModuleFactory.h" #include "RemoteServer.h" #include "RemoteTools.h" -#include "LuaTools.h" -#include "DFHackVersion.h" -#include "md5wrapper.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" #include "modules/DFSDL.h" #include "modules/DFSteam.h" #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" +#include "modules/Persistence.h" #include "modules/Textures.h" #include "modules/World.h" -#include "modules/Persistence.h" -#include "df/init.h" #include "df/gamest.h" +#include "df/global_objects.h" #include "df/graphic.h" +#include "df/init.h" #include "df/interfacest.h" #include "df/plotinfost.h" #include "df/viewscreen_dwarfmodest.h" @@ -62,28 +69,53 @@ distribution. #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_savegamest.h" -#include "df/world.h" #include "df/world_data.h" +#include "df/world.h" -#include -#include -#include -#include -#include -#include +#include +#include #include -#include -#include -#include -#include +#include +#include #include +#include #include -#include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include + +#include "md5wrapper.h" + #include +#include +#include + +#include + +#ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif #ifdef LINUX_BUILD #include @@ -91,22 +123,20 @@ distribution. using namespace DFHack; using namespace df::enums; + using df::global::init; using df::global::world; using std::string; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder); +static size_t loadScriptFiles(Core* core, color_ostream& out, std::span prefix, const std::filesystem::path& folder); namespace DFHack { + DBG_DECLARE(core, keybinding, DebugCategory::LINFO); + DBG_DECLARE(core, script, DebugCategory::LINFO); -DBG_DECLARE(core,keybinding,DebugCategory::LINFO); -DBG_DECLARE(core,script,DebugCategory::LINFO); - -static const std::string CONFIG_PATH = "dfhack-config/"; -static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/"; + Core* Core::active_instance = nullptr; class MainThread { public: @@ -187,7 +217,7 @@ uint32_t PerfCounters::getUnpausedFps() { struct CommandDepthCounter { - static const int MAX_DEPTH = 20; + static constexpr int MAX_DEPTH = 20; static thread_local int depth; CommandDepthCounter() { depth++; } ~CommandDepthCounter() { depth--; } @@ -197,7 +227,7 @@ thread_local int CommandDepthCounter::depth = 0; void Core::cheap_tokenise(std::string const& input, std::vector& output) { - std::string *cur = NULL; + std::string *cur = nullptr; size_t i = 0; // Check the first non-space character @@ -228,7 +258,7 @@ void Core::cheap_tokenise(std::string const& input, std::vector& ou { unsigned char c = input[i]; if (isspace(c)) { - cur = NULL; + cur = nullptr; } else { if (!cur) { output.push_back(""); @@ -260,50 +290,7 @@ struct IODATA PluginManager * plug_mgr; }; -// A thread function... for handling hotkeys. This is needed because -// all the plugin commands are expected to be run from foreign threads. -// Running them from one of the main DF threads will result in deadlock! -void fHKthread(void * iodata) -{ - Core * core = ((IODATA*) iodata)->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; - if(plug_mgr == 0 || core == 0) - { - std::cerr << "Hotkey thread has croaked." << std::endl; - return; - } - bool keep_going = true; - while(keep_going) - { - std::string stuff = core->getHotkeyCmd(keep_going); // waits on mutex! - if(!stuff.empty()) - { - color_ostream_proxy out(core->getConsole()); - - auto rv = core->runCommand(out, stuff); - - if (rv == CR_NOT_IMPLEMENTED) - out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); - } - } -} - -struct sortable -{ - bool recolor; - std::string name; - std::string description; - //FIXME: Nuke when MSVC stops failing at being C++11 compliant - sortable(bool recolor_,const std::string& name_,const std::string & description_): recolor(recolor_), name(name_), description(description_){}; - bool operator <(const sortable & rhs) const - { - if( name < rhs.name ) - return true; - return false; - }; -}; - -static std::string dfhack_version_desc() +std::string DFHack::dfhack_version_desc() { std::stringstream s; s << Version::dfhack_version() << " "; @@ -317,73 +304,60 @@ static std::string dfhack_version_desc() return s.str(); } -namespace { - struct ScriptArgs { - const std::string *pcmd; - std::vector *pargs; - }; - struct ScriptEnableState { - const std::string *pcmd; - bool pstate; - }; -} - -static bool init_run_script(color_ostream &out, lua_State *state, void *info) +static bool init_run_script(color_ostream &out, lua_State *state, const std::string_view pcmd, const std::span pargs) { - auto args = (ScriptArgs*)info; - if (!lua_checkstack(state, args->pargs->size()+10)) + if (!lua_checkstack(state, pargs.size()+10)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "run_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - for (size_t i = 0; i < args->pargs->size(); i++) - lua_pushstring(state, (*args->pargs)[i].c_str()); + lua_pushlstring(state, pcmd.data(), pcmd.size()); + for (const auto& arg : pargs) + lua_pushstring(state, arg.c_str()); return true; } -static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) +static command_result runLuaScript(color_ostream &out, const std::string_view name, const std::span args) { - ScriptArgs data; - data.pcmd = &name; - data.pargs = &args; + auto init_fn = [name, args](color_ostream& out, lua_State* state) -> bool { + return init_run_script(out, state, name, args); + }; - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_run_script, &data); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(true), init_fn); return ok ? CR_OK : CR_FAILURE; } -static bool init_enable_script(color_ostream &out, lua_State *state, void *info) +static bool init_enable_script(color_ostream &out, lua_State *state, const std::string_view name, bool enable) { - auto args = (ScriptEnableState*)info; if (!lua_checkstack(state, 4)) return false; Lua::PushDFHack(state); lua_getfield(state, -1, "enable_script"); lua_remove(state, -2); - lua_pushstring(state, args->pcmd->c_str()); - lua_pushboolean(state, args->pstate); + lua_pushlstring(state, name.data(), name.size()); + lua_pushboolean(state, enable); return true; } -static command_result enableLuaScript(color_ostream &out, std::string name, bool state) +command_result Core::enableLuaScript(color_ostream &out, const std::string_view name, bool enabled) { - ScriptEnableState data; - data.pcmd = &name; - data.pstate = state; + auto init_fn = [name, enabled](color_ostream& out, lua_State* state) -> bool { + return init_enable_script(out, state, name, enabled); + }; - bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_enable_script, &data); + bool ok = Lua::RunCoreQueryLoop(out, DFHack::Core::getInstance().getLuaState(), init_fn); return ok ? CR_OK : CR_FAILURE; } -command_result Core::runCommand(color_ostream &out, const std::string &command) +command_result Core::runCommand(color_ostream& out, const std::string& command) { if (!command.empty()) { std::vector parts; - Core::cheap_tokenise(command,parts); - if(parts.size() == 0) + Core::cheap_tokenise(command, parts); + if (parts.size() == 0) return CR_NOT_IMPLEMENTED; std::string first = parts[0]; @@ -399,20 +373,23 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream &con, const std::string &command) { +bool DFHack::is_builtin(color_ostream& con, const std::string& command) +{ CoreSuspender suspend; auto L = DFHack::Core::getInstance().getLuaState(); Lua::StackUnwinder top(L); if (!lua_checkstack(L, 1) || - !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) { + !Lua::PushModulePublic(con, L, "helpdb", "is_builtin")) + { con.printerr("Failed to load helpdb Lua code\n"); return false; } Lua::Push(L, command); - if (!Lua::SafeCall(con, L, 1, 1)) { + if (!Lua::SafeCall(con, L, 1, 1)) + { con.printerr("Failed Lua call to helpdb.is_builtin.\n"); return false; } @@ -458,7 +435,7 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: { completed = possible[0]; //fprintf(stderr, "Autocompleted %s to %s\n", , ); - con.printerr("%s is not recognized. Did you mean %s?\n", first.c_str(), completed.c_str()); + con.printerr("{} is not recognized. Did you mean {}?\n", first, completed); return true; } @@ -467,17 +444,17 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: std::string out; for (size_t i = 0; i < possible.size(); i++) out += " " + possible[i]; - con.printerr("%s is not recognized. Possible completions:%s\n", first.c_str(), out.c_str()); + con.printerr("{} is not recognized. Possible completions:{}\n", first, out); return true; } return false; } -bool Core::addScriptPath(std::string path, bool search_before) +bool Core::addScriptPath(std::filesystem::path path, bool search_before) { std::lock_guard lock(script_path_mutex); - std::vector &vec = script_paths[search_before ? 0 : 1]; + auto &vec = script_paths[search_before ? 0 : 1]; if (std::find(vec.begin(), vec.end(), path) != vec.end()) return false; if (!Filesystem::isdir(path)) @@ -486,19 +463,19 @@ bool Core::addScriptPath(std::string path, bool search_before) return true; } -bool Core::setModScriptPaths(const std::vector &mod_script_paths) { +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) +bool Core::removeScriptPath(std::filesystem::path path) { std::lock_guard lock(script_path_mutex); bool found = false; for (int i = 0; i < 2; i++) { - std::vector &vec = script_paths[i]; + auto &vec = script_paths[i]; while (1) { auto it = std::find(vec.begin(), vec.end(), path); @@ -511,48 +488,57 @@ bool Core::removeScriptPath(std::string path) return found; } -void Core::getScriptPaths(std::vector *dest) +void Core::getScriptPaths(std::vector *dest) { std::lock_guard lock(script_path_mutex); dest->clear(); - std::string df_path = this->p->getPath() + "/"; + std::filesystem::path df_pref_path = Filesystem::getBaseDir(); + std::filesystem::path df_install_path = Filesystem::getInstallDir(); for (auto & path : script_paths[0]) dest->emplace_back(path); - dest->push_back(df_path + CONFIG_PATH + "scripts"); + // should this be df_pref_path? probably + dest->push_back(getConfigPath() / "scripts"); if (df::global::world && isWorldLoaded()) { std::string save = World::ReadWorldFolder(); if (save.size()) - dest->emplace_back(df_path + "save/" + save + "/scripts"); + dest->emplace_back(df_pref_path / "save" / save / "scripts"); } - dest->emplace_back(df_path + "hack/scripts"); + dest->emplace_back(getHackPath() / "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::filesystem::path Core::findScript(std::string name) { - std::vector paths; + std::vector paths; getScriptPaths(&paths); - for (auto it = paths.begin(); it != paths.end(); ++it) + for (auto& path : paths) { - std::string path = *it + "/" + name; - if (Filesystem::isfile(path)) - return path; + std::error_code ec; + auto raw_path = path / name; + std::filesystem::path load_path = std::filesystem::weakly_canonical(raw_path, ec); + if (ec) + { + con.printerr("Error loading '{}' ({})\n", raw_path, ec.message()); + continue; + } + + if (Filesystem::isfile(load_path)) + return load_path; } - return ""; + return {}; } -bool loadScriptPaths(color_ostream &out, bool silent = false) +bool Core::loadScriptPaths(color_ostream &out, bool silent) { - using namespace std; - std::string filename(CONFIG_PATH + "script-paths.txt"); - ifstream file(filename); + std::filesystem::path filename{ getConfigPath() / "script-paths.txt" }; + std::ifstream file(filename); if (!file) { if (!silent) - out.printerr("Could not load %s\n", filename.c_str()); + out.printerr("Could not load {}\n", filename); return false; } std::string raw; @@ -560,34 +546,38 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) while (getline(file, raw)) { ++line; - istringstream ss(raw); + std::istringstream ss(raw); char ch; - ss >> skipws; + ss >> std::skipws; if (!(ss >> ch) || ch == '#') continue; - ss >> ws; // discard whitespace + ss >> std::ws; // discard whitespace std::string path; getline(ss, path); if (ch == '+' || ch == '-') { - if (!Core::getInstance().addScriptPath(path, ch == '+') && !silent) - out.printerr("%s:%i: Failed to add path: %s\n", filename.c_str(), line, path.c_str()); + if (!addScriptPath(path, ch == '+') && !silent) + out.printerr("{}:{}: Failed to add path: {}\n", filename, line, path); } else if (!silent) - out.printerr("%s:%i: Illegal character: %c\n", filename.c_str(), line, ch); + out.printerr("{}:{}: Illegal character: {}\n", filename, line, ch); } return true; } static void loadModScriptPaths(color_ostream &out) { - std::vector mod_script_paths; + std::vector mod_script_paths_str; + std::vector mod_script_paths; Lua::CallLuaModuleFunction(out, "script-manager", "get_mod_script_paths", {}, 1, [&](lua_State *L) { - Lua::GetVector(L, mod_script_paths); + Lua::GetVector(L, mod_script_paths_str); }); DEBUG(script,out).print("final mod script paths:\n"); - for (auto & path : mod_script_paths) - DEBUG(script,out).print(" %s\n", path.c_str()); + for (auto& path : mod_script_paths_str) + { + DEBUG(script, out).print(" {}\n", path); + mod_script_paths.push_back(std::filesystem::weakly_canonical(std::filesystem::path{ path })); + } Core::getInstance().setModScriptPaths(mod_script_paths); } @@ -607,7 +597,7 @@ static void sc_event_map_init() { } } -static state_change_event sc_event_id (std::string name) { +state_change_event DFHack::sc_event_id (std::string name) { sc_event_map_init(); auto it = state_change_event_map.find(name); if (it != state_change_event_map.end()) @@ -617,7 +607,7 @@ static state_change_event sc_event_id (std::string name) { return SC_UNKNOWN; } -static std::string sc_event_name (state_change_event id) { +std::string DFHack::sc_event_name (state_change_event id) { sc_event_map_init(); for (auto it = state_change_event_map.begin(); it != state_change_event_map.end(); ++it) { @@ -627,7 +617,7 @@ static std::string sc_event_name (state_change_event id) { return "SC_UNKNOWN"; } -void help_helper(color_ostream &con, const std::string &entry_name) { +void DFHack::help_helper(color_ostream &con, const std::string &entry_name) { ConditionalCoreSuspender suspend{}; if (!suspend) { @@ -675,14 +665,14 @@ void tags_helper(color_ostream &con, const std::string &tag) { } } -void ls_helper(color_ostream &con, const std::vector ¶ms) { +static void ls_helper(color_ostream &con, const std::span params) { std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - std::string exclude_strs = ""; + std::string_view exclude_strs; bool in_exclude = false; - for (auto str : params) { + for (const auto& str : params) { if (in_exclude) exclude_strs = str; else if (str == "--notags") @@ -727,60 +717,25 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s CommandDepthCounter counter; if (!counter.ok()) { - con.printerr("Cannot invoke \"%s\": maximum command depth exceeded (%i)\n", - first.c_str(), CommandDepthCounter::MAX_DEPTH); + con.printerr("Cannot invoke \"{}\": maximum command depth exceeded ({})\n", + first, CommandDepthCounter::MAX_DEPTH); return CR_FAILURE; } if (first.empty()) return CR_NOT_IMPLEMENTED; - if (first.find('\\') != std::string::npos) + if (has_backslashes(first)) { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", first.c_str()); - for (size_t i = 0; i < first.size(); i++) - { - if (first[i] == '\\') - first[i] = '/'; - } + con.printerr("Replacing backslashes with forward slashes in \"{}\"\n", first); + replace_backslashes_with_forwardslashes(first); } // let's see what we actually got command_result res; if (first == "help" || first == "man" || first == "?") { - if(!parts.size()) - { - if (con.is_console()) - { - con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" - "Some basic editing capabilities are included (single-line text editing).\n" - "The console also has a command history - you can navigate it with Up and Down keys.\n" - "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" - "by clicking on the program icon in the top bar of the window.\n\n"); - } - con.print("Here are some basic commands to get you started:\n" - " help|?|man - This text.\n" - " help - Usage help for the given plugin, command, or script.\n" - " tags - List the tags that the DFHack tools are grouped by.\n" - " ls|dir [] - List commands, optionally filtered by a tag or substring.\n" - " Optional parameters:\n" - " --notags: skip printing tags for each command.\n" - " --dev: include commands intended for developers and modders.\n" - " cls|clear - Clear the console.\n" - " fpause - Force DF to pause.\n" - " die - Force DF to close immediately, without saving.\n" - " keybinding - Modify bindings of commands to in-game key shortcuts.\n" - "\n" - "See more commands by running 'ls'.\n\n" - ); - - con.print("DFHack version %s\n", dfhack_version_desc().c_str()); - } - else - { - help_helper(con, parts[0]); - } + return Commands::help(con, *this, first, parts); } else if (first == "tags") { @@ -788,119 +743,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "load" || first == "unload" || first == "reload") { - bool all = false; - bool load = (first == "load"); - bool unload = (first == "unload"); - if (parts.size()) - { - for (auto p = parts.begin(); p != parts.end(); p++) - { - if (p->size() && (*p)[0] == '-') - { - if (p->find('a') != std::string::npos) - all = true; - } - } - auto ret = CR_OK; - if (all) - { - if (load && !plug_mgr->loadAll()) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unloadAll()) - ret = CR_FAILURE; - else if (!plug_mgr->reloadAll()) - ret = CR_FAILURE; - } - for (auto p = parts.begin(); p != parts.end(); p++) - { - if (!p->size() || (*p)[0] == '-') - continue; - if (load && !plug_mgr->load(*p)) - ret = CR_FAILURE; - else if (unload && !plug_mgr->unload(*p)) - ret = CR_FAILURE; - else if (!plug_mgr->reload(*p)) - ret = CR_FAILURE; - } - if (ret != CR_OK) - con.printerr("%s failed\n", first.c_str()); - return ret; - } - else { - con.printerr("%s: no arguments\n", first.c_str()); - return CR_FAILURE; - } + return Commands::load(con, *this, first, parts); } else if( first == "enable" || first == "disable" ) { - CoreSuspender suspend; - bool enable = (first == "enable"); - - if(parts.size()) - { - for (size_t i = 0; i < parts.size(); i++) - { - std::string part = parts[i]; - if (part.find('\\') != std::string::npos) - { - con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str()); - for (size_t j = 0; j < part.size(); j++) - { - if (part[j] == '\\') - part[j] = '/'; - } - } - - part = GetAliasCommand(part, true); - - Plugin * plug = (*plug_mgr)[part]; - - if(!plug) - { - std::string lua = findScript(part + ".lua"); - if (lua.size()) - { - res = enableLuaScript(con, part, enable); - } - else - { - res = CR_NOT_FOUND; - con.printerr("No such plugin or Lua script: %s\n", part.c_str()); - } - } - else if (!plug->can_set_enabled()) - { - res = CR_NOT_IMPLEMENTED; - con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); - } - else - { - res = plug->set_enabled(con, enable); - - if (res != CR_OK || plug->is_enabled() != enable) - con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); - } - } - - return res; - } - else - { - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) - { - Plugin * plug = it->second; - if (!plug->can_be_enabled()) continue; - - con.print( - "%21s %-3s%s\n", - (plug->getName()+":").c_str(), - plug->is_enabled() ? "on" : "off", - plug->can_set_enabled() ? "" : " (controlled internally)" - ); - } - - Lua::CallLuaModuleFunction(con, "script-manager", "list"); - } + return Commands::enable(con, *this, first, parts); } else if (first == "ls" || first == "dir") { @@ -908,191 +755,27 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "plug") { - const char *header_format = "%30s %10s %4s %8s\n"; - const char *row_format = "%30s %10s %4i %8s\n"; - con.print(header_format, "Name", "State", "Cmds", "Enabled"); - - plug_mgr->refresh(); - for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it) - { - Plugin * plug = it->second; - if (!plug) - continue; - if (parts.size() && std::find(parts.begin(), parts.end(), plug->getName()) == parts.end()) - continue; - color_value color; - switch (plug->getState()) - { - case Plugin::PS_LOADED: - color = COLOR_RESET; - break; - case Plugin::PS_UNLOADED: - case Plugin::PS_UNLOADING: - color = COLOR_YELLOW; - break; - case Plugin::PS_LOADING: - color = COLOR_LIGHTBLUE; - break; - case Plugin::PS_BROKEN: - color = COLOR_LIGHTRED; - break; - default: - color = COLOR_LIGHTMAGENTA; - break; - } - con.color(color); - con.print(row_format, - plug->getName().c_str(), - Plugin::getStateDescription(plug->getState()), - plug->size(), - (plug->can_be_enabled() - ? (plug->is_enabled() ? "enabled" : "disabled") - : "n/a") - ); - con.color(COLOR_RESET); - } + return Commands::plug(con, *this, first, parts); } else if (first == "type") { - if (!parts.size()) - { - con.printerr("type: no argument\n"); - return CR_WRONG_USAGE; - } - con << parts[0]; - bool builtin = is_builtin(con, parts[0]); - std::string lua_path = findScript(parts[0] + ".lua"); - Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); - if (builtin) - { - con << " is a built-in command"; - con << std::endl; - } - else if (IsAlias(parts[0])) - { - con << " is an alias: " << GetAliasCommand(parts[0]) << std::endl; - } - else if (plug) - { - con << " is a command implemented by the plugin " << plug->getName() << std::endl; - } - else if (lua_path.size()) - { - con << " is a Lua script: " << lua_path << std::endl; - } - else - { - con << " is not a recognized command." << std::endl; - plug = plug_mgr->getPluginByName(parts[0]); - if (plug) - con << "Plugin " << parts[0] << " exists and implements " << plug->size() << " commands." << std::endl; - return CR_FAILURE; - } + return Commands::type(con, *this, first, parts); } else if (first == "keybinding") { - if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) - { - std::string keystr = parts[1]; - if (parts[0] == "set") - ClearKeyBindings(keystr); - for (int i = parts.size()-1; i >= 2; i--) - { - if (!AddKeyBinding(keystr, parts[i])) { - con.printerr("Invalid key spec: %s\n", keystr.c_str()); - break; - } - } - } - else if (parts.size() >= 2 && parts[0] == "clear") - { - for (size_t i = 1; i < parts.size(); i++) - { - if (!ClearKeyBindings(parts[i])) { - con.printerr("Invalid key spec: %s\n", parts[i].c_str()); - break; - } - } - } - else if (parts.size() == 2 && parts[0] == "list") - { - std::vector list = ListKeyBindings(parts[1]); - if (list.empty()) - con << "No bindings." << std::endl; - for (size_t i = 0; i < list.size(); i++) - con << " " << list[i] << std::endl; - } - else - { - con << "Usage:" << std::endl - << " keybinding list " << std::endl - << " keybinding clear [@context]..." << std::endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl - << "Later adds, and earlier items within one command have priority." << std::endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl - << "Context may be used to limit the scope of the binding, by" << std::endl - << "requiring the current context to have a certain prefix." << std::endl - << "Current UI context is: " << std::endl - << join_strings("\n", Gui::getCurFocus(true)) << std::endl; - } + return Commands::keybinding(con, *this, first, parts); } else if (first == "alias") { - if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) - { - const std::string &name = parts[1]; - std::vector cmd(parts.begin() + 2, parts.end()); - if (!AddAlias(name, cmd, parts[0] == "replace")) - { - con.printerr("Could not add alias %s - already exists\n", name.c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 2 && (parts[0] == "delete" || parts[0] == "clear")) - { - if (!RemoveAlias(parts[1])) - { - con.printerr("Could not remove alias %s\n", parts[1].c_str()); - return CR_FAILURE; - } - } - else if (parts.size() >= 1 && (parts[0] == "list")) - { - auto aliases = ListAliases(); - for (auto p : aliases) - { - con << p.first << ": " << join_strings(" ", p.second) << std::endl; - } - } - else - { - con << "Usage: " << std::endl - << " alias add|replace " << std::endl - << " alias delete|clear " << std::endl - << " alias list" << std::endl; - } + return Commands::alias(con, *this, first, parts); } else if (first == "fpause") { - World::SetPauseState(true); -/* TODO: understand how this changes for v50 - if (auto scr = Gui::getViewscreenByType()) - { - scr->worldgen_paused = true; - } -*/ - con.print("The game was forced to pause!\n"); + return Commands::fpause(con, *this, first, parts); } else if (first == "cls" || first == "clear") { - if (con.is_console()) - ((Console&)con).clear(); - else - { - con.printerr("No console to clear.\n"); - return CR_NEEDS_CONSOLE; - } + return Commands::clear(con, *this, first, parts); } else if (first == "die") { @@ -1104,173 +787,27 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "kill-lua") { - bool force = false; - for (auto it = parts.begin(); it != parts.end(); ++it) - { - if (*it == "force") - force = true; - } - if (!Lua::Interrupt(force)) - { - con.printerr( - "Failed to register hook. This can happen if you have" - " lua profiling or coverage monitoring enabled. Use" - " 'kill-lua force' to force, but this may disable" - " profiling and coverage monitoring.\n"); - } + return Commands::kill_lua(con, *this, first, parts); } else if (first == "script") { - if(parts.size() == 1) - { - loadScriptFile(con, parts[0], false); - } - else - { - con << "Usage:" << std::endl - << " script " << std::endl; - return CR_WRONG_USAGE; - } + return Commands::script(con, *this, first, parts); } else if (first == "hide") { - if (!getConsole().hide()) - { - con.printerr("Could not hide console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::hide(con, *this, first, parts); } else if (first == "show") { - if (!getConsole().show()) - { - con.printerr("Could not show console\n"); - return CR_FAILURE; - } - return CR_OK; + return Commands::show(con, *this, first, parts); } else if (first == "sc-script") { - if (parts.empty() || parts[0] == "help" || parts[0] == "?") - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - con << "Valid event names (SC_ prefix is optional):" << std::endl; - for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) - { - std::string name = sc_event_name((state_change_event)i); - if (name != "SC_UNKNOWN") - con << " " << name << std::endl; - } - return CR_OK; - } - else if (parts[0] == "list") - { - if(parts.size() < 2) - parts.push_back(""); - if (parts[1].size() && sc_event_id(parts[1]) == SC_UNKNOWN) - { - con << "Unrecognized event name: " << parts[1] << std::endl; - return CR_WRONG_USAGE; - } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) - { - if (!parts[1].size() || (it->event == sc_event_id(parts[1]))) - { - con.print("%s (%s): %s%s\n", sc_event_name(it->event).c_str(), - it->save_specific ? "save-specific" : "global", - it->save_specific ? "/raw/" : "/", - it->path.c_str()); - } - } - return CR_OK; - } - else if (parts[0] == "add") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript script(evt, parts[2], save_specific); - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) - { - if (script == *it) - { - con << "Script already registered" << std::endl; - return CR_FAILURE; - } - } - state_change_scripts.push_back(script); - return CR_OK; - } - else if (parts[0] == "remove") - { - if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) - { - con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; - return CR_WRONG_USAGE; - } - state_change_event evt = sc_event_id(parts[1]); - if (evt == SC_UNKNOWN) - { - con << "Unrecognized event: " << parts[1] << std::endl; - return CR_FAILURE; - } - bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); - StateChangeScript tmp(evt, parts[2], save_specific); - auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), tmp); - if (it != state_change_scripts.end()) - { - state_change_scripts.erase(it); - return CR_OK; - } - else - { - con << "Unrecognized script" << std::endl; - return CR_FAILURE; - } - } - else - { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::sc_script(con, *this, first, parts); } else if (first == "devel/dump-rpc") { - if (parts.size() == 1) - { - std::ofstream file(parts[0]); - CoreService core; - core.dumpMethods(file); - - for (auto & it : *plug_mgr) - { - Plugin * plug = it.second; - if (!plug) - continue; - - std::unique_ptr svc(plug->rpc_connect(con)); - if (!svc) - continue; - - file << "// Plugin: " << plug->getName() << std::endl; - svc->dumpMethods(file); - } - } - else - { - con << "Usage: devel/dump-rpc \"filename\"" << std::endl; - return CR_WRONG_USAGE; - } + return Commands::dump_rpc(con, *this, first, parts); } else if (RunAlias(con, first, parts, res)) { @@ -1281,41 +818,38 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s res = plug_mgr->InvokeCommand(con, first, parts); if (res == CR_WRONG_USAGE) { - help_helper(con, first); + DFHack::help_helper(con, first); } else if (res == CR_NOT_IMPLEMENTED) { std::string completed; - std::string filename = findScript(first + ".lua"); - bool lua = filename != ""; - if ( !lua ) { - filename = findScript(first + ".rb"); - } + std::filesystem::path filename = findScript(first + ".lua"); + bool lua = !filename.empty(); if ( lua ) res = runLuaScript(con, first, parts); else if (!no_autocomplete && try_autocomplete(con, first, completed)) res = CR_NOT_IMPLEMENTED; else - con.printerr("%s is not a recognized command.\n", first.c_str()); + con.printerr("{} is not a recognized command.\n", first); if (res == CR_NOT_IMPLEMENTED) { Plugin *p = plug_mgr->getPluginByName(first); if (p) { - con.printerr("%s is a plugin ", first.c_str()); + con.printerr("{} is a plugin ", first); if (p->getState() == Plugin::PS_UNLOADED) - con.printerr("that is not loaded - try \"load %s\" or check stderr.log\n", - first.c_str()); + con.printerr("that is not loaded - try \"load {}\" or check stderr.log\n", + first); else if (p->size()) - con.printerr("that implements %zi commands - see \"help %s\" for details\n", - p->size(), first.c_str()); + con.printerr("that implements {} commands - see \"help {}\" for details\n", + p->size(), first); else con.printerr("but does not implement any commands\n"); } } } else if (res == CR_NEEDS_CONSOLE) - con.printerr("%s needs an interactive console to work.\n" + con.printerr("{} needs an interactive console to work.\n" "Please run this command from the DFHack console.\n\n" #ifdef WIN32 "You can show the console with the 'show' command." @@ -1323,24 +857,39 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s "The console is accessible when you run DF from the commandline\n" "via the './dfhack' script." #endif - "\n", first.c_str()); + "\n", first); return res; } - return CR_OK; -} - -bool Core::loadScriptFile(color_ostream &out, std::string fname, bool silent) -{ - if(!silent) { - INFO(script,out) << "Running script: " << fname << std::endl; - std::cerr << "Running script: " << fname << std::endl; - } - std::ifstream script(fname.c_str()); - if ( !script.good() ) + return CR_OK; +} + +bool Core::loadScriptFile(color_ostream &out, std::filesystem::path fname, bool silent) +{ + if(!silent) { + INFO(script,out) << "Running script: " << fname << std::endl; + std::cerr << "Running script: " << fname << std::endl; + } + + auto pathlist = {getHackPath(), getHackPath().parent_path(), std::filesystem::current_path()}; + + std::filesystem::path path; + + for (auto& p : pathlist) + { + auto candidate = fname.is_relative() ? p / fname : fname; + if (std::filesystem::exists(candidate)) + { + path = candidate; + break; + } + } + + std::ifstream script{ path }; + if ( !script ) { if(!silent) - out.printerr("Error loading script: %s\n", fname.c_str()); + out.printerr("Error loading script: {}\n", fname); return false; } std::string command; @@ -1379,12 +928,11 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // load baseline defaults - core->loadScriptFile(out, CONFIG_PATH + "init/default.dfhack.init", false); + core->loadScriptFile(out, core->getConfigPath() / "init" / "default.dfhack.init", false); // load user overrides std::vector prefixes(1, "dfhack"); - loadScriptFiles(core, out, prefixes, CONFIG_PATH + "init"); - + loadScriptFiles(core, out, prefixes, core->getConfigPath() / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); Lua::CallLuaModuleFunction(out, L, "dfhack", "getHideConsoleOnStartup", 0, 1, @@ -1395,9 +943,8 @@ static void run_dfhack_init(color_ostream &out, Core *core) } // Load dfhack.init in a dedicated thread (non-interactive console mode) -void fInitthread(void * iodata) +static void fInitthread(IODATA * iod) { - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; color_ostream_proxy out(core->getConsole()); @@ -1405,19 +952,18 @@ void fInitthread(void * iodata) } // A thread function... for the interactive console. -void fIOthread(void * iodata) +static void fIOthread(IODATA * iod) { - static const std::string HISTORY_FILE = CONFIG_PATH + "dfhack.history"; - - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + std::filesystem::path HISTORY_FILE = core->getConfigPath() / "dfhack.history"; + + PluginManager * plug_mgr = iod->plug_mgr; CommandHistory main_history; main_history.load(HISTORY_FILE.c_str()); Console & con = core->getConsole(); - if (plug_mgr == 0) + if (plug_mgr == nullptr) { con.printerr("Something horrible happened in Core's constructor...\n"); return; @@ -1426,9 +972,9 @@ void fIOthread(void * iodata) run_dfhack_init(con, core); con.print("DFHack is ready. Have a nice day!\n" - "DFHack version %s\n" + "DFHack version {}\n" "Type in '?' or 'help' for general help, 'ls' to see all commands.\n", - dfhack_version_desc().c_str()); + dfhack_version_desc()); int clueless_counter = 0; @@ -1437,7 +983,7 @@ void fIOthread(void * iodata) while (true) { - std::string command = ""; + std::string command; int ret; while ((ret = con.lineedit("[DFHack]# ",command, main_history)) == Console::RETRY); @@ -1455,7 +1001,7 @@ void fIOthread(void * iodata) { // a proper, non-empty command was entered main_history.add(command); - main_history.save(HISTORY_FILE.c_str()); + main_history.save(HISTORY_FILE); } auto rv = core->runCommand(con, command); @@ -1479,29 +1025,25 @@ Core::~Core() Core::Core() : d(std::make_unique()), script_path_mutex{}, - HotkeyMutex{}, - HotkeyCond{}, + armok_mutex{}, alias_mutex{}, started{false}, - misc_data_mutex{}, CoreSuspendMutex{}, CoreWakeup{}, ownerThread{}, toolCount{0} { // init the console. This must be always the first step! - plug_mgr = 0; + plug_mgr = nullptr; errorstate = false; vinfo = 0; - memset(&(s_mods), 0, sizeof(s_mods)); // set up hotkey capture suppress_duplicate_keyboard_events = true; - hotkey_set = NO; - last_world_data_ptr = NULL; - last_local_map_ptr = NULL; + last_world_data_ptr = nullptr; + last_local_map_ptr = nullptr; last_pause_state = false; - top_viewscreen = NULL; + top_viewscreen = nullptr; color_ostream::log_errors_to_stderr = true; }; @@ -1516,16 +1058,16 @@ void Core::fatal (std::string output, const char * title) out << "DFHack will now deactivate.\n"; if(con.isInited()) { - con.printerr("%s", out.str().c_str()); + con.printerr("{}", out.str()); con.reset_color(); con.print("\n"); } - fprintf(stderr, "%s\n", out.str().c_str()); + fmt::print(stderr, "{}\n", out.str()); out << "Check file stderr.log for details.\n"; std::cout << "DFHack fatal error: " << out.str() << std::endl; if (!title) title = "DFHack error!"; - DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), NULL); + DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, title, out.str().c_str(), nullptr); bool is_headless = bool(getenv("DFHACK_HEADLESS")); if (is_headless) @@ -1534,25 +1076,31 @@ void Core::fatal (std::string output, const char * title) } } -std::string Core::getHackPath() +std::filesystem::path Core::getHackPath() { -#ifdef LINUX_BUILD - return p->getPath() + "/hack/"; -#else - return p->getPath() + "\\hack\\"; -#endif + return hack_path; } df::viewscreen * Core::getTopViewscreen() { return getInstance().top_viewscreen; } -bool Core::InitMainThread() { +bool Core::InitMainThread(std::filesystem::path path) { + assert(active_instance == nullptr); + + // set this instance as the active instance + active_instance = this; + // this hook is always called from DF's main (render) thread, so capture this thread id df_render_thread = std::this_thread::get_id(); + hack_path = path; Filesystem::init(); + #if defined(LINUX_BUILD) || defined(WIN32) + dfhack_crashlog_init(); + #endif + // Re-route stdout and stderr again - DF seems to set up stdout and // stderr.txt on Windows as of 0.43.05. Also, log before switching files to // make it obvious what's going on if someone checks the *.txt files. @@ -1566,12 +1114,16 @@ bool Core::InitMainThread() { fprintf(stderr, "dfhack: redirecting stderr to stderr.log\n"); if (!freopen("stderr.log", "w", stderr)) std::cerr << "Could not redirect stderr to stderr.log" << std::endl; + // freopen makes stderr fully buffered on Windows; restore unbuffered + // writes so that stderr.log is up to date (e.g. in the event of a crash) + setvbuf(stderr, NULL, _IONBF, 0); std::cerr << "DFHack build: " << Version::git_description() << std::endl; if (strlen(Version::dfhack_run_url())) { std::cerr << "Build url: " << Version::dfhack_run_url() << std::endl; } std::cerr << "Starting with working directory: " << Filesystem::getcwd() << std::endl; + std::cerr << "Hack path: " << getHackPath() << std::endl; std::cerr << "Binding to SDL.\n"; if (!DFSDL::init(con)) { @@ -1580,16 +1132,12 @@ bool Core::InitMainThread() { } // find out what we are... - #ifdef LINUX_BUILD - const char * path = "hack/symbols.xml"; - #else - const char * path = "hack\\symbols.xml"; - #endif + std::filesystem::path symbols_path = getHackPath() / "symbols.xml"; auto local_vif = std::make_unique(); std::cerr << "Identifying DF version.\n"; try { - local_vif->loadFile(path); + local_vif->loadFile(symbols_path); } catch(Error::All & err) { @@ -1609,7 +1157,7 @@ bool Core::InitMainThread() { { if (!Version::git_xml_match()) { - const char *msg = ( + constexpr auto msg = ( "*******************************************************\n" "* BIG, UGLY ERROR MESSAGE *\n" "*******************************************************\n" @@ -1680,6 +1228,7 @@ bool Core::InitMainThread() { { "world", sizeof(df::world) }, { "game", sizeof(df::gamest) }, { "plotinfo", sizeof(df::plotinfost) }, + { "gps", sizeof(df::graphic) }, }; for (auto& gte : *df::global::global_table) @@ -1716,9 +1265,9 @@ bool Core::InitSimulationThread() { // the update hook is only called from the simulation thread, so capture this thread id df_simulation_thread = std::this_thread::get_id(); - if(started) + if (started) return true; - if(errorstate) + if (errorstate) return false; // Lock the CoreSuspendMutex until the thread exits or call Core::Shutdown @@ -1760,60 +1309,64 @@ bool Core::InitSimulationThread() std::cout << "Console disabled.\n"; } } - else if(con.init(false)) + else if (con.init(false)) std::cerr << "Console is running.\n"; else std::cerr << "Console has failed to initialize!\n"; -/* - // dump offsets to a file - std::ofstream dump("offsets.log"); - if(!dump.fail()) - { - //dump << vinfo->PrintOffsets(); - dump.close(); - } - */ - // initialize data defs + /* + // dump offsets to a file + std::ofstream dump("offsets.log"); + if(!dump.fail()) + { + //dump << vinfo->PrintOffsets(); + dump.close(); + } + */ + // initialize data defs virtual_identity::Init(this); // create config directory if it doesn't already exist - if (!Filesystem::mkdir_recursive(CONFIG_PATH)) - con.printerr("Failed to create config directory: '%s'\n", CONFIG_PATH.c_str()); + if (!Filesystem::mkdir_recursive(getConfigPath())) + con.printerr("Failed to create config directory: '{}'\n", getConfigPath()); // copy over default config files if necessary - std::map config_files; - std::map default_config_files; - if (Filesystem::listdir_recursive(CONFIG_PATH, config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_PATH.c_str()); - else if (Filesystem::listdir_recursive(CONFIG_DEFAULTS_PATH, default_config_files, 10, false) != 0) - con.printerr("Failed to list directory: '%s'\n", CONFIG_DEFAULTS_PATH.c_str()); + std::map config_files; + std::map default_config_files; + if (Filesystem::listdir_recursive(getConfigPath(), config_files, 10, false) != 0) + con.printerr("Failed to list directory: '{}'\n", getConfigPath()); + else if (Filesystem::listdir_recursive(getConfigDefaultsPath(), default_config_files, 10, false) != 0) + con.printerr("Failed to list directory: '{}'\n", getConfigDefaultsPath()); else { // ensure all config file directories exist before we start copying files - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over files if (!entry.second) continue; - std::string dirname = CONFIG_PATH + entry.first; + std::filesystem::path dirname = getConfigPath() / entry.first; if (!Filesystem::mkdir_recursive(dirname)) - con.printerr("Failed to create config directory: '%s'\n", dirname.c_str()); + con.printerr("Failed to create config directory: '{}'\n", dirname); } // copy files from the default tree that don't already exist in the config tree - for (auto &entry : default_config_files) { + for (auto& entry : default_config_files) + { // skip over directories if (entry.second) continue; - std::string filename = entry.first; - if (!config_files.count(filename)) { - std::string src_file = CONFIG_DEFAULTS_PATH + filename; + std::filesystem::path filename = entry.first; + if (!config_files.contains(filename)) + { + std::filesystem::path src_file = getConfigDefaultsPath() / filename; if (!Filesystem::isfile(src_file)) continue; - std::string dest_file = CONFIG_PATH + filename; + std::filesystem::path dest_file = getConfigPath() / filename; std::ifstream src(src_file, std::ios::binary); std::ofstream dest(dest_file, std::ios::binary); - if (!src.good() || !dest.good()) { - con.printerr("Copy failed: '%s'\n", filename.c_str()); + if (!src.good() || !dest.good()) + { + con.printerr("Copy failed: '{}'\n", filename); continue; } dest << src.rdbuf(); @@ -1823,6 +1376,17 @@ bool Core::InitSimulationThread() } } + // set lua default path if not already set + if (std::getenv("DFHACK_LUA_PATH") == nullptr) + { + std::filesystem::path lua_path = getHackPath() / "lua" / "?.lua"; +#ifdef WIN32 + _putenv_s("DFHACK_LUA_PATH", lua_path.string().c_str()); +#else + setenv("DFHACK_LUA_PATH", lua_path.string().c_str(), 1); +#endif + } + loadScriptPaths(con); // initialize common lua context @@ -1848,7 +1412,9 @@ bool Core::InitSimulationThread() plug_mgr->init(); std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); - IODATA *temp = new IODATA; + this->hotkey_mgr = new HotkeyManager(); + + auto *temp = new IODATA; temp->core = this; temp->plug_mgr = plug_mgr; @@ -1856,17 +1422,15 @@ bool Core::InitSimulationThread() { std::cerr << "Starting IO thread.\n"; // create IO thread - d->iothread = std::thread{fIOthread, (void*)temp}; + d->iothread = std::thread{fIOthread, temp}; } else { std::cerr << "Starting dfhack.init thread.\n"; - d->iothread = std::thread{fInitthread, (void*)temp}; + d->iothread = std::thread{fInitthread, temp}; } std::cerr << "Starting DF input capture thread.\n"; - // set up hotkey capture - d->hotkeythread = std::thread(fHKthread, (void *) temp); started = true; modstate = 0; @@ -1883,13 +1447,13 @@ bool Core::InitSimulationThread() if (raw[offset] == '"') { offset++; - size_t next = raw.find("\"", offset); + size_t next = raw.find('"', offset); args.push_back(raw.substr(offset, next - offset)); offset = next + 2; } else { - size_t next = raw.find(" ", offset); + size_t next = raw.find(' ', offset); if (next == std::string::npos) { args.push_back(raw.substr(offset)); @@ -1908,9 +1472,9 @@ bool Core::InitSimulationThread() if (first.length() > 0 && first[0] == '+') { std::vector cmd; - for (it++; it != args.end(); it++) { - const std::string & arg = *it; - if (arg.length() > 0 && arg[0] == '+') + for (const auto& arg : args) + { + if (!arg.empty() && arg[0] == '+') { break; } @@ -1920,9 +1484,9 @@ bool Core::InitSimulationThread() if (runCommand(con, first.substr(1), cmd) != CR_OK) { std::cerr << "Error running command: " << first.substr(1); - for (auto it2 = cmd.begin(); it2 != cmd.end(); it2++) + for (const auto& arg : cmd) { - std::cerr << " \"" << *it2 << "\""; + std::cerr << " \"" << arg << "\""; } std::cerr << "\n"; } @@ -1940,77 +1504,10 @@ bool Core::InitSimulationThread() return true; } -/// sets the current hotkey command -bool Core::setHotkeyCmd( std::string cmd ) -{ - // access command - std::lock_guard lock(HotkeyMutex); - hotkey_set = SET; - hotkey_cmd = cmd; - HotkeyCond.notify_all(); - return true; -} -/// removes the hotkey command and gives it to the caller thread -std::string Core::getHotkeyCmd( bool &keep_going ) -{ - std::string returner; - std::unique_lock lock(HotkeyMutex); - HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); - if (hotkey_set == SHUTDOWN) { - keep_going = false; - return returner; - } - hotkey_set = NO; - returner = hotkey_cmd; - hotkey_cmd.clear(); - return returner; -} - -void Core::print(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprint(format,args); - va_end(args); -} - -void Core::printerr(const char *format, ...) -{ - color_ostream_proxy proxy(getInstance().con); - - va_list args; - va_start(args,format); - proxy.vprinterr(format,args); - va_end(args); -} - -void Core::RegisterData( void *p, std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - misc_data_map[key] = p; -} - -void *Core::GetData( std::string key ) -{ - std::lock_guard lock(misc_data_mutex); - std::map::iterator it=misc_data_map.find(key); - - if ( it != misc_data_map.end() ) - { - void *p=it->second; - return p; - } - else - { - return 0;// or throw an error. - } -} Core& Core::getInstance() { - static Core instance; - return instance; + assert(Core::active_instance != nullptr); + return *Core::active_instance; } bool Core::isSuspended(void) @@ -2020,10 +1517,22 @@ bool Core::isSuspended(void) void Core::doUpdate(color_ostream &out) { + #ifdef WIN32 + // Re-chain the top-level exception filter once DF is fully running, + // in case DF installed its own crash filter after our init. If ours + // is still installed, this is a no-op. + static bool crashlog_rearmed = false; + if (!crashlog_rearmed) + { + crashlog_rearmed = true; + dfhack_crashlog_init(); + } + #endif + Lua::Core::Reset(out, "DF code execution"); // find the current viewscreen - df::viewscreen *screen = NULL; + df::viewscreen *screen = nullptr; if (df::global::gview) { screen = &df::global::gview->view; @@ -2055,8 +1564,8 @@ void Core::doUpdate(color_ostream &out) } // detect if the game was loaded or unloaded in the meantime - void *new_wdata = NULL; - void *new_mapdata = NULL; + df::world_data* new_wdata = nullptr; + df::map_block**** new_mapdata = nullptr; if (df::global::world && !is_load_save) { df::world_data *wdata = df::global::world->world_data; @@ -2185,117 +1694,113 @@ void Core::onUpdate(color_ostream &out) perf_counters.incCounter(perf_counters.update_lua_ms, step_start_ms); } -void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { - std::vector files; +static void getFilesWithPrefixAndSuffix(const std::filesystem::path& folder, const std::string& prefix, const std::string& suffix, std::vector& result) { + std::vector files; DFHack::Filesystem::listdir(folder, files); - for ( size_t a = 0; a < files.size(); a++ ) { - if ( prefix.length() > files[a].length() ) - continue; - if ( suffix.length() > files[a].length() ) - continue; - if ( files[a].compare(0, prefix.length(), prefix) != 0 ) - continue; - if ( files[a].compare(files[a].length()-suffix.length(), suffix.length(), suffix) != 0 ) - continue; - result.push_back(files[a]); + for ( const auto& f : files) { + if (f.stem().string().starts_with(prefix) && f.extension() == suffix) + result.push_back(f); } - return; } -size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder) { +size_t loadScriptFiles(Core* core, color_ostream& out, const std::span prefix, const std::filesystem::path& folder) { static const std::string suffix = ".init"; - std::vector scriptFiles; - for ( size_t a = 0; a < prefix.size(); a++ ) { - getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); - } - std::sort(scriptFiles.begin(), scriptFiles.end(), - [&](const std::string &a, const std::string &b) { - std::string a_base = a.substr(0, a.size() - suffix.size()); - std::string b_base = b.substr(0, b.size() - suffix.size()); - return a_base < b_base; - }); + std::vector scriptFiles; + for ( const auto& p : prefix ) { + getFilesWithPrefixAndSuffix(folder, p, ".init", scriptFiles); + } + + std::ranges::sort(scriptFiles, + [](const std::filesystem::path& a, const std::filesystem::path& b) { + return a.stem() < b.stem(); + }); + size_t result = 0; - for ( size_t a = 0; a < scriptFiles.size(); a++ ) { + for ( const auto& file : scriptFiles) { result++; - std::string path = ""; - if (folder != ".") - path = folder + "/"; - core->loadScriptFile(out, path + scriptFiles[a], false); + core->loadScriptFile(out, folder / file, false); } return result; } namespace DFHack { namespace X { - typedef state_change_event Key; - typedef std::vector Val; - typedef std::pair Entry; - typedef std::vector EntryVector; - typedef std::map InitVariationTable; - - EntryVector computeInitVariationTable(void* none, ...) { - va_list list; - va_start(list,none); + using Key = state_change_event; + using Val = std::vector; + using Entry = std::pair; + using EntryVector = std::vector; + using InitVariationTable = std::map; + + template + concept VariationTableTypes = std::same_as || std::convertible_to; + + template + static EntryVector computeInitVariationTable(Ts&&... ts) { + using Arg = std::variant; + std::vector args{ Arg{std::forward(ts)}... }; + + Key current_key = SC_UNKNOWN; EntryVector result; - while(true) { - Key key = (Key)va_arg(list,int); - if ( key == SC_UNKNOWN ) - break; - Val val; - while (true) { - const char *v = va_arg(list, const char *); - if (!v || !v[0]) - break; - val.emplace_back(v); + Val val; + for (auto& arg : args) { + if (std::holds_alternative(arg)) { + if (current_key != SC_UNKNOWN && !val.empty()) { + result.emplace_back(current_key, val); + val.clear(); + } + current_key = std::get(arg); + } else if (std::holds_alternative(arg)) { + auto str = std::get(arg); + if (!str.empty()) + val.emplace_back(str); } - result.push_back(Entry(key,val)); } - va_end(list); + return result; } - InitVariationTable getTable(const EntryVector& vec) { + static InitVariationTable getTable(const EntryVector& vec) { return InitVariationTable(vec.begin(),vec.end()); } } } void Core::handleLoadAndUnloadScripts(color_ostream& out, state_change_event event) { - static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable(0, - (int)SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", "", - (int)SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", "", - (int)SC_MAP_LOADED, "onMapLoad", "onLoadMap", "", - (int)SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", "", - (int)SC_UNKNOWN + static const X::InitVariationTable table = X::getTable(X::computeInitVariationTable( + SC_WORLD_LOADED, "onLoad", "onLoadWorld", "onWorldLoaded", + SC_WORLD_UNLOADED, "onUnload", "onUnloadWorld", "onWorldUnloaded", + SC_MAP_LOADED, "onMapLoad", "onLoadMap", + SC_MAP_UNLOADED, "onMapUnload", "onUnloadMap", + SC_UNKNOWN )); if (!df::global::world) return; - std::string rawFolder = !isWorldLoaded() ? "" : "save/" + World::ReadWorldFolder() + "/init"; + std::filesystem::path rawFolder = !isWorldLoaded() ? Filesystem::getInstallDir() : Filesystem::getBaseDir() / "save" / World::ReadWorldFolder() / "init"; auto i = table.find(event); if ( i != table.end() ) { const std::vector& set = i->second; // load baseline defaults - this->loadScriptFile(out, CONFIG_PATH + "init/default." + set[0] + ".init", false); + this->loadScriptFile(out, getConfigPath() / "init" / ("default." + set[0] + ".init"), false); - loadScriptFiles(this, out, set, CONFIG_PATH + "init"); + loadScriptFiles(this, out, set, getConfigPath() / "init"); loadScriptFiles(this, out, set, rawFolder); } - for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) + for (const auto& script : state_change_scripts) { - if (it->event == event) + if (script.event == event) { - if (!it->save_specific) + if (!script.save_specific) { - loadScriptFile(out, it->path, false); + loadScriptFile(out, script.path, false); } - else if (it->save_specific && isWorldLoaded()) + else if (script.save_specific && isWorldLoaded()) { - loadScriptFile(out, rawFolder + it->path, false); + loadScriptFile(out, rawFolder / script.path, false); } } } @@ -2305,7 +1810,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) { using df::global::gametype; static md5wrapper md5w; - static std::string ostype = ""; + static std::string ostype; if (!ostype.size()) { @@ -2356,17 +1861,17 @@ void Core::onStateChange(color_ostream &out, state_change_event event) if (evtlog.fail()) { if (DFHack::Filesystem::isdir(save_dir)) - out.printerr("Could not append to %s\n", evtlogpath.c_str()); + out.printerr("Could not append to {}\n", evtlogpath); } else { char timebuf[30]; - time_t rawtime = time(NULL); + time_t rawtime = time(nullptr); struct tm * timeinfo = localtime(&rawtime); strftime(timebuf, sizeof(timebuf), "[%Y-%m-%dT%H:%M:%S%z] ", timeinfo); evtlog << timebuf; evtlog << "DFHack " << Version::git_description() << " on " << ostype << "; "; - evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath()).substr(0, 10) << "; "; + evtlog << "cwd md5: " << md5w.getHashFromString(getHackPath().string().c_str()).substr(0, 10) << "; "; evtlog << "save: " << world->cur_savegame.save_dir << "; "; evtlog << sc_event_name(event) << "; "; if (gametype) @@ -2421,6 +1926,10 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { + #if defined(LINUX_BUILD) || defined(WIN32) + dfhack_crashlog_shutdown(); + #endif + if(errorstate) return true; errorstate = 1; @@ -2433,29 +1942,31 @@ int Core::Shutdown ( void ) con.shutdown(); } - if (d->hotkeythread.joinable()) { - std::unique_lock hot_lock(HotkeyMutex); - hotkey_set = SHUTDOWN; - HotkeyCond.notify_one(); - } - ServerMain::block(); - d->hotkeythread.join(); d->iothread.join(); + if (hotkey_mgr) { + delete hotkey_mgr; + hotkey_mgr = nullptr; + } + if(plug_mgr) { delete plug_mgr; - plug_mgr = 0; + plug_mgr = nullptr; } + // invalidate all modules - allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); - DFSteam::cleanup(getConsole()); - memset(&(s_mods), 0, sizeof(s_mods)); + DFSteam::cleanup(); + d.reset(); + + // clear active instance + Core::active_instance = nullptr; + return -1; } @@ -2507,27 +2018,35 @@ bool Core::DFH_ncurses_key(int key) return ncurses_wgetch(key, dummy); } -bool Core::getSuppressDuplicateKeyboardEvents() { +bool Core::getSuppressDuplicateKeyboardEvents() const { return suppress_duplicate_keyboard_events; } void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { - DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to %s\n", - suppress ? "true" : "false"); + DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to {}\n", + suppress); suppress_duplicate_keyboard_events = suppress; } void Core::setMortalMode(bool value) { - std::lock_guard lock(HotkeyMutex); - mortal_mode = value; + mortal_mode.exchange(value); +} + +bool Core::getMortalMode() { + return mortal_mode.load(); } void Core::setArmokTools(const std::vector &tool_names) { - std::lock_guard lock(HotkeyMutex); + std::lock_guard lock(armok_mutex); armok_tools.clear(); armok_tools.insert(tool_names.begin(), tool_names.end()); } +bool Core::isArmokTool(const std::string& tool) { + std::lock_guard lock(armok_mutex); + return armok_tools.contains(tool); +} + // returns true if the event is handled bool Core::DFH_SDL_Event(SDL_Event* ev) { uint32_t start_ms = p->getTickCount(); @@ -2536,6 +2055,10 @@ bool Core::DFH_SDL_Event(SDL_Event* ev) { return ret; } +void Core::DFH_SDL_Loop() { + DFHack::runRenderThreadCallbacks(); +} + bool Core::doSdlInputEvent(SDL_Event* ev) { // this should only ever be called from the render thread @@ -2564,44 +2087,47 @@ bool Core::doSdlInputEvent(SDL_Event* ev) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; else if (sym == SDLK_LALT || sym == SDLK_RALT) modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; + else if (sym == SDLK_LGUI || sym == SDLK_RGUI) // Renamed to LMETA/RMETA in SDL3 + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SUPER : modstate & ~DFH_MOD_SUPER; else if (ke.state == SDL_PRESSED && !hotkey_states[sym]) { // the check against hotkey_states[sym] ensures we only process keybindings once per keypress - DEBUG(keybinding).print("key down: sym=%d (%c)\n", sym, sym); - if (SelectHotkey(sym, modstate)) { + DEBUG(keybinding).print("key down: sym={}, char={}\n", sym, static_cast(sym)); + if ((hotkey_mgr->handleKeybind(sym, modstate))) { hotkey_states[sym] = true; if (modstate & (DFH_MOD_CTRL | DFH_MOD_ALT)) { DEBUG(keybinding).print("modifier key detected; not inhibiting SDL key down event\n"); return false; } - DEBUG(keybinding).print("%sinhibiting SDL key down event\n", + DEBUG(keybinding).print("{}inhibiting SDL key down event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } } else if (ke.state == SDL_RELEASED) { - DEBUG(keybinding).print("key up: sym=%d (%c)\n", sym, sym); + DEBUG(keybinding).print("key up: sym={}, char={}\n", sym, static_cast(sym)); hotkey_states[sym] = false; } } else if (ev->type == SDL_MOUSEBUTTONDOWN) { auto &but = ev->button; - DEBUG(keybinding).print("mouse button down: button=%d\n", but.button); + DEBUG(keybinding).print("mouse button down: button={}\n", but.button); // don't mess with the first three buttons, which are critical elements of DF's control scheme if (but.button > 3) { - SDL_Keycode sym = SDLK_F13 + but.button - 4; - if (sym <= SDLK_F24 && SelectHotkey(sym, modstate)) + // We represent mouse buttons as a negative number, permitting buttons 4-15 + SDL_Keycode sym = -but.button; + if (sym >= -15 && sym <= -4 && hotkey_mgr->handleKeybind(sym, modstate)) return suppress_duplicate_keyboard_events; } } else if (ev->type == SDL_TEXTINPUT) { auto &te = ev->text; - DEBUG(keybinding).print("text input: '%s' (modifiers: %s%s%s)\n", + DEBUG(keybinding).print("text input: '{}' (modifiers: {}{}{})\n", te.text, modstate & DFH_MOD_SHIFT ? "Shift" : "", modstate & DFH_MOD_CTRL ? "Ctrl" : "", modstate & DFH_MOD_ALT ? "Alt" : ""); if (strlen(te.text) == 1 && hotkey_states[te.text[0]]) { - DEBUG(keybinding).print("%sinhibiting SDL text event\n", + DEBUG(keybinding).print("{}inhibiting SDL text event\n", suppress_duplicate_keyboard_events ? "" : "not "); return suppress_duplicate_keyboard_events; } @@ -2610,236 +2136,6 @@ bool Core::doSdlInputEvent(SDL_Event* ev) return false; } -bool Core::SelectHotkey(int sym, int modifiers) -{ - // Find the topmost viewscreen - if (!df::global::gview || !df::global::plotinfo) - return false; - - df::viewscreen *screen = &df::global::gview->view; - while (screen->child) - screen = screen->child; - - if (sym == SDLK_KP_ENTER) - sym = SDLK_RETURN; - - std::string cmd; - - DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); - - { - std::lock_guard lock(HotkeyMutex); - - // Check the internal keybindings - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - auto &binding = bindings[i]; - DEBUG(keybinding).print("examining hotkey with commandline: '%s'\n", binding.cmdline.c_str()); - - if (binding.modifiers != modifiers) { - DEBUG(keybinding).print("skipping keybinding due to modifiers mismatch: 0x%x != 0x%x\n", - binding.modifiers, modifiers); - continue; - } - if (!binding.focus.empty()) { - if (!Gui::matchFocusString(binding.focus)) { - std::vector focusStrings = Gui::getCurFocus(true); - DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' != '%s'\n", - join_strings(", ", focusStrings).c_str(), binding.focus.c_str()); - continue; - } - } - if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) { - DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - if (mortal_mode && armok_tools.contains(binding.command[0])) { - DEBUG(keybinding).print("skipping keybinding due to mortal mode (command: '%s')\n", - binding.command[0].c_str()); - continue; - } - - cmd = binding.cmdline; - DEBUG(keybinding).print("matched hotkey\n"); - break; - } - - if (cmd.empty()) { - // Check the hotkey keybindings - int idx = sym - SDLK_F1; - if(idx >= 0 && idx < 8) - { -/* TODO: understand how this changes for v50 - if (modifiers & 1) - idx += 8; - - if (strict_virtual_cast(screen) && - df::global::plotinfo->main.mode != ui_sidebar_mode::Hotkeys && - df::global::plotinfo->main.hotkeys[idx].cmd == df::ui_hotkey::T_cmd::None) - { - cmd = df::global::plotinfo->main.hotkeys[idx].name; - } -*/ - } - } - } - - if (!cmd.empty()) { - setHotkeyCmd(cmd); - return true; - } - else - return false; -} - -static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus) -{ - *pmod = 0; - - if (pfocus) - { - *pfocus = ""; - - size_t idx = keyspec.find('@'); - if (idx != std::string::npos) - { - *pfocus = keyspec.substr(idx+1); - keyspec = keyspec.substr(0, idx); - } - } - - // ugh, ugly - for (;;) { - if (keyspec.size() > 6 && keyspec.substr(0, 6) == "Shift-") { - *pmod |= 1; - keyspec = keyspec.substr(6); - } else if (keyspec.size() > 5 && keyspec.substr(0, 5) == "Ctrl-") { - *pmod |= 2; - keyspec = keyspec.substr(5); - } else if (keyspec.size() > 4 && keyspec.substr(0, 4) == "Alt-") { - *pmod |= 4; - keyspec = keyspec.substr(4); - } else - break; - } - - if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDLK_a + (keyspec[0]-'A'); - return true; - } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDLK_BACKQUOTE; - return true; - } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDLK_0 + (keyspec[0]-'0'); - return true; - } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDLK_F1 + (keyspec[1]-'1'); - return true; - } else if (keyspec.size() == 3 && keyspec.substr(0, 2) == "F1" && keyspec[2] >= '0' && keyspec[2] <= '2') { - *psym = SDLK_F10 + (keyspec[2]-'0'); - return true; - } else if (keyspec.size() == 6 && keyspec.substr(0, 5) == "MOUSE" && keyspec[5] >= '4' && keyspec[5] <= '9') { - *psym = SDLK_F13 + (keyspec[5]-'4'); - return true; - } else if (keyspec.size() == 7 && keyspec.substr(0, 6) == "MOUSE1" && keyspec[5] >= '0' && keyspec[5] <= '5') { - *psym = SDLK_F19 + (keyspec[5]-'0'); - return true; - } else if (keyspec == "Enter") { - *psym = SDLK_RETURN; - return true; - } else - return false; -} - -bool Core::ClearKeyBindings(std::string keyspec) -{ - int sym, mod; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return false; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == mod && prefix_matches(focus, bindings[i].focus)) - bindings.erase(bindings.begin()+i); - } - - return true; -} - -bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) -{ - size_t at_pos = keyspec.find('@'); - if (at_pos != std::string::npos) - { - std::string raw_spec = keyspec.substr(0, at_pos); - std::string raw_focus = keyspec.substr(at_pos + 1); - if (raw_focus.find('|') != std::string::npos) - { - std::vector focus_strings; - split_string(&focus_strings, raw_focus, "|"); - for (size_t i = 0; i < focus_strings.size(); i++) - { - if (!AddKeyBinding(raw_spec + "@" + focus_strings[i], cmdline)) - return false; - } - return true; - } - } - int sym; - KeyBinding binding; - if (!parseKeySpec(keyspec, &sym, &binding.modifiers, &binding.focus)) - return false; - - cheap_tokenise(cmdline, binding.command); - if (binding.command.empty()) - return false; - - std::lock_guard lock(HotkeyMutex); - - // Don't add duplicates - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (bindings[i].modifiers == binding.modifiers && - bindings[i].cmdline == cmdline && - bindings[i].focus == binding.focus) - return true; - } - - binding.cmdline = cmdline; - bindings.push_back(binding); - return true; -} - -std::vector Core::ListKeyBindings(std::string keyspec) -{ - int sym, mod; - std::vector rv; - std::string focus; - if (!parseKeySpec(keyspec, &sym, &mod, &focus)) - return rv; - - std::lock_guard lock(HotkeyMutex); - - std::vector &bindings = key_bindings[sym]; - for (int i = bindings.size()-1; i >= 0; --i) { - if (focus.size() && focus != bindings[i].focus) - continue; - if (bindings[i].modifiers == mod) - { - std::string cmd = bindings[i].cmdline; - if (!bindings[i].focus.empty()) - cmd = "@" + bindings[i].focus + ": " + cmd; - rv.push_back(cmd); - } - } - - return rv; -} - bool Core::AddAlias(const std::string &name, const std::vector &command, bool replace) { std::lock_guard lock(alias_mutex); @@ -2865,7 +2161,7 @@ bool Core::RemoveAlias(const std::string &name) bool Core::IsAlias(const std::string &name) { std::lock_guard lock(alias_mutex); - return aliases.find(name) != aliases.end(); + return aliases.contains(name); } bool Core::RunAlias(color_ostream &out, const std::string &name, @@ -2899,143 +2195,3 @@ std::string Core::GetAliasCommand(const std::string &name, bool ignore_params) return aliases[name][0]; return join_strings(" ", aliases[name]); } - -///////////////// -// ClassNameCheck -///////////////// - -// Since there is no Process.cpp, put ClassNameCheck stuff in Core.cpp - -static std::set known_class_names; -static std::map known_vptrs; - -ClassNameCheck::ClassNameCheck(std::string _name) : name(_name), vptr(0) -{ - known_class_names.insert(name); -} - -ClassNameCheck &ClassNameCheck::operator= (const ClassNameCheck &b) -{ - name = b.name; vptr = b.vptr; return *this; -} - -bool ClassNameCheck::operator() (Process *p, void * ptr) const { - if (vptr == 0 && p->readClassName(ptr) == name) - { - vptr = ptr; - known_vptrs[name] = ptr; - } - return (vptr && vptr == ptr); -} - -void ClassNameCheck::getKnownClassNames(std::vector &names) -{ - std::set::iterator it = known_class_names.begin(); - - for (; it != known_class_names.end(); it++) - names.push_back(*it); -} - -MemoryPatcher::MemoryPatcher(Process *p_) : p(p_) -{ - if (!p) - p = Core::getInstance().p.get(); -} - -MemoryPatcher::~MemoryPatcher() -{ - close(); -} - -bool MemoryPatcher::verifyAccess(void *target, size_t count, bool write) -{ - uint8_t *sptr = (uint8_t*)target; - uint8_t *eptr = sptr + count; - - // Find the valid memory ranges - if (ranges.empty()) - p->getMemRanges(ranges); - - // Find the ranges that this area spans - unsigned start = 0; - while (start < ranges.size() && ranges[start].end <= sptr) - start++; - if (start >= ranges.size() || ranges[start].start > sptr) - return false; - - unsigned end = start+1; - while (end < ranges.size() && ranges[end].start < eptr) - { - if (ranges[end].start != ranges[end-1].end) - return false; - end++; - } - if (ranges[end-1].end < eptr) - return false; - - // Verify current permissions - for (unsigned i = start; i < end; i++) - if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) - return false; - - // Apply writable permissions & update - for (unsigned i = start; i < end; i++) - { - auto &perms = ranges[i]; - if ((perms.write || !write) && perms.read) - continue; - - save.push_back(perms); - perms.write = perms.read = true; - if (!p->setPermisions(perms, perms)) - return false; - } - - return true; -} - -bool MemoryPatcher::write(void *target, const void *src, size_t size) -{ - if (!makeWritable(target, size)) - return false; - - memmove(target, src, size); - return true; -} - -void MemoryPatcher::close() -{ - for (size_t i = 0; i < save.size(); i++) - p->setPermisions(save[i], save[i]); - - save.clear(); - ranges.clear(); -}; - - -bool Process::patchMemory(void *target, const void* src, size_t count) -{ - MemoryPatcher patcher(this); - - return patcher.write(target, src, count); -} - -/******************************************************************************* - M O D U L E S -*******************************************************************************/ - -#define MODULE_GETTER(TYPE) \ -TYPE * Core::get##TYPE() \ -{ \ - if(errorstate) return NULL;\ - if(!s_mods.p##TYPE)\ - {\ - std::unique_ptr mod = create##TYPE();\ - s_mods.p##TYPE = (TYPE *) mod.get();\ - allModules.push_back(std::move(mod));\ - }\ - return s_mods.p##TYPE;\ -} - -MODULE_GETTER(Materials); -MODULE_GETTER(Graphic); diff --git a/library/Crashlog-windows.cpp b/library/Crashlog-windows.cpp new file mode 100644 index 0000000000..16b32fed56 --- /dev/null +++ b/library/Crashlog-windows.cpp @@ -0,0 +1,337 @@ +/* + * DFHack crash reporting for Windows. + * + * Dwarf Fortress installs its own top-level unhandled exception filter, + * which produces crashlog.txt (a native stack trace with module/offset + * annotations). That trace is enough to identify that a crash happened + * inside the Lua VM, but not what Lua code was running. + * + * This module chains an exception filter ahead of DF's (and additionally + * registers a vectored continue handler in case our filter is itself + * replaced later). When a fatal exception occurs and the Lua VM was + * involved - lua53.dll on the native stack, or a DFHack Lua call active + * on the faulting thread - we write a Lua stack traceback to stderr.log + * before chaining to DF's filter so that its crashlog is still produced. + * + * Everything in the crash path runs in the context of a crashed process, + * so it uses bare Win32 IO only and is wrapped in SEH so that a secondary + * fault while reporting simply falls through to DF's handler. + */ + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include +#include + +#include "Core.h" +#include "Crashlog.h" +#include "DFHackVersion.h" + +#include "lua.h" +#include "lauxlib.h" +#include "lstate.h" + +namespace { + +std::atomic handler_active{false}; +LPTOP_LEVEL_EXCEPTION_FILTER previous_filter = nullptr; +PVOID continue_handler = nullptr; + +// image bounds of interest, resolved once at init (before anyone could crash) +struct ImageRange { const char *name; uintptr_t base, end; }; +ImageRange df_image = {"Dwarf Fortress.exe", 0, 0}; +ImageRange dfhack_image = {"dfhack.dll", 0, 0}; +ImageRange lua_image = {"lua53.dll", 0, 0}; + +void cache_image_range(ImageRange &range, const wchar_t *module_name) +{ + HMODULE m = GetModuleHandleW(module_name); + if (!m) + return; + auto dos = reinterpret_cast(m); + auto nt = reinterpret_cast( + reinterpret_cast(m) + dos->e_lfanew); + range.base = reinterpret_cast(m); + range.end = range.base + nt->OptionalHeader.SizeOfImage; +} + +const char *image_name_for(uintptr_t addr) +{ + for (const auto &r : {df_image, dfhack_image, lua_image}) + if (r.base && addr >= r.base && addr < r.end) + return r.name; + return nullptr; +} + +void log_write_str(HANDLE log, const char *str) +{ + DWORD written; + WriteFile(log, str, (DWORD)strlen(str), &written, nullptr); +} + +// Minimal output: append to stderr.log with raw Win32 IO so we depend on +// as little process state as possible. +void log_write(HANDLE log, const char *fmt, ...) +{ + char buf[8192]; + va_list args; + va_start(args, fmt); + int len = _vsnprintf(buf, sizeof(buf) - 1, fmt, args); + va_end(args); + if (len == 0) + return; + if (len < 0 || len >= (int)sizeof(buf)) + len = (int)sizeof(buf) - 1; // truncated output is still worth having + buf[sizeof(buf) - 1] = '\0'; + log_write_str(log, buf); + +} + +// Walk the faulting thread's stack looking for instruction pointers inside +// lua53.dll. Uses RtlVirtualUnwind (driven by .pdata unwind info), which does +// not need dbghelp or symbol files. +bool lua_frames_on_stack(EXCEPTION_POINTERS *ep) +{ + CONTEXT ctx = *ep->ContextRecord; + for (int i = 0; i < 200; i++) + { + if (ctx.Rip >= lua_image.base && ctx.Rip < lua_image.end) + return true; + // Return addresses land on the instruction after a call and are + // normally unaligned, so the only sane early-out here is a null + // ip. Garbage values are harmless: RtlLookupFunctionEntry returns + // NULL for them, and the iteration bound plus the caller's SEH + // guard cap the damage from a corrupt stack. + if (!ctx.Rip) + break; + + DWORD64 image_base = 0; + PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(ctx.Rip, &image_base, nullptr); + if (fn) + { + PVOID handler_data = nullptr; + DWORD64 establisher = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, ctx.Rip, fn, + &ctx, &handler_data, &establisher, nullptr); + } + else + { + // Leaf function: return address is at [rsp]. Unlike rip, rsp + // is architecturally required to stay 8-aligned, so a + // misaligned value here means the context is corrupt. + if (ctx.Rsp & 7) + break; + ctx.Rip = *reinterpret_cast(ctx.Rsp); + ctx.Rsp += 8; + } + } + return false; +} + +bool state_has_active_call(lua_State *L) +{ + return L && L->ci != &L->base_ci; +} + +void write_lua_traceback(HANDLE log, lua_State *L) +{ + if (!L) + return; + + // Mirror of struct lua_extra_state in depends/lua/include/dfhack_llimits.h: + // the first extraspace slot holds a pointer to the state's recursive + // critical section. If the faulting thread holds it already (e.g. the + // crash is inside a Lua call on this thread), TryEnterCriticalSection + // recurses and succeeds. If another thread is inside the VM, we note it + // and walk the state anyway - it is read-only and SEH-guarded, and an + // approximate trace is better than none. + bool locked = false; + CRITICAL_SECTION *cs = nullptr; + __try + { + cs = *reinterpret_cast(lua_getextraspace(L)); + if (cs) + locked = TryEnterCriticalSection(cs); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + cs = nullptr; + } + + __try + { + if (!locked) + log_write(log, "(could not acquire Lua state lock; " + "traceback may be inaccurate)\n"); + if (!state_has_active_call(L)) + { + log_write(log, "main lua_State %p: no active call\n", (void*)L); + } + else + { + luaL_traceback(L, L, "crash while Lua was executing", 0); + const char *tb = lua_tostring(L, -1); + if (tb) + { + log_write(log, "lua_State %p traceback:\n", (void*)L); + log_write_str(log, tb); + log_write(log, "\n"); + } + else + { + log_write(log, "lua_State %p: traceback not a string\n", (void*)L); + } + lua_pop(L, 1); + } + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + log_write(log, "(lua traceback attempt faulted)\n"); + } + + if (locked) + LeaveCriticalSection(cs); +} + +void report_crash(EXCEPTION_POINTERS *ep) +{ + HANDLE log = CreateFileA("stderr.log", FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (log == INVALID_HANDLE_VALUE) + return; + + EXCEPTION_RECORD *er = ep->ExceptionRecord; + const char *fault_image = image_name_for( + reinterpret_cast(er->ExceptionAddress)); + bool in_lua = lua_frames_on_stack(ep); + + lua_State *active = DFHack::Crashlog::active_state(); + lua_State *main_state = nullptr; + __try + { + if (!DFHack::Core::noInstance()) + main_state = DFHack::Core::getInstance().getLuaState(true); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + + log_write(log, + "\n===== DFHack crash report =====\n" + "DFHack %s, DF %s\n" + "Exception 0x%08lx at %p (module: %s), thread %lu\n", + DFHack::Version::dfhack_version(), DFHack::Version::df_version(), + er->ExceptionCode, er->ExceptionAddress, + fault_image ? fault_image : "?", + GetCurrentThreadId()); + + if (er->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + er->NumberParameters >= 2) + log_write(log, "access violation %s address %p\n", + er->ExceptionInformation[0] ? "writing" : "reading", + (void*)er->ExceptionInformation[1]); + + log_write(log, + "lua53.dll on native stack: %s\n" + "thread-local active lua_State: %p\n", + in_lua ? "yes" : "no", + (void*)active); + + if (in_lua || active || state_has_active_call(main_state)) + { + write_lua_traceback(log, active ? active : main_state); + if (active && main_state && active != main_state && + state_has_active_call(main_state)) + write_lua_traceback(log, main_state); + } + else + { + log_write(log, "no Lua involvement detected\n"); + } + + log_write(log, "===== end DFHack crash report =====\n\n"); + CloseHandle(log); +} + +LONG WINAPI dfhack_unhandled_exception(EXCEPTION_POINTERS *ep) +{ + if (!handler_active.exchange(true)) + { + __try + { + report_crash(ep); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + } + + if (previous_filter) + return previous_filter(ep); + return EXCEPTION_CONTINUE_SEARCH; +} + +// Runs only if the exception was never handled and DF's own filter (or +// whoever replaced us) declined to handle it either. Produces the same +// report; the handler_active flag prevents double-reporting. +LONG WINAPI dfhack_vectored_continue(EXCEPTION_POINTERS *ep) +{ + if (!handler_active.exchange(true)) + { + __try + { + report_crash(ep); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + } + return EXCEPTION_CONTINUE_SEARCH; +} + +void chain_top_level_filter() +{ + LPTOP_LEVEL_EXCEPTION_FILTER prev = + SetUnhandledExceptionFilter(dfhack_unhandled_exception); + if (prev && prev != dfhack_unhandled_exception) + previous_filter = prev; +} + +} + +namespace DFHack { + +void dfhack_crashlog_init() +{ + cache_image_range(df_image, nullptr); + cache_image_range(dfhack_image, L"dfhack.dll"); + cache_image_range(lua_image, L"lua53.dll"); + + // Install our top-level filter, chaining to whoever was there before + // (normally DF's own crash handler). This function is also called again + // on the first update tick so that a filter DF installs later in + // startup still ends up behind us in the chain. + chain_top_level_filter(); + + // Fallback path: runs after the top-level filter if the exception was + // never handled, catching the case where our filter was overwritten. + if (!continue_handler) + continue_handler = AddVectoredContinueHandler(0, dfhack_vectored_continue); +} + +void dfhack_crashlog_shutdown() +{ + // Probe the current top-level filter by installing ours and looking at + // what was there. If ours was current, the chain tail is what should be + // restored; otherwise put back whatever we just displaced. + LPTOP_LEVEL_EXCEPTION_FILTER cur = + SetUnhandledExceptionFilter(dfhack_unhandled_exception); + SetUnhandledExceptionFilter(cur == dfhack_unhandled_exception + ? previous_filter : cur); + if (continue_handler) + { + RemoveVectoredContinueHandler(continue_handler); + continue_handler = nullptr; + } +} + +} diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp new file mode 100644 index 0000000000..fdf20150d2 --- /dev/null +++ b/library/Crashlog.cpp @@ -0,0 +1,430 @@ +// Dl_info and the SA_SIGINFO handler form need the GNU/POSIX extensions. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "DFHackVersion.h" +#include "Crashlog.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Core.h" +#include "lua.h" +#include "lauxlib.h" +#include "lstate.h" + +const int BT_ENTRY_MAX = 25; +struct CrashInfo { + int backtrace_entries = 0; + void* backtrace[BT_ENTRY_MAX]; + int signal = 0; + int sig_code = 0; + void* fault_address = nullptr; + // The lua_State the faulting thread was executing in, if it was inside + // a DFHack Lua entry point when it crashed. + lua_State* active_lua_state = nullptr; + pid_t thread_id = 0; +}; + +CrashInfo crash_info; + +std::atomic crashed = false; +std::atomic crashlog_ready = false; +std::atomic shutdown = false; + +// Load base of liblua53, resolved at init so the crashlog thread can +// recognize Lua frames without depending on anything else being intact. +void* lua_image_base = nullptr; + +// Use eventfd for async-signal safe waiting +int crashlog_complete = -1; + +void flag_set(std::atomic_bool &atom) { + atom.store(true); + atom.notify_all(); +} +void flag_wait(std::atomic_bool &atom) { + atom.wait(false); +} + +void signal_crashlog_complete() { + if (crashlog_complete == -1) + return; + uint64_t v = 1; + [[maybe_unused]] auto _ = write(crashlog_complete, &v, sizeof(v)); +} + +std::thread crashlog_thread; + +// Force this method to be inlined so that it doesn't create a stack frame +[[gnu::always_inline]] inline void handle_signal_internal(int sig, siginfo_t *si) { + if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { + // Ensure the signal handler doesn't try to write a crashlog + // whilst the crashlog thread is unavailable. + std::quick_exit(1); + } + crash_info.signal = sig; + if (si) { + crash_info.sig_code = si->si_code; + crash_info.fault_address = si->si_addr; + } + crash_info.thread_id = (pid_t)syscall(SYS_gettid); + crash_info.active_lua_state = DFHack::Crashlog::active_state(); + crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); + + // Signal saving of crashlog + flag_set(crashlog_ready); + // Wait for completion via eventfd read, if fd isn't valid, bail + if (crashlog_complete != -1) { + [[maybe_unused]] uint64_t v; + [[maybe_unused]] auto _ = read(crashlog_complete, &v, sizeof(v)); + } + std::quick_exit(1); +} + +extern "C" void dfhack_crashlog_handle_signal(int sig, siginfo_t *si, void *) { + handle_signal_internal(sig, si); +} + +void dfhack_crashlog_handle_terminate() { + handle_signal_internal(0, nullptr); +} + +std::string signal_name(int sig) { + switch (sig) { + case SIGINT: + return "SIGINT"; + case SIGILL: + return "SIGILL"; + case SIGABRT: + return "SIGABRT"; + case SIGFPE: + return "SIGFPE"; + case SIGSEGV: + return "SIGSEGV"; + case SIGBUS: + return "SIGBUS"; + case SIGTERM: + return "SIGTERM"; + } + return ""; +} + +std::string signal_code_name(int sig, int code) { + // si_code values are only unique within their signal, so key on both. + switch (sig) { + case SIGSEGV: + switch (code) { + case SEGV_MAPERR: return "SEGV_MAPERR"; + case SEGV_ACCERR: return "SEGV_ACCERR"; + case SEGV_BNDERR: return "SEGV_BNDERR"; + } + break; + case SIGBUS: + switch (code) { + case BUS_ADRALN: return "BUS_ADRALN"; + case BUS_ADRERR: return "BUS_ADRERR"; + } + break; + case SIGILL: + switch (code) { + case ILL_ILLOPC: return "ILL_ILLOPC"; + case ILL_PRVOPC: return "ILL_PRVOPC"; + } + break; + case SIGFPE: + switch (code) { + case FPE_INTDIV: return "FPE_INTDIV"; + case FPE_FLTDIV: return "FPE_FLTDIV"; + } + break; + } + return ""; +} + +std::filesystem::path get_crashlog_path() { + std::time_t time = std::time(nullptr); + std::tm* tm = std::localtime(&time); + + std::string timestamp = "unknown"; + if (tm) { + char stamp[64]; + std::size_t out = strftime(&stamp[0], 63, "%Y-%m-%d-%H-%M-%S", tm); + if (out != 0) + timestamp = stamp; + } + + std::filesystem::path dir = "crashlog"; + std::error_code err; + std::filesystem::create_directories(dir, err); + + std::filesystem::path log_path = dir / ("crash_" + timestamp + ".txt"); + return log_path; +} + +// --------------------------------------------------------------------------- +// Lua crash diagnostics +// +// These run on the dedicated crashlog thread in normal (non-signal) context, +// after the native backtrace has already been written and flushed. The Lua +// state may itself be corrupt - that is often *why* we crashed - so the +// inspection is bounded by a fault guard: a segfault while tracing only +// loses the Lua section of the report. +// --------------------------------------------------------------------------- + +volatile sig_atomic_t lua_guard_armed = 0; +sigjmp_buf lua_guard_jmp; +pid_t crashlog_thread_id = 0; +const int guarded_signals[3] = {SIGSEGV, SIGBUS, SIGILL}; +struct sigaction saved_actions[3]; + +// Runs as a temporary signal disposition while the Lua state is inspected. +// Faults on the crashlog thread jump back to the guard; a fault on any other +// thread re-raises with the default disposition so the process still dies +// with its original signal. +extern "C" void lua_fault_guard(int sig, siginfo_t *, void *) { + if (lua_guard_armed && (pid_t)syscall(SYS_gettid) == crashlog_thread_id) + siglongjmp(lua_guard_jmp, sig); + signal(sig, SIG_DFL); + raise(sig); +} + +bool in_lua_image(void *addr) { + Dl_info di; + if (!dladdr(addr, &di)) + return false; + if (lua_image_base && di.dli_fbase == lua_image_base) + return true; + if (di.dli_fname) { + const char *base = std::strrchr(di.dli_fname, '/'); + base = base ? base + 1 : di.dli_fname; + if (std::strstr(base, "lua53") || std::strstr(base, "liblua")) + return true; + } + return false; +} + +bool state_has_active_call(lua_State *L) { + return L && L->ci != &L->base_ci; +} + +// Emits the Lua section of the crashlog. Must only be called between +// arming and disarming the fault guard. +void inspect_lua_state(std::ofstream &crashlog, lua_State *L) { + if (!L) + return; + + // Best-effort lock. The faulting thread is frozen inside its signal + // handler and usually still holds the state mutex, so trylock typically + // fails and we walk the state read-only - its only writer is suspended + // anyway. If we do acquire it, no other thread is inside the VM. + // Mirror of struct lua_extra_state in depends/lua/include/dfhack_llimits.h: + // the extraspace slot holds a pointer to the state's recursive mutex. + bool locked = false; + auto *mutex = *reinterpret_cast(lua_getextraspace(L)); + if (mutex) + locked = pthread_mutex_trylock(mutex) == 0; + if (!locked) + crashlog << "could not acquire Lua state lock; " + "the following is a best-effort snapshot\n"; + + if (!state_has_active_call(L)) { + crashlog << "lua_State " << (const void*)L << ": no active call\n"; + } else { + // A corrupted CallInfo chain (e.g. a cycle) would send + // luaL_traceback into an unbounded walk, which the fault guard + // cannot interrupt. Bound the chain first. + const CallInfo *ci = L->ci; + int depth = 0; + while (ci && ci != &L->base_ci && ++depth <= 65536) + ci = ci->previous; + if (ci != &L->base_ci) { + crashlog << "lua_State " << (const void*)L + << ": corrupted CallInfo chain; not safe to trace\n"; + } else { + crashlog << "lua_State " << (const void*)L << " traceback:\n"; + luaL_traceback(L, L, "crash while Lua was executing", 0); + const char *tb = lua_tostring(L, -1); + crashlog << (tb ? tb : "(traceback unavailable)") << "\n"; + lua_pop(L, 1); + } + } + + if (locked) + pthread_mutex_unlock(mutex); +} + +// Writes the Lua diagnostics section, guarded against faults in corrupted +// interpreter metadata. Fault guard handlers are installed for the duration +// of the inspection and the original dispositions are restored afterwards. +void write_lua_context(std::ofstream &crashlog) { + bool lua_on_stack = false; + for (int i = 0; i < crash_info.backtrace_entries; i++) { + if (in_lua_image(crash_info.backtrace[i])) { + lua_on_stack = true; + break; + } + } + + crashlog << "\nliblua53 on native stack: " << (lua_on_stack ? "yes" : "no") << "\n"; + crashlog << "thread-local active lua_State: " + << (const void*)crash_info.active_lua_state << "\n"; + + struct sigaction sa; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = lua_fault_guard; + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sigemptyset(&sa.sa_mask); + for (int i = 0; i < 3; i++) + sigaction(guarded_signals[i], &sa, &saved_actions[i]); + crashlog_thread_id = (pid_t)syscall(SYS_gettid); + + // Everything that dereferences the Lua state or Core singleton lives + // inside the guard: either may be pointing at corrupt memory. + int fault = sigsetjmp(lua_guard_jmp, 1); + if (fault == 0) { + lua_guard_armed = 1; + + lua_State *active = crash_info.active_lua_state; + lua_State *main_state = nullptr; + if (!DFHack::Core::noInstance()) + main_state = DFHack::Core::getInstance().getLuaState(true); + + if (!lua_on_stack && !active && !state_has_active_call(main_state)) { + crashlog << "no Lua involvement detected\n"; + } else { + inspect_lua_state(crashlog, active ? active : main_state); + if (active && main_state && active != main_state && + state_has_active_call(main_state)) + inspect_lua_state(crashlog, main_state); + } + lua_guard_armed = 0; + } else { + crashlog << "(inspecting the Lua state faulted: " + << signal_name(fault) << ")\n"; + } + + for (int i = 0; i < 3; i++) + sigaction(guarded_signals[i], &saved_actions[i], nullptr); +} + +void dfhack_save_crashlog() { + char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); + try { + std::filesystem::path crashlog_path = get_crashlog_path(); + std::ofstream crashlog(crashlog_path); + + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal; + std::string code = signal_code_name(crash_info.signal, crash_info.sig_code); + if (!code.empty()) + crashlog << " (" << code << ")"; + else if (crash_info.sig_code) + crashlog << " (code " << crash_info.sig_code << ")"; + // si_addr is only meaningful for faults (a null deref reads as + // (nil), which is itself diagnostic). + if (crash_info.signal == SIGSEGV || crash_info.signal == SIGBUS || + crash_info.signal == SIGILL || crash_info.signal == SIGFPE) + crashlog << " at address " << crash_info.fault_address; + crashlog << " on thread " << crash_info.thread_id << "\n"; + } + + if (crash_info.backtrace_entries >= 2 && backtrace_strings != nullptr) { + // Skip the first backtrace entry as it will always be dfhack_crashlog_handle_(signal|terminate) + for (int i = 1; i < crash_info.backtrace_entries; i++) { + crashlog << i - 1 << "> " << backtrace_strings[i] << "\n"; + } + } else { + // Make it clear if no relevant backtrace was able to be obtained + crashlog << "Failed to obtain relevant backtrace\n"; + } + + // Make sure the native report is on disk before touching the Lua + // state, which may itself be corrupt. + crashlog.flush(); + write_lua_context(crashlog); + } catch (...) {} + + free(backtrace_strings); +} + +void dfhack_crashlog_thread() { + // Wait for activation signal + flag_wait(crashlog_ready); + if (shutdown.load()) // Shutting down gracefully, end thread. + return; + + dfhack_save_crashlog(); + signal_crashlog_complete(); + std::quick_exit(1); +} + +std::terminate_handler term_handler = nullptr; + +const int desired_signals[4] = {SIGSEGV,SIGBUS,SIGILL,SIGABRT}; +namespace DFHack { + void dfhack_crashlog_init() { + // Initialize eventfd flag + crashlog_complete = eventfd(0, EFD_CLOEXEC); + + crashlog_thread = std::thread(dfhack_crashlog_thread); + + struct sigaction sa; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = dfhack_crashlog_handle_signal; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + for (int signal : desired_signals) { + sigaction(signal, &sa, nullptr); + } + term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); + + // Symbols used by the signal handler and the crashlog thread must be + // resolved ahead of time - lazy binding cannot run in the crash path. + // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s. + // Using it here ensures it is loaded before use in the signal handler. + [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); + [[maybe_unused]] lua_State *s = DFHack::Crashlog::active_state(); + [[maybe_unused]] pid_t t = (pid_t)syscall(SYS_gettid); + Dl_info di; + if (dladdr((void*)&lua_close, &di)) + lua_image_base = di.dli_fbase; + } + + void dfhack_crashlog_shutdown() { + shutdown.exchange(true); + for (int signal : desired_signals) { + std::signal(signal, SIG_DFL); + } + std::set_terminate(term_handler); + + // Shutdown the crashlog thread. + flag_set(crashlog_ready); + crashlog_thread.join(); + + // If the signal handler is somehow running whilst here, let it terminate + signal_crashlog_complete(); + if (crashlog_complete != -1) + close(crashlog_complete); // Close fd + return; + } +} diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index 5d2386282e..ce11f174ed 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -27,6 +27,7 @@ distribution. #include #include #include +#include #include "MemAccess.h" #include "Core.h" @@ -86,27 +87,43 @@ void *enum_identity::do_allocate() const { return p; } -/* The order of global object constructor calls is - * undefined between compilation units. Therefore, - * this list has to be plain data, so that it gets - * initialized by the loader in the initial mmap. - */ -compound_identity *compound_identity::list = NULL; -std::vector compound_identity::top_scope; +/****** COMPOUND IDENTITIES ******/ -compound_identity::compound_identity(size_t size, TAllocateFn alloc, - const compound_identity *scope_parent, const char *dfhack_name) - : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(const_cast(scope_parent)) // fixme +decltype(compound_identity::list) compound_identity::list = nullptr; +decltype(compound_identity::parent_map) compound_identity::parent_map = nullptr; +decltype(compound_identity::children_map) compound_identity::children_map = nullptr; +decltype(compound_identity::top_scope) compound_identity::top_scope = nullptr; + +void compound_identity::ensure_compound_identity_init() { - next = list; list = this; + static std::once_flag compound_identity_init_flag; + std::call_once(compound_identity_init_flag, [] () { + list = new (std::remove_pointer::type)(); + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + top_scope = new std::vector(); + }); } -void compound_identity::doInit(Core *) +compound_identity::compound_identity(size_t size, TAllocateFn alloc, + const compound_identity* scope_parent, const char* dfhack_name) + : constructed_identity(size, alloc), dfhack_name(dfhack_name), scope_parent(scope_parent) { + ensure_compound_identity_init(); + if (scope_parent) - scope_parent->scope_children.push_back(this); + { + (*parent_map)[this] = scope_parent; + (*children_map)[scope_parent].push_back(this); + } else - top_scope.push_back(this); + (*top_scope).push_back(this); + + list->push_back(this); +} + +void compound_identity::doInit(Core *) const +{ } const std::string compound_identity::getFullName() const @@ -117,6 +134,18 @@ const std::string compound_identity::getFullName() const return getName(); } +bool compound_identity::is_equivalent(const compound_identity* other) const +{ + if (this->byte_size() != other->byte_size() || strcmp(this->getName(), other->getName()) != 0) + return false; + + if (this->scope_parent != other->scope_parent && + !(this->scope_parent && other->scope_parent && this->scope_parent->is_equivalent(other->scope_parent))) + return false; + + return true; +} + static std::mutex *known_mutex = NULL; void compound_identity::Init(Core *core) @@ -124,10 +153,10 @@ void compound_identity::Init(Core *core) if (!known_mutex) known_mutex = new std::mutex(); - // This cannot be done in the constructors, because - // they are called in an undefined order. - for (compound_identity *p = list; p; p = p->next) - p->doInit(core); + // do any late initialization required for compound identities + + for (auto ci : *list) + ci->doInit(core); } bitfield_identity::bitfield_identity(size_t size, @@ -176,27 +205,44 @@ enum_identity::ComplexData::ComplexData(std::initializer_list values) } } +/****** STRUCT IDENTITIES ******/ + +decltype(struct_identity::parent_map) struct_identity::parent_map = nullptr; +decltype(struct_identity::children_map) struct_identity::children_map = nullptr; + +void struct_identity::ensure_struct_identity_init() +{ + static std::once_flag struct_identity_init_flag; + std::call_once(struct_identity_init_flag, []() { + parent_map = new (std::remove_pointer::type)(); + children_map = new (std::remove_pointer::type)(); + }); +} + + + struct_identity::struct_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name, const struct_identity *parent, const struct_field_info *fields) : compound_identity(size, alloc, scope_parent, dfhack_name), - parent(const_cast(parent)), has_children(false), fields(fields) + fields(fields) { + ensure_struct_identity_init(); + if (parent) + { + (*parent_map)[this] = parent; + (*children_map)[parent].push_back(this); + } } -void struct_identity::doInit(Core *core) +void struct_identity::doInit(Core *core) const { compound_identity::doInit(core); - - if (parent) { - parent->children.push_back(this); - parent->has_children = true; - } } bool struct_identity::is_subclass(const struct_identity *actual) const { - if (!has_children && actual != this) + if (!hasChildren() && actual != this) return false; for (; actual; actual = actual->getParent()) @@ -205,7 +251,28 @@ bool struct_identity::is_subclass(const struct_identity *actual) const return false; } -const std::string pointer_identity::getFullName() const +bool struct_identity::is_equivalent(const struct_identity* other) const +{ + if (!static_cast(this)->is_equivalent(static_cast(other))) + return false; + + if (this->parent != other->parent && + !(this->parent && other->parent && this->parent->is_equivalent(other->parent))) + return false; + + const struct_field_info* f0 = this->fields; + const struct_field_info* f1 = other->fields; + + for (; f0->mode != struct_field_info::Mode::END && f1->mode != struct_field_info::Mode::END; f0++, f1++) + { + if (f0->mode != f1->mode || f0->offset != f1->offset || f0->count != f1->count || strcmp(f0->name, f1->name) != 0) + return false; + } + + return true; +} + +const std::string pointer_identity_base::getFullName() const { return (target ? target->getFullName() : std::string("void")) + "*"; } @@ -228,23 +295,42 @@ const std::string bit_container_identity::getFullName(const type_identity *) con const std::string df::buffer_container_identity::getFullName(const type_identity *item) const { return (item ? item->getFullName() : std::string("void")) + - (size > 0 ? stl_sprintf("[%d]", size) : std::string("[]")); + (size > 0 ? fmt::format("[{}]", size) : std::string("[]")); } union_identity::union_identity(size_t size, const TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields) + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields) : struct_identity(size, alloc, scope_parent, dfhack_name, parent, fields) { } +/****** VIRTUAL IDENTITIES ******/ + +decltype(virtual_identity::name_lookup) virtual_identity::name_lookup = nullptr; +decltype(virtual_identity::known) virtual_identity::known = nullptr; +decltype(virtual_identity::vtable_ptr_map) virtual_identity::vtable_ptr_map = nullptr; +decltype(virtual_identity::interpose_list_map) virtual_identity::interpose_list_map = nullptr; + +void virtual_identity::ensure_virtual_identity_init() +{ + static std::once_flag virtual_identity_init_flag; + std::call_once(virtual_identity_init_flag, []() { + name_lookup = new (std::remove_pointer::type)(); + known = new (std::remove_pointer::type)(); + vtable_ptr_map = new (std::remove_pointer::type)(); + interpose_list_map = new (std::remove_pointer::type)(); + }); +} + virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, const virtual_identity *parent, const struct_field_info *fields, bool is_plugin) : struct_identity(size, alloc, NULL, dfhack_name, parent, fields), original_name(original_name), - vtable_ptr(NULL), is_plugin(is_plugin) + is_plugin(is_plugin) { + ensure_virtual_identity_init(); // Plugins are initialized after Init was called, so they need to be added to the name table here if (is_plugin) { @@ -252,15 +338,10 @@ virtual_identity::virtual_identity(size_t size, const TAllocateFn alloc, } } -/* Vtable name to identity lookup. */ -static std::map name_lookup; - -/* Vtable pointer to identity lookup. */ -std::map virtual_identity::known; - virtual_identity::~virtual_identity() { // Remove interpose entries, so that they don't try accessing this object later + auto& interpose_list = (*interpose_list_map)[this]; for (auto it = interpose_list.begin(); it != interpose_list.end(); ++it) if (it->second) it->second->on_host_delete(this); @@ -269,75 +350,79 @@ virtual_identity::~virtual_identity() // Remove global lookup table entries if we're from a plugin if (is_plugin) { - name_lookup.erase(getOriginalName()); + (*name_lookup).erase(getOriginalName()); - if (vtable_ptr) - known.erase(vtable_ptr); + if (vtable_ptr()) + (*known).erase(vtable_ptr()); } } -void virtual_identity::doInit(Core *core) +void virtual_identity::doInit(Core *core) const { struct_identity::doInit(core); auto vtname = getOriginalName(); - name_lookup[vtname] = this; + (*name_lookup)[vtname] = this; - vtable_ptr = core->vinfo->getVTable(vtname); + auto vtable_ptr = core->vinfo->getVTable(vtname); if (vtable_ptr) - known[vtable_ptr] = this; + { + (*known)[vtable_ptr] = this; + (*vtable_ptr_map)[this] = vtable_ptr; + } } -virtual_identity *virtual_identity::find(const std::string &name) +const virtual_identity *virtual_identity::find(std::string_view name) { - auto name_it = name_lookup.find(name); + auto name_it = (*name_lookup).find(name); - return (name_it != name_lookup.end()) ? name_it->second : NULL; + return (name_it != (*name_lookup).end()) ? name_it->second : nullptr; } -virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) +const virtual_identity *virtual_identity::get(virtual_ptr instance_ptr) { - if (!instance_ptr) return NULL; + if (!instance_ptr) return nullptr; return find(get_vtable(instance_ptr)); } -virtual_identity *virtual_identity::find(void *vtable) +const virtual_identity *virtual_identity::find(void *vtable) { if (!vtable || !known_mutex) - return NULL; + return nullptr; + + ensure_virtual_identity_init(); // Actually, a reader/writer lock would be sufficient, // since the table is only written once per class. std::lock_guard lock(*known_mutex); - std::map::iterator it = known.find(vtable); - - if (it != known.end()) + auto it = (*known).find(vtable); + if (it != (*known).end()) return it->second; // If using a reader/writer lock, re-grab as write here, and recheck Core &core = Core::getInstance(); - std::string name = core.p->doReadClassName(vtable); + std::string name = core.p->readClassName(vtable); - auto name_it = name_lookup.find(name); - if (name_it != name_lookup.end()) { - virtual_identity *p = name_it->second; + auto name_it = (*name_lookup).find(name); + if (name_it != (*name_lookup).end()) { + const virtual_identity *p = name_it->second; - if (p->vtable_ptr && p->vtable_ptr != vtable) { + if (p->vtable_ptr() && p->vtable_ptr() != vtable) { std::cerr << "Conflicting vtable ptr for class '" << p->getName() << "': found 0x" << std::hex << uintptr_t(vtable) - << ", previous 0x" << uintptr_t(p->vtable_ptr) << std::dec << std::endl; + << ", previous 0x" << uintptr_t(p->vtable_ptr()) << std::dec << std::endl; abort(); - } else if (!p->vtable_ptr) { + } else if (!p->vtable_ptr()) { uintptr_t pv = uintptr_t(vtable); pv -= Core::getInstance().vinfo->getRebaseDelta(); std::cerr << "" << std::endl; } - known[vtable] = p; - p->vtable_ptr = vtable; + (*known)[vtable] = p; + (*vtable_ptr_map)[p] = vtable; return p; } @@ -346,14 +431,15 @@ virtual_identity *virtual_identity::find(void *vtable) << std::hex << uintptr_t(vtable) << std::dec << std::endl; } - known[vtable] = NULL; - return NULL; + (*known).erase(vtable); + return nullptr; } -void virtual_identity::adjust_vtable(virtual_ptr obj, virtual_identity *main) +void virtual_identity::adjust_vtable(virtual_ptr obj, const virtual_identity *main) const { - if (vtable_ptr) { - *(void**)obj = vtable_ptr; + auto vp = vtable_ptr(); + if (vp) { + *(void**)obj = vp; return; } @@ -366,10 +452,10 @@ void virtual_identity::adjust_vtable(virtual_ptr obj, virtual_identity *main) virtual_ptr virtual_identity::clone(virtual_ptr obj) { - virtual_identity *id = get(obj); - if (!id) return NULL; + const virtual_identity *id = get(obj); + if (!id) return nullptr; virtual_ptr copy = id->instantiate(); - if (!copy) return NULL; + if (!copy) return nullptr; id->do_copy(copy, obj); return copy; } @@ -429,7 +515,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p, std::string name = format_key(items[i].name, i); if (items[i].size > 1) - name += stl_sprintf("=%u", value); + name += fmt::format("={}", value); pvec->push_back(name); } diff --git a/library/DataIdentity.cpp b/library/DataIdentity.cpp index e6b4889cf8..cc6ea64fa6 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -1,49 +1,55 @@ -#include +#include "DataIdentity.h" + +#include "BitArray.h" +#include "DataDefs.h" #include +#include +#include #include +#include +#include +#include #include +#include #include -#include #include +#include -#include "DataFuncs.h" -#include "DataIdentity.h" // the space after the uses of "type" in OPAQUE_IDENTITY_TRAITS_NAME is _required_ // without it the macro generates a syntax error when type is a template specification namespace df { -#define NUMBER_IDENTITY_TRAITS(category, type, name) \ - const category##_identity identity_traits::identity(name); -#define INTEGER_IDENTITY_TRAITS(type, name) NUMBER_IDENTITY_TRAITS(integer, type, name) -#define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type, #type) +#define NUMBER_IDENTITY_TRAITS(type, name) \ + const type_identity_for identity_traits::identity(name); #define OPAQUE_IDENTITY_TRAITS_NAME(name, ...) \ - const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_noassign_fn<__VA_ARGS__ >, name) + const type_identity_for<__VA_ARGS__ > identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_fn<__VA_ARGS__ >, name) #define OPAQUE_IDENTITY_TRAITS(...) OPAQUE_IDENTITY_TRAITS_NAME(#__VA_ARGS__, __VA_ARGS__ ) - INTEGER_IDENTITY_TRAITS(char, "char"); - INTEGER_IDENTITY_TRAITS(signed char, "int8_t"); - INTEGER_IDENTITY_TRAITS(unsigned char, "uint8_t"); - INTEGER_IDENTITY_TRAITS(short, "int16_t"); - INTEGER_IDENTITY_TRAITS(unsigned short, "uint16_t"); - INTEGER_IDENTITY_TRAITS(int, "int32_t"); - INTEGER_IDENTITY_TRAITS(unsigned int, "uint32_t"); - INTEGER_IDENTITY_TRAITS(long, "long"); - INTEGER_IDENTITY_TRAITS(unsigned long, "unsigned long"); - INTEGER_IDENTITY_TRAITS(long long, "int64_t"); - INTEGER_IDENTITY_TRAITS(unsigned long long, "uint64_t"); - FLOAT_IDENTITY_TRAITS(float); - FLOAT_IDENTITY_TRAITS(double); - - const bool_identity identity_traits::identity; - const stl_string_identity identity_traits::identity; - const ptr_string_identity identity_traits::identity; - const ptr_string_identity identity_traits::identity; - const pointer_identity identity_traits::identity; - const stl_ptr_vector_identity identity_traits >::identity; - const stl_bit_vector_identity identity_traits >::identity; - const bit_array_identity identity_traits >::identity; + NUMBER_IDENTITY_TRAITS(char, "char"); + NUMBER_IDENTITY_TRAITS(signed char, "int8_t"); + NUMBER_IDENTITY_TRAITS(unsigned char, "uint8_t"); + NUMBER_IDENTITY_TRAITS(short, "int16_t"); + NUMBER_IDENTITY_TRAITS(unsigned short, "uint16_t"); + NUMBER_IDENTITY_TRAITS(int, "int32_t"); + NUMBER_IDENTITY_TRAITS(unsigned int, "uint32_t"); + NUMBER_IDENTITY_TRAITS(long, "long"); + NUMBER_IDENTITY_TRAITS(unsigned long, "unsigned long"); + NUMBER_IDENTITY_TRAITS(long long, "int64_t"); + NUMBER_IDENTITY_TRAITS(unsigned long long, "uint64_t"); + NUMBER_IDENTITY_TRAITS(wchar_t, "wchar_t"); + NUMBER_IDENTITY_TRAITS(float, "float"); + NUMBER_IDENTITY_TRAITS(double, "double"); + NUMBER_IDENTITY_TRAITS(bool, "bool"); + const type_identity_for identity_traits::identity; + const type_identity_for identity_traits::identity; + const type_identity_for identity_traits::identity; + const type_identity_for identity_traits::identity; + const type_identity_for identity_traits::identity(nullptr); + const type_identity_for > identity_traits >::identity; + const type_identity_for > identity_traits >::identity; + const type_identity_for > identity_traits >::identity; OPAQUE_IDENTITY_TRAITS(std::condition_variable); OPAQUE_IDENTITY_TRAITS(std::fstream); @@ -56,9 +62,11 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); + OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; - const stl_container_identity > stl_vector_int32_t_identity("vector", identity_traits::get()); - const stl_container_identity > stl_vector_int16_t_identity("vector", identity_traits::get()); + const type_identity_for > stl_vector_int32_t_identity("vector", identity_traits::get()); + const type_identity_for > stl_vector_int16_t_identity("vector", identity_traits::get()); } diff --git a/library/DataStatics.cpp b/library/DataStatics.cpp index fe60e54544..af7613cdaa 100644 --- a/library/DataStatics.cpp +++ b/library/DataStatics.cpp @@ -22,8 +22,8 @@ namespace { DFHack::VersionInfo *global_table_ = DFHack::Core::getInstance().vinfo.get(); \ void * tmp_; -#define INIT_GLOBAL_FUNCTION_ITEM(type,name) \ - if (global_table_->getAddress(#name,tmp_)) name = (type*)tmp_; +#define INIT_GLOBAL_FUNCTION_ITEM(name, ...) \ + if (global_table_->getAddress(#name,tmp_)) name = (__VA_ARGS__*)tmp_; #define TID(type) (&identity_traits< type >::identity) diff --git a/library/Debug.cpp b/library/Debug.cpp index dafbeb5ce3..50bef6e538 100644 --- a/library/Debug.cpp +++ b/library/Debug.cpp @@ -66,9 +66,6 @@ void DebugManager::unregisterCategory(DebugCategory& cat) DebugRegisterBase::DebugRegisterBase(DebugCategory* cat) { - // Make sure Core lives at least as long any DebugCategory to - // allow debug prints until all Debugcategories has been destructed - Core::getInstance(); DebugManager::getInstance().registerCategory(*cat); } diff --git a/library/Error.cpp b/library/Error.cpp index 3cd0eb31d6..9abb9ea7c9 100644 --- a/library/Error.cpp +++ b/library/Error.cpp @@ -1,6 +1,9 @@ #include "Error.h" +#include "Format.h" #include "MiscUtils.h" +#include + using namespace DFHack::Error; inline std::string safe_str(const char *s) @@ -24,7 +27,7 @@ VTableMissing::VTableMissing(const char *name) {} SymbolsXmlParse::SymbolsXmlParse(const char* desc, int id, int row, int col) - :AllSymbols(stl_sprintf("error %d: %s, at row %d col %d", id, desc, row, col)), + :AllSymbols(fmt::format("error {}: {}, at row {} col {}", id, desc, row, col)), desc(safe_str(desc)), id(id), row(row), col(col) {} diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 951472eba6..761de48778 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -2,11 +2,47 @@ #include "Export.h" #include "df/gamest.h" +#include "df/global_objects.h" + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; +static std::unique_ptr core_instance; + +static std::filesystem::path getModulePath() +{ +#ifdef _WIN32 + HMODULE module = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)getModulePath, &module); + if (!module) return std::filesystem::path(); // should never happen, but just in case, return an empty path instead of crashing + + wchar_t path[MAX_PATH]; + GetModuleFileNameW(module, path, MAX_PATH); + return std::filesystem::path(path); +#else + Dl_info info; + dladdr((const void*)getModulePath, &info); + return std::filesystem::path(info.dli_fname); +#endif +} + +static std::filesystem::path basepath{getModulePath()}; + +// called by the chainloader before the main thread is initialized and before any other hooks are called. +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath) +{ + basepath = dllpath.parent_path(); +} + // called from the main thread before the simulation thread is started // and the main event loop is initiated DFhackCExport void dfhooks_init() { @@ -16,8 +52,11 @@ DFhackCExport void dfhooks_init() { return; } + // construct DFHack core instance + core_instance = std::make_unique(); + // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread() || !df::global::game) { + if (!core_instance->InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } @@ -26,6 +65,8 @@ DFhackCExport void dfhooks_init() { if (cmdline.find("--disable-dfhack") != std::string::npos) { fprintf(stderr, "dfhack: --disable-dfhack specified on commandline; disabling\n"); disabled = true; + core_instance->Shutdown(); + core_instance.reset(); return; } @@ -36,14 +77,16 @@ DFhackCExport void dfhooks_init() { DFhackCExport void dfhooks_shutdown() { if (disabled) return; - DFHack::Core::getInstance().Shutdown(); + core_instance->Shutdown(); + // release DFHack core instance + core_instance.reset(); } // called from the simulation thread in the main event loop DFhackCExport void dfhooks_update() { if (disabled) return; - DFHack::Core::getInstance().Update(); + core_instance->Update(); } // called from the simulation thread just before adding the macro @@ -59,7 +102,7 @@ DFhackCExport void dfhooks_prerender() { DFhackCExport bool dfhooks_sdl_event(SDL_Event* event) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_SDL_Event(event); + return core_instance->DFH_SDL_Event(event); } // called from the main thread just after setting mouse state in gps and just @@ -68,6 +111,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged + core_instance->DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input @@ -77,5 +121,5 @@ DFhackCExport void dfhooks_sdl_loop() { DFhackCExport bool dfhooks_ncurses_key(int key) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_ncurses_key(key); + return core_instance->DFH_ncurses_key(key); } diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4b82dddc17..d937333b11 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -36,6 +36,7 @@ distribution. #include "LuaTools.h" #include "LuaWrapper.h" #include "md5wrapper.h" +#include "MemoryPatcher.h" #include "MiscUtils.h" #include "PluginManager.h" @@ -47,6 +48,7 @@ distribution. #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Kitchen.h" @@ -69,9 +71,12 @@ distribution. #include "df/building_civzonest.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" +#include "df/building_workshopst.h" +#include "df/builtin_mats.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" +#include "df/construction_flags.h" #include "df/creature_raw.h" #include "df/dfhack_material_category.h" #include "df/enabler.h" @@ -89,6 +94,7 @@ distribution. #include "df/job_item.h" #include "df/job_material_category.h" #include "df/language_word_table.h" +#include "df/manager_order.h" #include "df/material.h" #include "df/map_block.h" #include "df/nemesis_record.h" @@ -99,6 +105,7 @@ distribution. #include "df/report_zoom_type.h" #include "df/specific_ref.h" #include "df/specific_ref_type.h" +#include "df/squad_use_flags.h" #include "df/squad.h" #include "df/unit.h" #include "df/unit_misc_trait.h" @@ -113,7 +120,10 @@ distribution. #include #include #include +#include #include +#include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -230,8 +240,14 @@ static void decode_pen(lua_State *L, Pen &pen, int idx) get_char_field(L, &pen.ch, idx, "ch", 0); + // COLOR_RESET only has meaning on text consoles; substitute the Pen + // defaults so out-of-range colors can't reach the screen buffers get_int_field(L, &pen.fg, idx, "fg", 7); + if (pen.fg < 0 || pen.fg > COLOR_MAX) + pen.fg = COLOR_GREY; get_int_field(L, &pen.bg, idx, "bg", 0); + if (pen.bg < 0 || pen.bg > COLOR_MAX) + pen.bg = COLOR_BLACK; lua_getfield(L, idx, "bold"); if (lua_isnil(L, -1)) @@ -245,7 +261,11 @@ static void decode_pen(lua_State *L, Pen &pen, int idx) get_int_or_closure_field(L, &pen.tile, idx, "tile", 0); bool tcolor = get_int_field(L, &pen.tile_fg, idx, "tile_fg", 7); + if (pen.tile_fg < 0 || pen.tile_fg > COLOR_MAX) + pen.tile_fg = COLOR_GREY; tcolor = get_int_field(L, &pen.tile_bg, idx, "tile_bg", 0) || tcolor; + if (pen.tile_bg < 0 || pen.tile_bg > COLOR_MAX) + pen.tile_bg = COLOR_BLACK; if (tcolor) pen.tile_mode = Pen::TileColor; @@ -484,7 +504,7 @@ static bool decode_matinfo(lua_State *state, MaterialInfo *info, bool numpair = if (auto item = Lua::GetDFObject(state, 1)) return info->decode(item); if (auto plant = Lua::GetDFObject(state, 1)) - return info->decode(MaterialInfo::PLANT_BASE, plant->material); + return info->decode(df::builtin_mats::PLANT_1, plant->material); if (auto mvec = Lua::GetDFObject(state, 1)) return info->decode(*mvec, luaL_checkint(state, 2)); } @@ -627,7 +647,7 @@ void Lua::Push(lua_State *L, const Screen::Pen &info) return; } - new (L) Pen(info); + Lua::make_lua_userdata(L, info); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN); lua_setmetatable(L, -2); @@ -1126,7 +1146,7 @@ static int dfhack_random_init(lua_State *L) static int dfhack_random_new(lua_State *L) { - new (L) MersenneRNG(); + Lua::make_lua_userdata(L); lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_RANDOM_TOKEN); lua_setmetatable(L, -2); @@ -1210,19 +1230,19 @@ static int dfhack_random_perlin(lua_State *L) switch (size) { case 1: { - auto pdata = new (L) PerlinNoise1D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_1, 1); break; } case 2: { - auto pdata = new (L) PerlinNoise2D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_2, 1); break; } case 3: { - auto pdata = new (L) PerlinNoise3D(); + auto pdata = Lua::make_lua_userdata>(L); pdata->init(*prng); lua_pushcclosure(L, eval_perlin_3, 1); break; @@ -1288,7 +1308,7 @@ static void addCommandToHistory(string id, string src_file, string command) { CommandHistory *history = ensureCommandHistory(id, src_file); history->add(command); - history->save(src_file.c_str()); + history->save(std::filesystem::path{ src_file }); } /************************ @@ -1312,11 +1332,21 @@ static void OpenModule(lua_State *state, const char *mname, const luaL_Reg *reg2 lua_pop(state, 1); } -#define WRAPM(module, function) { #function, df::wrap_function(module::function,true) } -#define WRAP(function) { #function, df::wrap_function(function,true) } -#define WRAPN(name, function) { #name, df::wrap_function(function,true) } +#define WRAPM(module, function) { #function, df::wrap_function(module::function, #function, true) } +#define WRAP(function) { #function, df::wrap_function(function, #function, true) } +#define WRAPN(name, function) { #name, df::wrap_function(function, #function, true) } #define CWRAP(name, function) { #name, &Lua::CallWithCatchWrapper } +// The _D variants are used to indicate that a wrapped function is deprecated. +// Deprecated functions will still function but will emit a warning to the DFHack console. +// This warning is displayed only once for each distinct appearance in a script (by script path and line number). +// The additional "message" argument is displayed with the warning; it is recommended that this message +// indicate the recommended replacement or substitution. The message must not be empty. + +#define WRAPM_D(module, function, message) { #function, df::wrap_function(module::function, #function, true, message) } +#define WRAP_D(function, message) { #function, df::wrap_function(function, #function, true, message) } +#define WRAPN_D(name, function, message) { #name, df::wrap_function(function, #function, true, message) } + /***** DFHack module *****/ static string getOSType() @@ -1350,8 +1380,9 @@ static string getArchitectureName() static string getDFVersion() { return Core::getInstance().vinfo->getVersion(); } static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); } -static string getDFPath() { return Core::getInstance().p->getPath(); } -static string getHackPath() { return Core::getInstance().getHackPath(); } +static std::filesystem::path getDFPath() { return Core::getInstance().p->getPath(); } +static std::filesystem::path getHackPath() { return Core::getInstance().getHackPath(); } +static std::filesystem::path getConfigPath() { return Core::getInstance().getConfigPath(); } static bool isWorldLoaded() { return Core::getInstance().isWorldLoaded(); } static bool isMapLoaded() { return Core::getInstance().isMapLoaded(); } @@ -1377,6 +1408,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = { WRAP(getDFPath), WRAP(getTickCount), WRAP(getHackPath), + WRAP(getConfigPath), WRAP(isWorldLoaded), WRAP(isMapLoaded), WRAP(isSiteLoaded), @@ -1862,6 +1894,123 @@ static const luaL_Reg dfhack_gui_funcs[] = { { NULL, NULL } }; +/***** Hotkey module *****/ +static bool hotkey_addKeybind(const std::string spec, const std::string cmd) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return false; + return hotkey_mgr->addKeybind(spec, cmd); +} + +static bool hotkey_isDisruptiveKeybind(const std::string spec) { + auto key = Hotkey::KeySpec::parse(spec); + if (!key.has_value()) + return true; + return key.value().isDisruptive(); +} + +static int hotkey_requestKeybindingInput(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) return 0; + bool cancel = false; + if (lua_gettop(L) == 1) + cancel = lua_toboolean(L, -1); + hotkey_mgr->requestKeybindingInput(cancel); + return 0; +} + +static int hotkey_getKeybindingInput(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto input = hotkey_mgr->getKeybindingInput(); + + if (input.empty()) { + lua_pushnil(L); + } else { + lua_pushlstring(L, input.data(), input.size()); + } + return 1; +} + +static int hotkey_removeKeybind(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + if (!hotkey_mgr) { + lua_pushboolean(L, false); + return 1; + } + + bool res = false; + switch (lua_gettop(L)) { + case 1: + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -1)); + break; + case 2: + luaL_checkstring(L, -2); + res = hotkey_mgr->removeKeybind(lua_tostring(L, -2), lua_toboolean(L, -1)); + break; + case 3: + luaL_checkstring(L, -3); + luaL_checkstring(L, -1); + res = hotkey_mgr->removeKeybind( + lua_tostring(L, -3), + lua_toboolean(L, -2), + lua_tostring(L, -1) + ); + break; + } + + lua_pushboolean(L, res); + return 1; +} + +void hotkey_pushBindArray(lua_State *L, const std::vector& binds) { + lua_createtable(L, binds.size(), 0); + int i = 1; + for (const auto& bind : binds) { + lua_createtable(L, 0, 2); + + lua_pushlstring(L, "spec", 4); + auto spec_str = bind.spec.toString(true); + lua_pushlstring(L, spec_str.data(), spec_str.size()); + lua_settable(L, -3); + + lua_pushlstring(L, "command", 7); + lua_pushlstring(L, bind.cmdline.data(), bind.cmdline.size()); + lua_settable(L, -3); + lua_rawseti(L, -2, i++); + } +} + +static int hotkey_listActiveKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listActiveKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static int hotkey_listAllKeybinds(lua_State *L) { + auto hotkey_mgr = Core::getInstance().getHotkeyManager(); + auto binds = hotkey_mgr->listAllKeybinds(); + + hotkey_pushBindArray(L, binds); + return 1; +} + +static const luaL_Reg dfhack_hotkey_funcs[] = { + { "removeKeybind", hotkey_removeKeybind }, + { "listActiveKeybinds", hotkey_listActiveKeybinds }, + { "listAllKeybinds", hotkey_listAllKeybinds }, + { "requestKeybindingInput", hotkey_requestKeybindingInput }, + { "getKeybindingInput", hotkey_getKeybindingInput }, + { NULL, NULL } +}; + +static const LuaWrapper::FunctionReg dfhack_hotkey_module[] = { + WRAPN(addKeybind, hotkey_addKeybind), + WRAPN(isDisruptiveKeybind, hotkey_isDisruptiveKeybind), + { NULL, NULL } +}; + /***** Job module *****/ static bool jobEqual(const df::job *job1, const df::job *job2) @@ -1895,11 +2044,14 @@ static const LuaWrapper::FunctionReg dfhack_job_module[] = { WRAPM(Job,isSuitableItem), WRAPM(Job,isSuitableMaterial), WRAPM(Job,getName), + WRAPM(Job,getManagerOrderName), WRAPM(Job,linkIntoWorld), WRAPM(Job,removePostings), WRAPM(Job,disconnectJobItem), WRAPM(Job,disconnectJobGeneralRef), WRAPM(Job,removeJob), + WRAPM(Job,createLinked), + WRAPM(Job,assignToWorkshop), WRAPN(is_equal, jobEqual), WRAPN(is_item_equal, jobItemEqual), { NULL, NULL } @@ -2004,6 +2156,8 @@ static const luaL_Reg dfhack_textures_funcs[] = { /***** Units module *****/ +static int getBreathingState(df::unit* u) { return (int)Units::getBreathingState(u); } + static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isActive), WRAPM(Units, isVisible), @@ -2056,6 +2210,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, hasExtravision), WRAPM(Units, isOpposedToLife), WRAPM(Units, isBloodsucker), + WRAPM(Units, breathes), WRAPM(Units, isDwarf), WRAPM(Units, isAnimal), WRAPM(Units, isMerchant), @@ -2126,6 +2281,9 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, setGroupActionTimers), WRAPM(Units, getUnitByNobleRole), WRAPM(Units, unassignTrainer), + WRAPM(Units, hasUnbailableSocialActivity), + WRAPM(Units, isJobAvailable), + WRAPN(getBreathingState, getBreathingState), { NULL, NULL } }; @@ -2289,10 +2447,11 @@ static int units_assignTrainer(lua_State *L) { } static int units_getReadablename(lua_State *L) { + bool skip_english = lua_toboolean(L, 2); // defaults to false if (auto unit = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(unit)); + Lua::Push(L, Units::getReadableName(unit, skip_english)); else if (auto hf = Lua::GetDFObject(L, 1)) - Lua::Push(L, Units::getReadableName(hf)); + Lua::Push(L, Units::getReadableName(hf, skip_english)); else luaL_argerror(L, 1, "Expected a unit or a historical figure"); return 1; @@ -2318,6 +2477,25 @@ static int units_getProfessionName(lua_State *L) { return 1; } +int32_t units_getFocusPenalty(lua_State *L) { + auto unit = Lua::GetDFObject(L, 1); + Units::need_type_set needs; + auto top = lua_gettop(L); + if (top < 2) { + luaL_argerror(L, 2, "Expected at least one need type"); + } else { + for (int i = 2; i <= top; ++i) { + try { + needs.set(luaL_checkint(L, i)); + } catch ([[maybe_unused]] const std::out_of_range &e) { + luaL_argerror(L, i, "Expected a need type"); + } + } + Lua::Push(L, Units::getFocusPenalty(unit, needs)); + } + return 1; +} + static const luaL_Reg dfhack_units_funcs[] = { { "getPosition", units_getPosition }, { "getOuterContainerRef", units_getOuterContainerRef }, @@ -2332,6 +2510,7 @@ static const luaL_Reg dfhack_units_funcs[] = { { "getReadableName", units_getReadablename }, { "getVisibleName", units_getVisibleName }, { "getProfessionName", units_getProfessionName }, + { "getFocusPenalty", units_getFocusPenalty }, { NULL, NULL } }; @@ -2341,6 +2520,8 @@ static const LuaWrapper::FunctionReg dfhack_military_module[] = { WRAPM(Military, makeSquad), WRAPM(Military, updateRoomAssignments), WRAPM(Military, getSquadName), + WRAPM(Military, removeFromSquad), + WRAPM(Military, addToSquad), { NULL, NULL } }; @@ -2442,7 +2623,7 @@ static int items_moveToBuilding(lua_State *state) static int items_moveToInventory(lua_State *state) { auto item = Lua::CheckDFObject(state, 1); auto unit = Lua::CheckDFObject(state, 2); - auto use_mode = (df::unit_inventory_item::T_mode)luaL_optint(state, 3, 0); + auto use_mode = (df::inv_item_role_type)luaL_optint(state, 3, 0); int body_part = luaL_optint(state, 4, -1); lua_pushboolean(state, Items::moveToInventory(item, unit, use_mode, body_part)); return 1; @@ -2456,8 +2637,11 @@ static int items_createItem(lua_State *state) auto mat_type = lua_tointeger(state, 4); auto mat_index = lua_tointeger(state, 5); bool no_floor = lua_toboolean(state, 6); + int count = lua_tointeger(state, 7); + if (count < 1) + count = 1; vector out_items; - Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor); + Items::createItem(out_items, unit, item_type, item_subtype, mat_type, mat_index, no_floor, count); Lua::PushVector(state, out_items); return 1; } @@ -2498,6 +2682,7 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = { WRAPM(Maps, getWalkableGroup), WRAPM(Maps, canWalkBetween), WRAPM(Maps, spawnFlow), + WRAPM(Maps, addBlockColumns), WRAPN(hasTileAssignment, hasTileAssignment), WRAPN(getTileAssignment, getTileAssignment), WRAPN(setTileAssignment, setTileAssignment), @@ -2533,6 +2718,24 @@ static int maps_ensureTileBlock(lua_State *L) return 1; } +static int maps_getTileBlockCoord(lua_State *L) +{ + auto pos = CheckCoordXYZ(L, 1, true); + return Lua::PushPosXYZ(L, Maps::getTileBlockCoord(pos)); +} + +static int maps_getBlockOrigin(lua_State *L) +{ + auto pos = CheckCoordXYZ(L, 1, true); + return Lua::PushPosXYZ(L, Maps::getBlockOrigin(pos)); +} + +static int maps_getTileBlockOffset(lua_State *L) +{ + auto pos = CheckCoordXYZ(L, 1, true); + return Lua::PushPosXYZ(L, Maps::getTileBlockOffset(pos)); +} + static int maps_getTileType(lua_State *L) { auto pos = CheckCoordXYZ(L, 1, true); @@ -2565,6 +2768,18 @@ static int maps_getTileBiomeRgn(lua_State *L) return Lua::PushPosXY(L, Maps::getTileBiomeRgn(pos)); } +static int maps_getCurrentWeather(lua_State *L) +{ + if (lua_gettop(L) == 0) + { + lua_pushinteger(L, Maps::getCurrentWeather()); + } else { + auto pos = CheckCoordXYZ(L, 1, true); + lua_pushinteger(L, Maps::getCurrentWeather(pos)); + } + return 1; +} + static int maps_getPlantAtTile(lua_State *L) { auto pos = CheckCoordXYZ(L, 1, true); @@ -2603,6 +2818,7 @@ static int maps_setTileAquifer(lua_State* L) case 1: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p); + break; case 2: Lua::CheckDFAssign(L, &p, 1); rv = Maps::setTileAquifer(p, lua_toboolean(L, 2)); @@ -2627,21 +2843,306 @@ static int maps_removeTileAquifer(lua_State* L) return 1; } +static int maps_addMaterialSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + int16_t mat = lua_tointeger(L, 2); + int32_t matg = lua_tointeger(L, 3); + df::matter_state state = (df::matter_state)lua_tointeger(L, 4); + int32_t amount = lua_tointeger(L, 5); + rv = Maps::addMaterialSpatter(pos, mat, matg, state, amount); + + lua_pushinteger(L, rv); + return 1; +} + +static int maps_addItemSpatter(lua_State *L) +{ + int32_t rv; + df::coord pos; + + Lua::CheckDFAssign(L, &pos, 1); + df::item_type i_type = (df::item_type)lua_tointeger(L, 2); + int16_t i_subtype = lua_tointeger(L, 3); + int16_t i_subcat1 = lua_tointeger(L, 4); + int32_t i_subcat2 = lua_tointeger(L, 5); + int32_t print_variant = lua_tointeger(L, 6); + int32_t amount = lua_tointeger(L, 7); + rv = Maps::addItemSpatter(pos, i_type, i_subtype, i_subcat1, i_subcat2, print_variant, amount); + + lua_pushinteger(L, rv); + return 1; +} + +/* + * Spec-table parsing helpers for maps_forEachTile. + */ + +// Resolve an enum value given as a number or as a DFHack enum item name. +template +static int check_enum_int(lua_State *L, int idx, const char *context) +{ + if (lua_isnumber(L, idx)) + return (int)lua_tointeger(L, idx); + if (lua_isstring(L, idx)) { + E val; + if (find_enum_item(&val, lua_tostring(L, idx))) + return (int)val; + luaL_error(L, "forEachTile: unknown %s '%s'", context, lua_tostring(L, idx)); + } + luaL_error(L, "forEachTile: %s must be a number or enum name", context); + return 0; +} + +// spec[key]: a single enum value or an array of them; inserted into out. +template +static void parse_enum_set(lua_State *L, int tbl, const char *key, + std::unordered_set &out) +{ + lua_getfield(L, tbl, key); + if (lua_istable(L, -1)) { + int spec = lua_absindex(L, -1); + int n = (int)luaL_len(L, spec); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, spec, i); + out.insert((V)check_enum_int(L, -1, key)); + lua_pop(L, 1); + } + } + else if (!lua_isnil(L, -1)) + out.insert((V)check_enum_int(L, -1, key)); + lua_pop(L, 1); +} + +// spec[key]: a table of bitfield field name -> required value. Compiles to a +// (value & mask) == bits test accumulated into mask and bits. +template +static void parse_bitfield_spec(lua_State *L, int tbl, const char *key, + uint32_t &mask, uint32_t &bits) +{ + lua_getfield(L, tbl, key); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + luaL_checktype(L, -1, LUA_TTABLE); + int spec = lua_absindex(L, -1); + lua_pushnil(L); + while (lua_next(L, spec)) { + if (lua_type(L, -2) != LUA_TSTRING) + luaL_error(L, "forEachTile: %s field names must be strings", key); + const char *name = lua_tostring(L, -2); + unsigned idx; + if (!find_bitfield_field(&idx, name, (BF*)NULL)) + luaL_error(L, "forEachTile: unknown %s field '%s'", key, name); + int width = df::bitfield_traits::bits[idx].size; + int value; + if (lua_isboolean(L, -1)) + value = lua_toboolean(L, -1) ? 1 : 0; + else + value = (int)luaL_checkinteger(L, -1); + if (value < 0 || value >= (1 << width)) + luaL_error(L, "forEachTile: %s.%s value %d out of range", key, name, value); + uint32_t fmask = ((1u << width) - 1) << idx; + mask |= fmask; + bits |= (uint32_t)value << idx & fmask; + lua_pop(L, 1); + } + lua_pop(L, 1); +} + +// Wrap the Lua function at stack index fn as a per-tile predicate/callback. +// The function is invoked as fn(x, y, z, block, localx, localy, tiletype), +// where localx/localy are the tile's coordinates within the block. If truthy +// is set, the wrapper returns whether the function returned a truthy value +// (for filters); otherwise only a return value of exactly false stops the +// scan (for callbacks). +static std::function +wrap_lua_tile_fn(lua_State *L, int fn, bool truthy = false) +{ + return [L, fn, truthy](df::map_block *block, df::coord2d local, df::coord pos) { + lua_pushvalue(L, fn); + lua_pushinteger(L, pos.x); + lua_pushinteger(L, pos.y); + lua_pushinteger(L, pos.z); + Lua::PushDFObject(L, block); + lua_pushinteger(L, local.x); + lua_pushinteger(L, local.y); + lua_pushinteger(L, (int)block->tiletype[local.x][local.y]); + lua_call(L, 7, 1); + bool cont = truthy ? lua_toboolean(L, -1) != 0 + : !(lua_isboolean(L, -1) && !lua_toboolean(L, -1)); + lua_pop(L, 1); + return cont; + }; +} + +// If spec[key] is a function, leave it on the stack and return its absolute +// index wrapped as a tile function; otherwise return an empty std::function. +static std::function +opt_lua_tile_fn(lua_State *L, int tbl, const char *key, bool truthy = false) +{ + lua_getfield(L, tbl, key); + if (lua_isfunction(L, -1)) + return wrap_lua_tile_fn(L, lua_absindex(L, -1), truthy); + lua_pop(L, 1); + return {}; +} + +static int maps_forEachTile(lua_State *L) +{ + // Bounds forms: {x1,y1,z1,x2,y2,z2} | pos1, pos2 | x1,y1,z1,x2,y2,z2 + cuboid bounds; + int spec_base; + if (lua_isnumber(L, 1)) { + bounds = cuboid( + (int16_t)luaL_checkinteger(L, 1), (int16_t)luaL_checkinteger(L, 2), + (int16_t)luaL_checkinteger(L, 3), (int16_t)luaL_checkinteger(L, 4), + (int16_t)luaL_checkinteger(L, 5), (int16_t)luaL_checkinteger(L, 6)); + spec_base = 7; + } + else if (lua_istable(L, 1) && luaL_len(L, 1) == 6) { + int16_t v[6]; + for (int i = 0; i < 6; i++) { + lua_rawgeti(L, 1, i + 1); + v[i] = (int16_t)luaL_checkinteger(L, -1); + lua_pop(L, 1); + } + bounds = cuboid(v[0], v[1], v[2], v[3], v[4], v[5]); + spec_base = 2; + } + else { + df::coord p1, p2; + Lua::CheckDFAssign(L, &p1, 1); + Lua::CheckDFAssign(L, &p2, 2); + bounds = cuboid(p1, p2); + spec_base = 3; + } + + Maps::TileFilter filter; + Maps::TileActions actions; + + // Any Lua functions found in the spec tables are left on the stack so the + // wrapped references stay valid for the duration of the scan. + int base = lua_gettop(L); + + int fidx = spec_base, aidx = spec_base + 1; + if (lua_isfunction(L, fidx)) + filter.extra = wrap_lua_tile_fn(L, fidx, true); + else if (lua_istable(L, fidx)) { + parse_enum_set(L, fidx, "tiletype", filter.tiletypes); + parse_enum_set(L, fidx, "tiletypes", filter.tiletypes); + parse_enum_set(L, fidx, "material", filter.materials); + parse_enum_set(L, fidx, "shape", filter.shapes); + parse_enum_set(L, fidx, "shape_basic", filter.shapes_basic); + parse_enum_set(L, fidx, "special", filter.specials); + parse_enum_set(L, fidx, "variant", filter.variants); + parse_bitfield_spec(L, fidx, "designation", + filter.designation_mask, filter.designation_bits); + parse_bitfield_spec(L, fidx, "occupancy", + filter.occupancy_mask, filter.occupancy_bits); + filter.extra = opt_lua_tile_fn(L, fidx, "filter", true); + } + else if (!lua_isnoneornil(L, fidx)) + return luaL_error(L, "forEachTile: filter must be a table or function"); + + if (lua_isfunction(L, aidx)) + actions.callback = wrap_lua_tile_fn(L, aidx); + else if (lua_istable(L, aidx)) { + lua_getfield(L, aidx, "set_tiletype"); + if (lua_istable(L, -1)) { + // {[tiletype] = replacement_tiletype} map; keys and values may + // be numbers or enum names + int spec = lua_absindex(L, -1); + lua_pushnil(L); + while (lua_next(L, spec)) { + df::tiletype from = (df::tiletype)check_enum_int( + L, -2, "set_tiletype key"); + df::tiletype to = (df::tiletype)check_enum_int( + L, -1, "set_tiletype value"); + actions.tiletype_map[from] = to; + lua_pop(L, 1); + } + } + else if (!lua_isnil(L, -1)) + actions.set_tiletype = (df::tiletype)check_enum_int(L, -1, "set_tiletype"); + lua_pop(L, 1); + + parse_bitfield_spec(L, aidx, "designation", + actions.designation_clear, actions.designation_set); + parse_bitfield_spec(L, aidx, "occupancy", + actions.occupancy_clear, actions.occupancy_set); + + lua_getfield(L, aidx, "construct"); + if (lua_istable(L, -1)) { + int spec = lua_absindex(L, -1); + actions.construct = true; + lua_getfield(L, spec, "item_type"); + if (!lua_isnil(L, -1)) + actions.construct_item_type = (df::item_type)check_enum_int(L, -1, "construct.item_type"); + lua_pop(L, 1); + get_int_field(L, &actions.construct_item_subtype, spec, "item_subtype", -1); + get_int_field(L, &actions.construct_mat_type, spec, "mat_type", -1); + get_int_field(L, &actions.construct_mat_index, spec, "mat_index", -1); + lua_getfield(L, spec, "tiletype"); + if (!lua_isnil(L, -1)) + actions.construct_tiletype = (df::tiletype)check_enum_int(L, -1, "construct.tiletype"); + lua_pop(L, 1); + uint32_t fmask = 0, fbits = 0; + parse_bitfield_spec(L, spec, "flags", fmask, fbits); + actions.construct_flags = (uint8_t)fbits; + } + else if (!lua_isnil(L, -1)) + return luaL_error(L, "forEachTile: construct must be a table"); + lua_pop(L, 1); + + actions.callback = opt_lua_tile_fn(L, aidx, "callback"); + } + else if (!lua_isnoneornil(L, aidx)) + return luaL_error(L, "forEachTile: actions must be a table or function"); + + auto result = Maps::forEachTile(bounds, filter, actions); + + lua_settop(L, base); // drop wrapped spec functions + lua_newtable(L); + lua_pushinteger(L, result.tiles_scanned); + lua_setfield(L, -2, "scanned"); + lua_pushinteger(L, result.tiles_matched); + lua_setfield(L, -2, "matched"); + lua_pushinteger(L, result.tiletypes_changed); + lua_setfield(L, -2, "changed"); + lua_pushinteger(L, result.constructions_added); + lua_setfield(L, -2, "constructed"); + lua_pushboolean(L, result.aborted); + lua_setfield(L, -2, "aborted"); + return 1; +} + static const luaL_Reg dfhack_maps_funcs[] = { { "isValidTilePos", maps_isValidTilePos }, { "isTileVisible", maps_isTileVisible }, { "getTileBlock", maps_getTileBlock }, { "ensureTileBlock", maps_ensureTileBlock }, + { "getTileBlockCoord", maps_getTileBlockCoord }, + { "getBlockOrigin", maps_getBlockOrigin }, + { "getTileBlockOffset", maps_getTileBlockOffset }, { "getTileType", maps_getTileType }, { "getTileFlags", maps_getTileFlags }, { "getRegionBiome", maps_getRegionBiome }, { "getTileBiomeRgn", maps_getTileBiomeRgn }, + { "getCurrentWeather", maps_getCurrentWeather }, { "getPlantAtTile", maps_getPlantAtTile }, { "getBiomeType", maps_getBiomeType }, { "isTileAquifer", maps_isTileAquifer }, { "isTileHeavyAquifer", maps_isTileHeavyAquifer }, { "setTileAquifer", maps_setTileAquifer }, { "removeTileAquifer", maps_removeTileAquifer }, + { "addMaterialSpatter", maps_addMaterialSpatter }, + { "addItemSpatter", maps_addItemSpatter }, + { "forEachTile", maps_forEachTile }, { NULL, NULL } }; @@ -2662,20 +3163,17 @@ static const LuaWrapper::FunctionReg dfhack_world_module[] = { { NULL, NULL } }; -#define WORLD_GAMEMODE_WRAPPER(func) \ - static int world_gamemode_##func(lua_State *L) \ - { \ - int gametype = luaL_optint(L, 1, -1); \ - lua_pushboolean(L, World::func((df::game_type)gametype)); \ - return 1;\ - } -#define WORLD_GAMEMODE_FUNC(func) \ - {#func, world_gamemode_##func} +using gamemode_func = auto (df::game_type t) -> bool; +template +static int world_gamemode(lua_State* L) +{ + int gametype = luaL_optint(L, 1, -1); + lua_pushboolean(L, gmf((df::game_type)gametype)); + return 1; +} -WORLD_GAMEMODE_WRAPPER(isFortressMode); -WORLD_GAMEMODE_WRAPPER(isAdventureMode); -WORLD_GAMEMODE_WRAPPER(isArena); -WORLD_GAMEMODE_WRAPPER(isLegends); +#define WORLD_GAMEMODE_FUNC(func) \ + {#func, world_gamemode} static const luaL_Reg dfhack_world_funcs[] = { WORLD_GAMEMODE_FUNC(isFortressMode), @@ -2732,6 +3230,7 @@ static bool buildings_containsTile(df::building *bld, int x, int y) { static const LuaWrapper::FunctionReg dfhack_buildings_module[] = { WRAPM(Buildings, getGeneralRef), WRAPM(Buildings, getSpecificRef), + WRAPM(Buildings, getOwner), WRAPM(Buildings, setOwner), WRAPM(Buildings, allocInstance), WRAPM(Buildings, checkFreeTiles), @@ -2815,7 +3314,7 @@ int buildings_setSize(lua_State *state) lua_pushinteger(state, size.x); lua_pushinteger(state, size.y); lua_pushinteger(state, area); - lua_pushinteger(state, Buildings::countExtentTiles(&bld->room, area)); + lua_pushinteger(state, Buildings::countExtentTiles(bld, area)); return 5; } else @@ -2936,6 +3435,34 @@ static int screen_readTile(lua_State *L) return 1; } +static int screen_paintMapPortTile(lua_State *L) +{ + Pen pen; + Lua::CheckPen(L, &pen, 1); + int x = luaL_checkint(L, 2); + int y = luaL_checkint(L, 3); + if (lua_gettop(L) >= 4 && !lua_isnil(L, 4)) + { + if (lua_type(L, 4) == LUA_TSTRING) + pen.ch = lua_tostring(L, 4)[0]; + else + pen.ch = luaL_checkint(L, 4); + } + if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) + pen.tile = luaL_checkint(L, 5); + lua_pushboolean(L, Screen::paintMapPortTile(pen, x, y)); + return 1; +} + +static int screen_readMapPortTile(lua_State *L) +{ + int x = luaL_checkint(L, 1); + int y = luaL_checkint(L, 2); + Pen pen = Screen::readMapPortTile(x, y, &df::graphic_map_portst::screentexpos_site); + Lua::Push(L, pen); + return 1; +} + static int screen_paintString(lua_State *L) { Pen pen; @@ -3123,6 +3650,8 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "getWindowSize", screen_getWindowSize }, { "paintTile", screen_paintTile }, { "readTile", screen_readTile }, + { "paintMapPortTile", screen_paintMapPortTile }, + { "readMapPortTile", screen_readMapPortTile }, { "paintString", screen_paintString }, { "fillRect", screen_fillRect }, { "findGraphicsTile", screen_findGraphicsTile }, @@ -3151,9 +3680,10 @@ static const LuaWrapper::FunctionReg dfhack_filesystem_module[] = { WRAPM(Filesystem, exists), WRAPM(Filesystem, isfile), WRAPM(Filesystem, isdir), - WRAPM(Filesystem, atime), - WRAPM(Filesystem, ctime), WRAPM(Filesystem, mtime), + WRAPM(Filesystem, canonicalize), + WRAPM(Filesystem, getInstallDir), + WRAPM(Filesystem, getBaseDir), {NULL, NULL} }; @@ -3161,7 +3691,7 @@ static int filesystem_listdir(lua_State *L) { luaL_checktype(L,1,LUA_TSTRING); string dir=lua_tostring(L,1); - vector files; + vector files; int err = DFHack::Filesystem::listdir(dir, files); if (err) { @@ -3174,7 +3704,7 @@ static int filesystem_listdir(lua_State *L) for(size_t i=0;i= 3 && !lua_isnil(L, 3)) include_prefix = lua_toboolean(L, 3); - std::map files; + std::map files; int err = DFHack::Filesystem::listdir_recursive(dir, files, depth, include_prefix); if (err != 0 && err != -1) { lua_pushnil(L); @@ -3205,7 +3735,7 @@ static int filesystem_listdir_recursive(lua_State *L) lua_pushinteger(L, i++); lua_newtable(L); lua_pushstring(L, "path"); - lua_pushstring(L, (it->first).c_str()); + lua_pushstring(L, Filesystem::as_string(it->first).c_str()); lua_settable(L, -3); lua_pushstring(L, "isdir"); lua_pushboolean(L, it->second); @@ -3490,7 +4020,7 @@ static void setPreferredNumberFormat(color_ostream & out, int32_t type_int) { set_preferred_number_format_type(type); break; default: - WARN(luaapi, out).print("invalid number format enum value: %d\n", type_int); + WARN(luaapi, out).print("invalid number format enum value: {}\n", type_int); } } @@ -3574,7 +4104,7 @@ static int internal_setAddress(lua_State *L) // Print via printerr, so that it is definitely logged to stderr.log. uintptr_t iaddr = addr - Core::getInstance().vinfo->getRebaseDelta(); - fprintf(stderr, "Setting global '%s' to %p (%p)\n", name.c_str(), (void*)addr, (void*)iaddr); + fmt::print(stderr, "Setting global '{}' to {} ({})\n", name, (addr), (void*)iaddr); fflush(stderr); return 1; @@ -3784,32 +4314,39 @@ static int internal_diffscan(lua_State *L) bool has_newv = !lua_isnil(L, 7); bool has_diffv = !lua_isnil(L, 8); -#define LOOP(esz, etype) \ - case esz: { \ - etype *pold = (etype*)old_data; \ - etype *pnew = (etype*)new_data; \ - etype oldv = (etype)luaL_optint(L, 6, 0); \ - etype newv = (etype)luaL_optint(L, 7, 0); \ - etype diffv = (etype)luaL_optint(L, 8, 0); \ - for (int i = start_idx; i < end_idx; i++) { \ - if (pold[i] == pnew[i]) continue; \ - if (has_oldv && pold[i] != oldv) continue; \ - if (has_newv && pnew[i] != newv) continue; \ - if (has_diffv && etype(pnew[i]-pold[i]) != diffv) continue; \ - lua_pushinteger(L, i); return 1; \ - } \ - break; \ - } + auto loop = [&]() -> std::optional + { + etype* pold = (etype*)old_data; + etype* pnew = (etype*)new_data; + etype oldv = (etype)luaL_optint(L, 6, 0); + etype newv = (etype)luaL_optint(L, 7, 0); + etype diffv = (etype)luaL_optint(L, 8, 0); + for (int i = start_idx; i < end_idx; i++) { + if (pold[i] == pnew[i]) continue; + if (has_oldv && pold[i] != oldv) continue; + if (has_newv && pnew[i] != newv) continue; + if (has_diffv && etype(pnew[i] - pold[i]) != diffv) continue; + return i; + } + return std::nullopt; + }; + + std::optional res; switch (eltsize) { - LOOP(1, uint8_t); - LOOP(2, uint16_t); - LOOP(4, uint32_t); - default: - luaL_argerror(L, 5, "invalid element size"); + case 1: + res = loop.operator()(); break; + case 2: + res = loop.operator()(); break; + case 4: + res = loop.operator()(); break; + default: + luaL_argerror(L, 5, "invalid element size"); } -#undef LOOP + if (res) + lua_pushinteger(L,*res); + else + lua_pushnil(L); - lua_pushnil(L); return 1; } @@ -3923,6 +4460,9 @@ static int internal_getModifiers(lua_State *L) lua_pushstring(L, "alt"); lua_pushboolean(L, modstate & DFH_MOD_ALT); lua_settable(L, -3); + lua_pushstring(L, "super"); + lua_pushboolean(L, modstate & DFH_MOD_SUPER); + lua_settable(L, -3); return 1; } @@ -3945,12 +4485,12 @@ static int internal_getScriptPaths(lua_State *L) { int i = 1; lua_newtable(L); - vector paths; + vector paths; Core::getInstance().getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { lua_pushinteger(L, i++); - lua_pushstring(L, it->c_str()); + lua_pushstring(L, it->string().c_str()); lua_settable(L, -3); } return 1; @@ -3959,9 +4499,9 @@ static int internal_getScriptPaths(lua_State *L) static int internal_findScript(lua_State *L) { const char *name = luaL_checkstring(L, 1); - string path = Core::getInstance().findScript(name); - if (path.size()) - lua_pushstring(L, path.c_str()); + std::filesystem::path path = Core::getInstance().findScript(name); + if (!path.empty()) + lua_pushstring(L, Filesystem::as_string(path).c_str()); else lua_pushnil(L); return 1; @@ -4264,6 +4804,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "translation", dfhack_translation_module); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); + OpenModule(state, "hotkey", dfhack_hotkey_module, dfhack_hotkey_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index c0f8bd1d2e..44fe872cb8 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -33,6 +33,7 @@ distribution. #include "DataFuncs.h" #include "LuaWrapper.h" #include "LuaTools.h" +#include "Crashlog.h" #include "MiscUtils.h" #include "DFHackVersion.h" #include "PluginManager.h" @@ -244,12 +245,12 @@ static void check_valid_ptr_index(lua_State *state, int val_index) } } -static void signal_typeid_error(color_ostream *out, lua_State *state, - const type_identity *type, const char *msg, +static void signal_typeid_error(color_ostream* out, lua_State* state, + const type_identity* type, std::string_view msg, int val_index, bool perr, bool signal) { std::string typestr = type ? type->getFullName() : "any pointer"; - std::string error = stl_sprintf(msg, typestr.c_str()); + std::string error = fmt::vformat(msg, fmt::make_format_args(typestr)); //FIXME: C++26 if (signal) { @@ -261,7 +262,7 @@ static void signal_typeid_error(color_ostream *out, lua_State *state, else if (perr) { if (out) - out->printerr("%s", error.c_str()); + out->printerr("{}", error); else dfhack_printerr(state, error); } @@ -282,7 +283,7 @@ void *DFHack::Lua::CheckDFObject(lua_State *state, const type_identity *type, in void *rv = get_object_internal(state, type, val_index, exact_type, false); if (!rv) - signal_typeid_error(NULL, state, type, "invalid pointer type; expected: %s", + signal_typeid_error(NULL, state, type, "invalid pointer type; expected: {}", val_index, false, true); return rv; @@ -366,9 +367,9 @@ static int lua_dfhack_print(lua_State *S) { std::string str = lua_print_fmt(S); if (color_ostream *out = Lua::GetOutput(S)) - out->print("%s", str.c_str());//*out << str; + out->print("{}", str); else - Core::print("%s", str.c_str()); + Core::print("{}", str); return 0; } @@ -378,16 +379,16 @@ static int lua_dfhack_println(lua_State *S) if (color_ostream *out = Lua::GetOutput(S)) *out << str << std::endl; else - Core::print("%s\n", str.c_str()); + Core::print("{}\n", str); return 0; } void dfhack_printerr(lua_State *S, const std::string &str) { if (color_ostream *out = Lua::GetOutput(S)) - out->printerr("%s\n", str.c_str()); + out->printerr("{}\n", str); else - Core::printerr("%s\n", str.c_str()); + Core::printerr("{}\n", str); } static int lua_dfhack_printerr(lua_State *S) @@ -426,7 +427,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) DFHack::CommandHistory hist; if (hfile) - hist.load(hfile); + hist.load(std::filesystem::path{ hfile }); std::string ret; int rv = pstream->lineedit(prompt, ret, hist); @@ -446,7 +447,7 @@ static int dfhack_lineedit_sync(lua_State *S, Console *pstream) else { if (hfile) - hist.save(hfile); + hist.save(std::filesystem::path{ hfile }); lua_pushlstring(S, ret.data(), ret.size()); return 1; } @@ -505,11 +506,19 @@ volatile std::sig_atomic_t lstop = 0; static void interrupt_hook (lua_State *L, lua_Debug *ar); static void interrupt_init (lua_State *L) { - lua_sethook(L, interrupt_hook, LUA_MASKCOUNT, 256); + lua_sethook(L, interrupt_hook, LUA_MASKCOUNT, 16384); } +constexpr auto interrupt_log_buffer_size = 64; +using timer = std::chrono::high_resolution_clock; +static std::array interrupt_log_buffer; +static size_t interrupt_log_buffer_index = 0; + static void interrupt_hook (lua_State *L, lua_Debug *ar) { + interrupt_log_buffer[interrupt_log_buffer_index] = timer::now(); + interrupt_log_buffer_index = (interrupt_log_buffer_index + 1) % interrupt_log_buffer_size; + if (lstop) { lstop = 0; @@ -518,6 +527,24 @@ static void interrupt_hook (lua_State *L, lua_Debug *ar) } } +static int dfhack_dump_interrupt_timing_data(lua_State* L) +{ + int base = lua_gettop(L); + lua_createtable(L, interrupt_log_buffer_size-1, 0); + auto last = timer::now(); + for (auto idx = 0; idx < interrupt_log_buffer_size; idx++) + { + auto i = (idx + interrupt_log_buffer_index) % interrupt_log_buffer_size; + auto dur = interrupt_log_buffer[i] - last; + last = interrupt_log_buffer[i]; + if (idx>0) { + lua_pushinteger(L, std::chrono::duration_cast(dur).count()); + lua_rawseti(L, base + 1, idx); + } + } + return 1; +} + bool DFHack::Lua::Interrupt (bool force) { lua_State *L = DFHack::Core::getInstance().getLuaState(true); @@ -563,7 +590,7 @@ static void report_error(lua_State *L, color_ostream *out = NULL, bool pop = fal assert(msg); if (out) - out->printerr("%s\n", msg); + out->printerr("{}\n", msg); else dfhack_printerr(L, msg); @@ -810,10 +837,26 @@ static int dfhack_safecall (lua_State *L) return Lua::TailPCallK(L, lua_gettop(L) - 2, LUA_MULTRET, 1, 0); } +// The lua_State through which this thread is currently executing a DFHack +// Lua entry point, or nullptr. Consulted by the crash reporter. +static thread_local lua_State *t_active_state = nullptr; + +lua_State *DFHack::Crashlog::active_state() { + return t_active_state; +} + +lua_State *DFHack::Crashlog::set_active_state(lua_State *state) { + lua_State *old = t_active_state; + t_active_state = state; + return old; +} + bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres, bool perr) { AssertCoreSuspend(L); + Crashlog::ActiveStateGuard lua_guard(L); + int base = lua_gettop(L) - nargs; color_ostream *cur_out = Lua::GetOutput(L); @@ -842,7 +885,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!lua_checkstack(L, 1 + nargs) || !Lua::PushModulePublic(out, L, module_name, fn_name)) { if (perr) - out.printerr("Failed to load %s Lua code\n", module_name); + out.printerr("Failed to load {} Lua code\n", module_name); return false; } @@ -850,7 +893,7 @@ bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, if (!Lua::SafeCall(out, L, nargs, nres, perr)) { if (perr) - out.printerr("Failed Lua call to '%s.%s'\n", module_name, fn_name); + out.printerr("Failed Lua call to '{}.{}'\n", module_name, fn_name); return false; } @@ -975,6 +1018,8 @@ int DFHack::Lua::SafeResume(color_ostream &out, lua_State *from, lua_State *thre { AssertCoreSuspend(from); + Crashlog::ActiveStateGuard lua_guard(thread ? thread : from); + color_ostream *cur_out = Lua::GetOutput(from); set_dfhack_output(from, &out); @@ -1077,7 +1122,7 @@ bool DFHack::Lua::Require(color_ostream &out, lua_State *state, } static bool doAssignDFObject(color_ostream *out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact, bool perr, bool signal) { if (signal) @@ -1100,7 +1145,7 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } else if (!lua_isuserdata(state, val_index)) { - signal_typeid_error(out, state, type, "pointer to %s expected", + signal_typeid_error(out, state, type, "pointer to {} expected", val_index, perr, signal); return false; } @@ -1109,13 +1154,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, void *in_ptr = Lua::GetDFObject(state, type, val_index, exact); if (!in_ptr) { - signal_typeid_error(out, state, type, "incompatible pointer type: %s expected", + signal_typeid_error(out, state, type, "incompatible pointer type: {} expected", val_index, perr, signal); return false; } if (!type->copy(target, in_ptr)) { - signal_typeid_error(out, state, type, "no copy support for %s", + signal_typeid_error(out, state, type, "no copy support for {}", val_index, perr, signal); return false; } @@ -1124,13 +1169,13 @@ static bool doAssignDFObject(color_ostream *out, lua_State *state, } bool DFHack::Lua::AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type, bool perr) { return doAssignDFObject(&out, state, type, target, val_index, exact_type, perr, false); } -void DFHack::Lua::CheckDFAssign(lua_State *state, type_identity *type, +void DFHack::Lua::CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type) { doAssignDFObject(NULL, state, type, target, val_index, exact_type, false, true); @@ -1193,9 +1238,7 @@ static int resume_query_loop(color_ostream &out, return rv; } -bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg) +bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, DFHack::Lua::init_fn init) { if (!lua_checkstack(state, 20)) return false; @@ -1213,7 +1256,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, int base = lua_gettop(state); - if (!init(out, state, arg)) + if (!init(out, state)) { lua_settop(state, base); return false; @@ -1240,13 +1283,13 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, if (histfile != histname) { if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); hist.clear(); histname = histfile; if (!histname.empty()) - hist.load(histname.c_str()); + hist.load(std::filesystem::path{ histname }); } if (prompt.empty()) @@ -1269,7 +1312,7 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, } if (!histname.empty()) - hist.save(histname.c_str()); + hist.save(std::filesystem::path{ histname }); { CoreSuspender suspend; @@ -1283,40 +1326,32 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, return (rv == LUA_OK); } -namespace { - struct InterpreterArgs { - const char *prompt; - const char *hfile; - }; -} - -static bool init_interpreter(color_ostream &out, lua_State *state, void *info) +static bool init_interpreter(color_ostream &out, lua_State *state, std::string_view prompt, const std::filesystem::path& hfile) { - auto args = (InterpreterArgs*)info; lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); lua_getfield(state, -1, "interpreter"); lua_remove(state, -2); - lua_pushstring(state, args->prompt); - lua_pushstring(state, args->hfile); + lua_pushlstring(state, prompt.data(), prompt.size()); + lua_pushlstring(state, hfile.string().data(), hfile.string().size()); return true; } bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, - const char *prompt, const char *hfile) + std::string_view prompt, std::filesystem::path hfile) { if (!out.is_console()) return false; - if (!hfile) - hfile = "dfhack-config/lua.history"; - if (!prompt) + if (hfile.empty()) + hfile = DFHack::Core::getInstance().getConfigPath() / "lua.history"; + if (prompt.empty()) prompt = "lua"; - InterpreterArgs args; - args.prompt = prompt; - args.hfile = hfile; + auto init_fn = [&](color_ostream& out, lua_State* state) { + return init_interpreter(out, state, prompt, hfile); + }; - return RunCoreQueryLoop(out, state, init_interpreter, &args); + return RunCoreQueryLoop(out, state, init_fn); } static bool do_invoke_cleanup(lua_State *L, int nargs, int errorfun, bool success) @@ -1508,6 +1543,7 @@ static const luaL_Reg dfhack_funcs[] = { { "with_suspend", lua_dfhack_with_suspend }, { "open_plugin", dfhack_open_plugin }, { "curry", dfhack_curry }, + { "dump_interrupt_timing_data", dfhack_dump_interrupt_timing_data }, { NULL, NULL } }; @@ -2000,6 +2036,38 @@ int dfhack_timeout(lua_State *L) return 1; } +int dfhack_timeout_until(lua_State *L) +{ + using df::global::world; + + // Parse arguments + lua_Number tick = luaL_checknumber(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_settop(L, 2); + + if (!Core::getInstance().isWorldLoaded()) + { + lua_pushnil(L); + return 1; + } + + int delta = tick - world->frame_counter; + + if (delta <= 0) + luaL_error(L, "Invalid timeout: %d", delta); + + // Queue the timeout + int id = next_timeout_id++; + tick_timers.insert(std::pair(tick, id)); + + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_TIMEOUTS_TOKEN); + lua_swap(L); + lua_rawseti(L, -2, id); + + lua_pushinteger(L, id); + return 1; +} + int dfhack_timeout_active(lua_State *L) { int id = luaL_optint(L, 1, -1); @@ -2111,6 +2179,8 @@ static void Lua::Core::InitCoreContext(color_ostream &out) lua_pushcfunction(State, dfhack_timeout); lua_setfield(State, -2, "timeout"); + lua_pushcfunction(State, dfhack_timeout_until); + lua_setfield(State, -2, "timeout_until"); lua_pushcfunction(State, dfhack_timeout_active); lua_setfield(State, -2, "timeout_active"); @@ -2145,7 +2215,7 @@ void DFHack::Lua::Core::Reset(color_ostream &out, const char *where) if (top != 0) { - out.printerr("Common lua context stack top left at %d after %s.\n", top, where); + out.printerr("Common lua context stack top left at {} after {}.\n", top, where); lua_settop(State, 0); } diff --git a/library/LuaTools.test.cpp b/library/LuaTools.test.cpp new file mode 100644 index 0000000000..304ebf8899 --- /dev/null +++ b/library/LuaTools.test.cpp @@ -0,0 +1,74 @@ +// Regression test for https://github.com/DFHack/dfhack/issues/5450 +// (fixed by https://github.com/DFHack/dfhack/pull/5451) +// +// df::identity_traits::get() returns a pointer to a *const* instance of +// a type_identity subclass for most types. When the df_object constraint +// required a pointer to non-const type_identity, Lua::Push rejected those +// types and fell back to the bool overload, so e.g. a pointer to a vector +// arrived in Lua as a boolean. + +#include "LuaTools.h" +#include "LuaWrapper.h" + +#include +#include + +#include +#include + +// The constraint must accept types whose identity is exposed through a +// pointer-to-const. This is the compile-time half of the regression check: +// it fails to build if df_object ever becomes too strict again. +static_assert(DFHack::Lua::df_object>); +static_assert(DFHack::Lua::df_object); +static_assert(DFHack::Lua::df_object); + +class LuaPushTest : public testing::Test { +protected: + lua_State *L = nullptr; + + void SetUp() override + { + L = luaL_newstate(); + ASSERT_NE(nullptr, L); + luaL_openlibs(L); + DFHack::LuaWrapper::AttachDFGlobals(L); + } + + void TearDown() override + { + if (L) + lua_close(L); + } +}; + +TEST_F(LuaPushTest, PushVectorPointer) +{ + std::vector vec{ 7, 42 }; + + // NB: intentionally avoids df_object-constrained calls below so that + // this test still compiles if the constraint regresses - in that case + // Push() falls back to the bool overload and the checks fail at runtime + DFHack::Lua::Push(L, &vec); + + // the pointer must arrive as a wrapped DF object, not a boolean + ASSERT_EQ(LUA_TUSERDATA, lua_type(L, -1)); + ASSERT_EQ(DFHack::Lua::OBJ_REF, DFHack::Lua::IsDFObject(L, -1)); + + // the same pointer must round-trip back out of Lua + EXPECT_EQ(static_cast(&vec), + DFHack::Lua::GetDFObject(L, df::identity_traits>::get(), -1)); + + // and the pushed object must behave like the vector it wraps + lua_len(L, -1); + EXPECT_EQ(lua_Integer(vec.size()), lua_tointeger(L, -1)); + lua_pop(L, 1); + + lua_geti(L, -1, 0); + EXPECT_EQ(7, lua_tointeger(L, -1)); + lua_pop(L, 1); + + lua_geti(L, -1, 1); + EXPECT_EQ(42, lua_tointeger(L, -1)); + lua_pop(L, 1); +} diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index 1aa7711e68..45d1f8e8e3 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -41,6 +41,7 @@ distribution. #include "PluginManager.h" #include "MiscUtils.h" +#include "modules/Filesystem.h" #include #include @@ -114,86 +115,30 @@ void enum_identity::lua_write(lua_State *state, int fname_idx, void *ptr, int va base_type->lua_write(state, fname_idx, ptr, val_index); } -void df::integer_identity_base::lua_read(lua_State *state, int fname_idx, void *ptr) const +void DFHack::lua_read_path(lua_State* state, void* ptr) { - lua_pushinteger(state, read(ptr)); + auto ppath = (std::filesystem::path*)ptr; + auto str = DFHack::Filesystem::as_string(*ppath); + lua_pushlstring(state, (char*)str.data(), str.size()); } -void df::integer_identity_base::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const -{ - int is_num = 0; - auto value = lua_tointegerx(state, val_index, &is_num); - if (!is_num) - field_error(state, fname_idx, "integer expected", "write"); - write(ptr, value); -} - -void df::float_identity_base::lua_read(lua_State *state, int fname_idx, void *ptr) const -{ - lua_pushnumber(state, read(ptr)); -} - -void df::float_identity_base::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const -{ - if (!lua_isnumber(state, val_index)) - field_error(state, fname_idx, "number expected", "write"); - - write(ptr, lua_tonumber(state, val_index)); -} - -void df::bool_identity::lua_read(lua_State *state, int fname_idx, void *ptr) const -{ - lua_pushboolean(state, *(bool*)ptr); -} - -void df::bool_identity::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const -{ - char *pb = (char*)ptr; - - if (lua_isboolean(state, val_index) || lua_isnil(state, val_index)) - *pb = lua_toboolean(state, val_index); - else if (lua_isnumber(state, val_index)) - *pb = lua_tointeger(state, val_index); - else - field_error(state, fname_idx, "boolean or number expected", "write"); -} - -void df::ptr_string_identity::lua_read(lua_State *state, int fname_idx, void *ptr) const -{ - auto pstr = (char**)ptr; - if (*pstr) - lua_pushstring(state, *pstr); - else - lua_pushnil(state); -} - -void df::ptr_string_identity::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const -{ - field_error(state, fname_idx, "raw pointer string", "write"); -} - -void df::stl_string_identity::lua_read(lua_State *state, int fname_idx, void *ptr) const -{ - auto pstr = (std::string*)ptr; - lua_pushlstring(state, pstr->data(), pstr->size()); -} - -void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const +void DFHack::lua_write_path(lua_State* state, int fname_idx, void* ptr, int val_index) { size_t size; - const char *bytes = lua_tolstring(state, val_index, &size); + const char* bytes = lua_tolstring(state, val_index, &size); if (!bytes) - field_error(state, fname_idx, "string expected", "write"); + field_error(state, fname_idx, "path expected", "write"); - *(std::string*)ptr = std::string(bytes, size); + std::u8string str((char8_t*)bytes, size); + *(std::filesystem::path*)ptr = std::filesystem::path(str); } -void df::pointer_identity::lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target) +void df::pointer_identity_base::lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target) { push_object_internal(state, target, *(void**)ptr); } -void df::pointer_identity::lua_read(lua_State *state, int fname_idx, void *ptr) const +void df::pointer_identity_base::lua_read(lua_State *state, int fname_idx, void *ptr) const { lua_read(state, fname_idx, ptr, target); } @@ -257,7 +202,7 @@ static bool is_null(lua_State *state, int val_index) !lua_touserdata(state, val_index)); } -void df::pointer_identity::lua_write(lua_State *state, int fname_idx, void *ptr, +void df::pointer_identity_base::lua_write(lua_State *state, int fname_idx, void *ptr, const type_identity *target, int val_index) { auto pptr = (void**)ptr; @@ -281,7 +226,7 @@ void df::pointer_identity::lua_write(lua_State *state, int fname_idx, void *ptr, } } -void df::pointer_identity::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const +void df::pointer_identity_base::lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const { lua_write(state, fname_idx, ptr, target, val_index); } @@ -354,14 +299,14 @@ void ptr_container_identity::lua_item_read(lua_State *state, int fname_idx, void { auto id = (type_identity*)lua_touserdata(state, UPVAL_ITEM_ID); void *pitem = item_pointer(&df::identity_traits::identity, ptr, idx); - df::pointer_identity::lua_read(state, fname_idx, pitem, id); + df::pointer_identity_base::lua_read(state, fname_idx, pitem, id); } void ptr_container_identity::lua_item_write(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const { auto id = (type_identity*)lua_touserdata(state, UPVAL_ITEM_ID); void *pitem = item_pointer(&df::identity_traits::identity, ptr, idx); - df::pointer_identity::lua_write(state, fname_idx, pitem, id, val_index); + df::pointer_identity_base::lua_write(state, fname_idx, pitem, id, val_index); } bool ptr_container_identity::lua_insert2(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const @@ -369,7 +314,7 @@ bool ptr_container_identity::lua_insert2(lua_State *state, int fname_idx, void * auto id = (type_identity*)lua_touserdata(state, UPVAL_ITEM_ID); void *pitem = NULL; - df::pointer_identity::lua_write(state, fname_idx, &pitem, id, val_index); + df::pointer_identity_base::lua_write(state, fname_idx, &pitem, id, val_index); return insert(ptr, idx, pitem); } @@ -503,7 +448,7 @@ static void read_field(lua_State *state, const struct_field_info *field, void *p return; case struct_field_info::POINTER: - df::pointer_identity::lua_read(state, 2, ptr, field->type); + df::pointer_identity_base::lua_read(state, 2, ptr, field->type); return; case struct_field_info::CONTAINER: @@ -589,7 +534,7 @@ static void write_field(lua_State *state, const struct_field_info *field, void * return; case struct_field_info::POINTER: - df::pointer_identity::lua_write(state, 2, ptr, field->type, value_idx); + df::pointer_identity_base::lua_write(state, 2, ptr, field->type, value_idx); return; case struct_field_info::STATIC_ARRAY: @@ -1234,7 +1179,7 @@ int LuaWrapper::method_wrapper_core(lua_State *state, function_identity_base *id field_error(state, UPVAL_METHOD_NAME, e.what(), "invoke"); } catch (std::exception &e) { - std::string tmp = stl_sprintf("C++ exception: %s", e.what()); + std::string tmp = fmt::format("C++ exception: {}", e.what()); field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); } @@ -1752,7 +1697,7 @@ void other_vectors_identity::build_metatable(lua_State *state) const SetPtrMethods(state, base+1, base+2); } -void global_identity::build_metatable(lua_State *state) const +void global_identity_base::build_metatable(lua_State *state) const { int base = lua_gettop(state); MakeFieldMetatable(state, this, meta_global_index, meta_global_newindex, meta_struct_next, true); @@ -1821,12 +1766,12 @@ void LuaWrapper::push_adhoc_pointer(lua_State *state, void *ptr, const type_iden { /* * HACK: relies on - * 1) pointer_identity destructor being no-op + * 1) pointer_identity_base destructor being no-op * 2) lua gc never moving objects in memory */ - void *newobj = lua_newuserdata(state, sizeof(pointer_identity)); - id = new (newobj) pointer_identity(target); + void *newobj = lua_newuserdata(state, sizeof(pointer_identity_base)); + id = new (newobj) pointer_identity_base(target); SaveInTable(state, const_cast(target), &DFHACK_PTR_IDTABLE_TOKEN); lua_pop(state, 1); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 608ef95683..0d30429297 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -35,6 +35,8 @@ distribution. // must be last due to MS stupidity #include "DataDefs.h" #include "DataIdentity.h" +#include "Debug.h" +#include "HashUtil.h" #include "LuaWrapper.h" #include "LuaTools.h" @@ -46,10 +48,15 @@ distribution. using namespace DFHack; using namespace DFHack::LuaWrapper; +namespace DFHack +{ + DBG_DECLARE(lua, deprecation, DebugCategory::LWARNING); +} + /** * Report an error while accessing a field (index = field name). */ -void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) +[[noreturn]] void LuaWrapper::field_error(lua_State *state, int index, const char *err, const char *mode) { if (lua_islightuserdata(state, UPVAL_METATABLE)) lua_pushstring(state, "(global)"); @@ -59,14 +66,15 @@ void LuaWrapper::field_error(lua_State *state, int index, const char *err, const const char *fname = index ? lua_tostring(state, index) : "*"; luaL_error(state, "Cannot %s field %s.%s: %s.", mode, (cname ? cname : "?"), (fname ? fname : "?"), err); + std::abort(); // should never be reached but makes gcc happy } /* */ -static int change_error(lua_State *state) +[[noreturn]] static int change_error(lua_State *state) { luaL_error(state, "Attempt to change a read-only table.\n"); - return 0; + std::abort(); // should never be reached but makes gcc happy } /** @@ -131,7 +139,7 @@ bool LuaWrapper::LookupTypeInfo(lua_State *state, bool in_method) return true; } -void LuaWrapper::LookupInTable(lua_State *state, void *id, LuaToken *tname) +void LuaWrapper::LookupInTable(lua_State *state, const void *id, LuaToken *tname) { lua_rawgetp(state, LUA_REGISTRYINDEX, tname); lua_rawgetp(state, -1, id); @@ -167,12 +175,15 @@ static void BuildTypeMetatable(lua_State *state, const type_identity *type); void LuaWrapper::push_object_ref(lua_State *state, void *ptr) { // stack: [metatable] - auto ref = (DFRefHeader*)lua_newuserdata(state, sizeof(DFRefHeader)); - ref->ptr = ptr; - ref->field_info = NULL; - ref->tag_ptr = NULL; - ref->tag_identity = NULL; - ref->tag_attr = NULL; + void* stg = lua_newuserdata(state, sizeof(DFRefHeader)); + new (stg) DFRefHeader + { + .ptr = ptr, + .field_info = NULL, + .tag_ptr = NULL, + .tag_identity = NULL, + .tag_attr = NULL, + }; lua_swap(state); lua_setmetatable(state, -2); @@ -217,7 +228,7 @@ void LuaWrapper::push_object_internal(lua_State *state, const type_identity *typ if (type->type() == IDTYPE_CLASS) { - virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); + const virtual_identity *class_vid = virtual_identity::get(virtual_ptr(ptr)); if (class_vid) type = class_vid; } @@ -272,8 +283,8 @@ bool LuaWrapper::is_type_compatible(lua_State *state, const type_identity *type1 { case IDTYPE_POINTER: return is_type_compatible(state, - ((const pointer_identity*)type1)->getTarget(), 0, - ((const pointer_identity*)type2)->getTarget(), 0, + ((const pointer_identity_base*)type1)->getTarget(), 0, + ((const pointer_identity_base*)type2)->getTarget(), 0, exact_equal); break; @@ -293,8 +304,8 @@ bool LuaWrapper::is_type_compatible(lua_State *state, const type_identity *type1 case IDTYPE_STL_PTR_VECTOR: { - auto b1 = (const df::stl_ptr_vector_identity*)type1; - auto b2 = (const df::stl_ptr_vector_identity*)type2; + auto b1 = (const df::ptr_container_identity*)type1; + auto b2 = (const df::ptr_container_identity*)type2; const type_identity *item1 = b1->getItemType(), *item2 = b2->getItemType(); fetch_container_details(state, meta1, &item1, NULL); @@ -310,6 +321,8 @@ bool LuaWrapper::is_type_compatible(lua_State *state, const type_identity *type1 auto b1 = (struct_identity*)type1; auto b2 = (struct_identity*)type2; + if (b1->is_equivalent(b2)) return true; + return (!exact_equal && b1->is_subclass(b2)); } @@ -369,7 +382,9 @@ void *LuaWrapper::get_object_internal(lua_State *state, const type_identity *typ if (!LookupTypeInfo(state, in_method)) // metatable -> type? return NULL; - if (type && lua_touserdata(state, -1) != type) + type_identity* othertype = static_cast(lua_touserdata(state, -1)); + + if (type && othertype != type) { /* * If valid but different type, do an intelligent comparison. @@ -1005,7 +1020,8 @@ static int meta_type_tostring(lua_State *state) lua_getfield(state, -1, "__metatable"); const char *cname = lua_tostring(state, -1); - lua_pushstring(state, stl_sprintf("", cname).c_str()); + auto str = fmt::format("", cname); + lua_pushlstring(state, str.data(), str.size()); return 1; } @@ -1029,28 +1045,22 @@ static int meta_ptr_tostring(lua_State *state) lua_getfield(state, UPVAL_METATABLE, "__metatable"); const char *cname = lua_tostring(state, -1); - if (has_length) - lua_pushstring(state, stl_sprintf("<%s[%" PRIu64 "]: %p>", cname, length, (void*)ptr).c_str()); - else - lua_pushstring(state, stl_sprintf("<%s: %p>", cname, (void*)ptr).c_str()); + auto str = has_length ? + fmt::format("<{}[{}]: {}>", cname, length, static_cast(ptr)) : + fmt::format("<{}: {}>", cname, static_cast(ptr)); + + lua_pushlstring(state, str.data(), str.size()); + return 1; } /** - * Metamethod: __index for enum.attrs + * Push the attr entry for the given enum item value. + * Out-of-range values resolve to the default entry. */ -static int meta_enum_attr_index(lua_State *state) +static void push_enum_attr(lua_State *state, enum_identity *id, int64_t idx) { - if (!lua_isnumber(state, 2)) - lua_rawget(state, UPVAL_FIELDTABLE); - if (!lua_isnumber(state, 2)) - luaL_error(state, "Invalid index in enum.attrs[]"); - - auto id = (enum_identity*)lua_touserdata(state, lua_upvalueindex(2)); - auto *complex = id->getComplex(); - - int64_t idx = lua_tonumber(state, 2); - if (complex) + if (auto *complex = id->getComplex()) { auto it = complex->value_index_map.find(idx); if (it != complex->value_index_map.end()) @@ -1069,6 +1079,20 @@ static int meta_enum_attr_index(lua_State *state) auto atype = id->getAttrType(); push_object_internal(state, atype, ptr + unsigned(atype->byte_size()*idx)); +} + +/** + * Metamethod: __index for enum.attrs + */ +static int meta_enum_attr_index(lua_State *state) +{ + if (!lua_isnumber(state, 2)) + lua_rawget(state, UPVAL_FIELDTABLE); + if (!lua_isnumber(state, 2)) + luaL_error(state, "Invalid index in enum.attrs[]"); + + auto id = (enum_identity*)lua_touserdata(state, lua_upvalueindex(2)); + push_enum_attr(state, id, lua_tonumber(state, 2)); return 1; } @@ -1444,6 +1468,83 @@ static int complex_enum_ipairs(lua_State *L) return 3; } +/* + * enum.attrs ipairs() support + * + * enum.attrs[] returns a default entry for out-of-range indexes, so a plain + * ipairs() would never terminate. These iterators stop at the same place as + * ipairs() on the enum itself (DFHack/dfhack#1860). + * + * upvalues for the *_ipairs functions: + * 1: enum_identity + * 2: enum_identity::ComplexData (complex enums only) + * + * upvalues for the *_inext iterators (UPVAL_TYPETABLE is required by + * push_object_internal): + * 1: DFHACK_TYPETABLE + * 2: enum_identity (plain) or enum_identity::ComplexData (complex) + * 3: enum_identity (complex only) + */ + +static int meta_enum_attr_inext(lua_State *L) +{ + auto id = (enum_identity*)lua_touserdata(L, lua_upvalueindex(2)); + int64_t i = luaL_checkint(L, 2) + 1; + if (i <= id->getLastItem()) + { + lua_pushinteger(L, i); + push_enum_attr(L, id, i); + return 2; + } + else + { + lua_pushnil(L); + return 1; + } +} + +static int meta_enum_attr_ipairs(lua_State *L) +{ + auto id = (enum_identity*)lua_touserdata(L, lua_upvalueindex(1)); + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_TYPETABLE_TOKEN); + lua_pushvalue(L, lua_upvalueindex(1)); + lua_pushcclosure(L, meta_enum_attr_inext, 2); + lua_pushnil(L); + lua_pushinteger(L, id->getFirstItem() - 1); + return 3; +} + +static int complex_enum_attr_inext(lua_State *L) +{ + bool is_first = lua_isuserdata(L, 2); + int64_t i = (is_first) + ? ((enum_identity::ComplexData*)lua_touserdata(L, lua_upvalueindex(2)))->index_value_map[0] + : luaL_checkint(L, 2); + if (is_first || complex_enum_next_item_helper(L, i)) + { + auto id = (enum_identity*)lua_touserdata(L, lua_upvalueindex(3)); + lua_pushinteger(L, i); + push_enum_attr(L, id, i); + return 2; + } + else + { + lua_pushnil(L); + return 1; + } +} + +static int complex_enum_attr_ipairs(lua_State *L) +{ + lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_TYPETABLE_TOKEN); + lua_pushvalue(L, lua_upvalueindex(2)); + lua_pushvalue(L, lua_upvalueindex(1)); + lua_pushcclosure(L, complex_enum_attr_inext, 3); + lua_pushnil(L); + lua_pushlightuserdata(L, (void*)1); + return 3; +} + static void RenderTypeChildren(lua_State *state, const std::vector &children); @@ -1544,7 +1645,21 @@ static void FillEnumKeys(lua_State *state, int ix_meta, int ftable, enum_identit lua_pushvalue(state, base+1); lua_pushcclosure(state, meta_enum_attr_index, 3); - freeze_table(state, false, (eid->getFullName()+".attrs").c_str()); + freeze_table(state, true, (eid->getFullName()+".attrs").c_str()); + + lua_pushlightuserdata(state, eid); + if (complex) + { + lua_pushlightuserdata(state, (void*)complex); + lua_pushcclosure(state, complex_enum_attr_ipairs, 2); + } + else + { + lua_pushcclosure(state, meta_enum_attr_ipairs, 1); + } + lua_setfield(state, -2, "__ipairs"); + lua_pop(state, 1); + lua_setfield(state, ftable, "attrs"); } @@ -1641,7 +1756,7 @@ static void RenderType(lua_State *state, const compound_identity *node) case IDTYPE_UNION: // TODO: change this to union-type? what relies on this? lua_pushboolean(state, true); lua_setfield(state, ftable, "_union"); - // fall through + [[fallthrough]]; case IDTYPE_STRUCT: lua_pushstring(state, "struct-type"); lua_setfield(state, ftable, "_kind"); @@ -1858,6 +1973,51 @@ void LuaWrapper::AttachDFGlobals(lua_State *state) lua_pop(state, 1); } +namespace +{ + struct deprecation_notification + { + std::string name; + std::string source; + int line; + + bool operator==(const deprecation_notification& other) const = default; + + struct hash + { + std::size_t operator()(const deprecation_notification& n) const noexcept + { + return hash_value(n.name, n.source, n.line); + } + }; + }; + + static std::unordered_set deprecation_notification_history; +} + +void LuaWrapper::notify_deprecated(lua_State* state, std::string_view name, std::string_view message) +{ + lua_Debug ar; + std::optional notify; + + for (int depth = 0; lua_getstack(state, depth, &ar); depth++) + { + if (lua_getinfo(state, "nSl", &ar) && std::strcmp(ar.what, "Lua") == 0) + { + notify = deprecation_notification{std::string(name),ar.short_src,ar.currentline}; + break; + } + } + + if (!notify) return; + + if (!deprecation_notification_history.contains(*notify)) + { + WARN(deprecation).print("Deprecated function {} called from {} at line {}: {}\n", notify->name, notify->source, notify->line, message); + deprecation_notification_history.insert(*notify); + } +} + namespace DFHack { namespace LuaWrapper { struct LuaToken { int reserved; }; diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp new file mode 100644 index 0000000000..44aa865c42 --- /dev/null +++ b/library/MemoryPatcher.cpp @@ -0,0 +1,83 @@ +#include "Core.h" +#include "MemoryPatcher.h" + +namespace DFHack +{ + MemoryPatcher::MemoryPatcher(Process* p_) : p(p_) + { + if (!p) + p = Core::getInstance().p.get(); + } + + MemoryPatcher::~MemoryPatcher() + { + close(); + } + + bool MemoryPatcher::verifyAccess(void* target, size_t count, bool write) + { + auto* sptr = (uint8_t*)target; + uint8_t* eptr = sptr + count; + + // Find the valid memory ranges + if (ranges.empty()) + p->getMemRanges(ranges); + + // Find the ranges that this area spans + unsigned start = 0; + while (start < ranges.size() && ranges[start].end <= sptr) + start++; + if (start >= ranges.size() || ranges[start].start > sptr) + return false; + + unsigned end = start + 1; + while (end < ranges.size() && ranges[end].start < eptr) + { + if (ranges[end].start != ranges[end - 1].end) + return false; + end++; + } + if (ranges[end - 1].end < eptr) + return false; + + // Verify current permissions + for (unsigned i = start; i < end; i++) + if (!ranges[i].valid || !(ranges[i].read || ranges[i].execute) || ranges[i].shared) + return false; + + // Apply writable permissions & update + for (unsigned i = start; i < end; i++) + { + auto& perms = ranges[i]; + if ((perms.write || !write) && perms.read) + continue; + + save.push_back(perms); + perms.write = perms.read = true; + if (!p->setPermissions(perms, perms)) + return false; + } + + return true; + } + + bool MemoryPatcher::write(void* target, const void* src, size_t size) + { + if (!makeWritable(target, size)) + return false; + + memmove(target, src, size); + + p->flushCache(target, size); + return true; + } + + void MemoryPatcher::close() + { + for (size_t i = 0; i < save.size(); i++) + p->setPermissions(save[i], save[i]); + + save.clear(); + ranges.clear(); + }; +} diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 082657ea34..d91471803b 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -22,39 +22,49 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" #include "Export.h" #include "MiscUtils.h" #include "ColorText.h" -#include "modules/DFSDL.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#ifndef LINUX_BUILD -// We don't want min and max macros +#ifdef WIN32 #define NOMINMAX - #include - // Suppress warning which occurs in header on some WinSDK versions - // See dfhack/dfhack#5147 for more information - #pragma warning(push) - #pragma warning(disable:4091) - #include - #pragma warning(pop) -#else - #include - #include - #include +#define WIN32_LEAN_AND_MEAN +#include +#include +// Suppress warning which occurs in header on some WinSDK versions +// See dfhack/dfhack#5147 for more information +#pragma warning(push) +#pragma warning(disable:4091) +#include +#pragma warning(pop) #endif -#include -#include -#include -#include -#include - -#include -#include -#include -#include +#ifdef LINUX_BUILD +#include +#include +#include +#endif NumberFormatType preferred_number_format_type = NumberFormatType::DEFAULT; @@ -499,8 +509,8 @@ uint64_t GetTimeMs64() /* Character decoding */ // See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. -#define UTF8_ACCEPT 0 -#define UTF8_REJECT 12 +constexpr auto UTF8_ACCEPT = 0; +constexpr auto UTF8_REJECT = 12; static const uint8_t utf8d[] = { // The first part of the table maps bytes to character classes that @@ -653,20 +663,31 @@ std::string UTF2DF(const std::string &in) out.resize(pos); return out; } - -DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +static bool console_is_utf8() { - bool is_utf = false; #ifdef LINUX_BUILD + static bool checked = false; + static bool is_utf = false; + if (checked) + return is_utf; + std::string locale = ""; if (getenv("LANG")) locale += getenv("LANG"); if (getenv("LC_CTYPE")) locale += getenv("LC_CTYPE"); locale = toUpper_cp437(locale); - is_utf = (locale.find("UTF-8") != std::string::npos) || - (locale.find("UTF8") != std::string::npos); + is_utf = (locale.find("UTF-8") != std::string::npos) || (locale.find("UTF8") != std::string::npos); + checked = true; + return is_utf; +#else + return true; // Since DF 53.11, Windows console is always UTF-8 #endif +} + +DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in) +{ + bool is_utf = console_is_utf8(); return is_utf ? DF2UTF(in) : in; } diff --git a/library/PlugLoad.cpp b/library/PlugLoad.cpp index 059e6aef95..d3b7560773 100644 --- a/library/PlugLoad.cpp +++ b/library/PlugLoad.cpp @@ -1,24 +1,18 @@ -#include "Core.h" #include "Debug.h" -#include "Export.h" #include "PluginManager.h" -#include "Hooks.h" #include -#include #include -#include - -#include -#include #ifdef WIN32 #define NOMINMAX +#define WIN32_LEAN_AND_MEAN #include +#include #define global_search_handle() GetModuleHandle(nullptr) #define get_function_address(plugin, function) GetProcAddress((HMODULE)plugin, function) #define clear_error() -#define load_library(fn) LoadLibrary(fn) +#define load_library(fn) LoadLibraryExW(fn.wstring().c_str(), NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); #define close_library(handle) (!(FreeLibrary((HMODULE)handle))) #else #include @@ -33,7 +27,7 @@ #define global_search_handle() (RTLD_DEFAULT) #define get_function_address(plugin, function) dlsym((void*)plugin, function) #define clear_error() dlerror() -#define load_library(fn) dlopen(fn, RTLD_NOW | RTLD_LOCAL); +#define load_library(fn) dlopen(fn.c_str(), RTLD_NOW | RTLD_LOCAL); #define close_library(handle) dlclose((void*)handle) #endif @@ -71,13 +65,13 @@ namespace DFHack } } - DFLibrary * OpenPlugin (const char * filename) + DFLibrary * OpenPlugin (std::filesystem::path filename) { clear_error(); DFLibrary* ret = (DFLibrary*)load_library(filename); if (!ret) { auto error = get_error(); - WARN(plugins).print("OpenPlugin on %s failed: %s\n", filename, error.c_str()); + WARN(plugins).print("OpenPlugin on {} failed: {}\n", filename.string(), error); } return ret; } @@ -91,7 +85,7 @@ namespace DFHack if (res != 0) { auto error = get_error(); - WARN(plugins).print("ClosePlugin failed: %s\n", error.c_str()); + WARN(plugins).print("ClosePlugin failed: {}\n", error); } return (res == 0); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 458c0377c2..5052b93637 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -22,35 +22,46 @@ must not be misrepresented as being the original software. distribution. */ -#include "modules/EventManager.h" -#include "modules/Filesystem.h" -#include "modules/Screen.h" -#include "modules/World.h" -#include "Internal.h" +#include "PluginManager.h" + +#include "ColorText.h" #include "Core.h" +#include "CoreDefs.h" +#include "Format.h" +#include "LuaWrapper.h" +#include "LuaTools.h" #include "MemAccess.h" -#include "PluginManager.h" +#include "MiscUtils.h" #include "RemoteServer.h" -#include "Console.h" #include "Types.h" #include "VersionInfo.h" -#include "DataDefs.h" -#include "MiscUtils.h" -#include "DFHackVersion.h" - -#include "LuaWrapper.h" -#include "LuaTools.h" - -using namespace DFHack; +#include "modules/EventManager.h" +#include "modules/Filesystem.h" +#include "modules/Screen.h" +#include "modules/World.h" +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include -using namespace std; -#include +#include "df/viewscreen.h" + +#include +#include + +using namespace DFHack; +using std::string; #if defined(_LINUX) static const string plugin_suffix = ".plug.so"; @@ -60,14 +71,14 @@ using namespace std; static const string plugin_suffix = ".plug.dll"; #endif -static string getPluginPath() +static std::filesystem::path getPluginPath() { - return Core::getInstance().getHackPath() + "plugins/"; + return Core::getInstance().getHackPath() / "plugins"; } -static string getPluginPath (std::string name) +static std::filesystem::path getPluginPath (std::string name) { - return getPluginPath() + name + plugin_suffix; + return getPluginPath() / (name + plugin_suffix); } struct Plugin::RefLock @@ -175,7 +186,7 @@ struct Plugin::LuaEvent : public Lua::Event::Owner { } }; -Plugin::Plugin(Core * core, const std::string & path, +Plugin::Plugin(Core * core, const std::filesystem::path& path, const std::string &name, PluginManager * pm) :path(path), name(name), @@ -237,7 +248,7 @@ bool Plugin::load(color_ostream &con) else if(state != PS_UNLOADED && state != PS_DELETED) { if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be loaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be loaded\n", name); return false; } state = PS_LOADING; @@ -245,19 +256,19 @@ bool Plugin::load(color_ostream &con) // enter suspend CoreSuspender suspend; // open the library, etc - fprintf(stderr, "loading plugin %s\n", name.c_str()); - DFLibrary * plug = OpenPlugin(path.c_str()); + fmt::print(stderr, "loading plugin {}\n", name); + DFLibrary * plug = OpenPlugin(path); if(!plug) { RefAutolock lock(access); if (!Filesystem::isfile(path)) { - con.printerr("Plugin %s does not exist on disk\n", name.c_str()); + con.printerr("Plugin {} does not exist on disk\n", name); state = PS_DELETED; return false; } else { - con.printerr("Can't load plugin %s\n", name.c_str()); + con.printerr("Can't load plugin {}\n", name); state = PS_UNLOADED; return false; } @@ -266,7 +277,7 @@ bool Plugin::load(color_ostream &con) #define plugin_check_symbol(sym) \ if (!LookupPlugin(plug, sym)) \ { \ - con.printerr("Plugin %s: missing symbol: %s\n", name.c_str(), sym); \ + con.printerr("Plugin {}: missing symbol: {}\n", name, sym); \ plugin_abort_load; \ return false; \ } @@ -280,7 +291,7 @@ bool Plugin::load(color_ostream &con) const char ** plug_name =(const char ** ) LookupPlugin(plug, "plugin_name"); if (name != *plug_name) { - con.printerr("Plugin %s: name mismatch, claims to be %s\n", name.c_str(), *plug_name); + con.printerr("Plugin {}: name mismatch, claims to be {}\n", name, *plug_name); plugin_abort_load; return false; } @@ -293,15 +304,15 @@ bool Plugin::load(color_ostream &con) const char *plug_git_desc = plug_git_desc_ptr ? *plug_git_desc_ptr : "unknown"; if (*plugin_abi_version != Version::dfhack_abi_version()) { - con.printerr("Plugin %s: ABI version mismatch (Plugin: %i, DFHack: %i)\n", + con.printerr("Plugin {}: ABI version mismatch (Plugin: {}, DFHack: {})\n", *plug_name, *plugin_abi_version, Version::dfhack_abi_version()); plugin_abort_load; return false; } if (strcmp(dfhack_version, *plug_version) != 0) { - con.printerr("Plugin %s was not built for this version of DFHack.\n" - "Plugin: %s, DFHack: %s\n", *plug_name, *plug_version, dfhack_version); + con.printerr("Plugin {} was not built for this version of DFHack.\n" + "Plugin: {}, DFHack: {}\n", *plug_name, *plug_version, dfhack_version); plugin_abort_load; return false; } @@ -309,18 +320,18 @@ bool Plugin::load(color_ostream &con) { if (strcmp(dfhack_git_desc, plug_git_desc) != 0) { - std::string msg = stl_sprintf("Warning: Plugin %s compiled for DFHack %s, running DFHack %s\n", + std::string msg = fmt::format("Warning: Plugin {} compiled for DFHack {}, running DFHack {}\n", *plug_name, plug_git_desc, dfhack_git_desc); - con << msg << flush; - cerr << msg << flush; + con << msg << std::flush; + std::cerr << msg << std::flush; } } else - con.printerr("Warning: Plugin %s missing git information\n", *plug_name); + con.printerr("Warning: Plugin {} missing git information\n", *plug_name); bool *plug_dev = (bool*)LookupPlugin(plug, "plugin_dev"); if (plug_dev && *plug_dev && getenv("DFHACK_NO_DEV_PLUGINS")) { - con.print("Skipping dev plugin: %s\n", *plug_name); + con.print("Skipping dev plugin: {}\n", *plug_name); plugin_abort_load; return false; } @@ -337,7 +348,7 @@ bool Plugin::load(color_ostream &con) } if (missing_globals.size()) { - con.printerr("Plugin %s is missing required globals: %s\n", + con.printerr("Plugin {} is missing required globals: {}\n", *plug_name, join_strings(", ", missing_globals).c_str()); plugin_abort_load; return false; @@ -363,18 +374,18 @@ bool Plugin::load(color_ostream &con) state = PS_LOADED; parent->registerCommands(this); if ((plugin_onupdate || plugin_enable) && !plugin_is_enabled) - con.printerr("Plugin %s has no enabled var!\n", name.c_str()); + con.printerr("Plugin {} has no enabled var!\n", name); if (Core::getInstance().isWorldLoaded() && plugin_load_world_data && plugin_load_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to load saved world data.\n", name); if (Core::getInstance().isMapLoaded() && plugin_load_site_data && World::IsSiteLoaded() && plugin_load_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to load saved site data.\n", name.c_str()); - fprintf(stderr, "loaded plugin %s; DFHack build %s\n", name.c_str(), plug_git_desc); - fflush(stderr); + con.printerr("Plugin {} has failed to load saved site data.\n", name); + + fmt::print(stderr, "loaded plugin {}; DFHack build {}\n", name, plug_git_desc); return true; } else { - con.printerr("Plugin %s has failed to initialize properly.\n", name.c_str()); + con.printerr("Plugin {} has failed to initialize properly.\n", name); plugin_is_enabled = 0; plugin_onupdate = 0; reset_lua(); @@ -392,7 +403,7 @@ bool Plugin::unload(color_ostream &con) { if (Screen::hasActiveScreens(this)) { - con.printerr("Cannot unload plugin %s: has active viewscreens\n", name.c_str()); + con.printerr("Cannot unload plugin {}: has active viewscreens\n", name); access->unlock(); return false; } @@ -401,7 +412,7 @@ bool Plugin::unload(color_ostream &con) if (plugin_onstatechange && plugin_onstatechange(con, SC_BEGIN_UNLOAD) != CR_OK) { - con.printerr("Plugin %s has refused to be unloaded.\n", name.c_str()); + con.printerr("Plugin {} has refused to be unloaded.\n", name); access->unlock(); return false; } @@ -417,9 +428,9 @@ bool Plugin::unload(color_ostream &con) CoreSuspender suspend; access->lock(); if (Core::getInstance().isMapLoaded() && plugin_save_site_data && World::IsSiteLoaded() && plugin_save_site_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save site data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save site data.\n", name); if (Core::getInstance().isWorldLoaded() && plugin_save_world_data && plugin_save_world_data(con) != CR_OK) - con.printerr("Plugin %s has failed to save world data.\n", name.c_str()); + con.printerr("Plugin {} has failed to save world data.\n", name); // holding the access lock while releasing the CoreSuspender creates a deadlock risk access->unlock(); } @@ -447,7 +458,7 @@ bool Plugin::unload(color_ostream &con) } else { - con.printerr("Plugin %s has failed to shutdown!\n",name.c_str()); + con.printerr("Plugin {} has failed to shutdown!\n",name); state = PS_BROKEN; } access->unlock(); @@ -459,7 +470,7 @@ bool Plugin::unload(color_ostream &con) return true; } else if (state == PS_BROKEN) - con.printerr("Plugin %s is broken - cannot be unloaded\n", name.c_str()); + con.printerr("Plugin {} is broken - cannot be unloaded\n", name); access->unlock(); return false; } @@ -483,25 +494,38 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s if (auto cmdIt = std::ranges::find_if(commands, [&](const PluginCommand &cmd) { return cmd.name == command; }); commands.end() != cmdIt) { + // an uncaught exception from a plugin command would propagate + // through runCommand and terminate the game; report it instead + auto call = [&]() -> command_result { + try { + return cmdIt->function(out, parameters); + } catch (const std::exception &e) { + out.printerr("Exception in {} command '{}': {}\n", name, command, e.what()); + return CR_FAILURE; + } catch (...) { + out.printerr("Exception in {} command '{}'\n", name, command); + return CR_FAILURE; + } + }; // running interactive things from some other source than the console would break it if (!out.is_console() && cmdIt->interactive) cr = CR_NEEDS_CONSOLE; else if (cmdIt->guard) { CoreSuspender suspend; if (!cmdIt->guard(Core::getInstance().getTopViewscreen())) { - out.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str()); + out.printerr("Could not invoke {}: unsuitable UI state.\n", command); cr = CR_WRONG_USAGE; } else { - cr = cmdIt->function(out, parameters); + cr = call(); } } else if (cmdIt->unlocked) { - cr = cmdIt->function(out, parameters); + cr = call(); } else { CoreSuspender suspend; - cr = cmdIt->function(out, parameters); + cr = call(); } } } @@ -771,6 +795,7 @@ int Plugin::lua_cmd_wrapper(lua_State *state) int Plugin::lua_fun_wrapper(lua_State *state) { + using DFHack::LuaWrapper::UPVAL_CONTAINER_ID; auto cmd = (LuaFunction*)lua_touserdata(state, UPVAL_CONTAINER_ID); RefAutoinc lock(cmd->owner->access); @@ -869,7 +894,7 @@ void PluginManager::init() loadAll(); bool any_loaded = false; - for (auto p : all_plugins) + for (auto& p : all_plugins) { if (p.second->getState() == Plugin::PS_LOADED) { @@ -892,13 +917,13 @@ bool PluginManager::addPlugin(string name) { if (all_plugins.find(name) != all_plugins.end()) { - Core::printerr("Plugin already exists: %s\n", name.c_str()); + Core::printerr("Plugin already exists: {}\n", name); return false; } - string path = getPluginPath(name); + std::filesystem::path path = getPluginPath(name); if (!Filesystem::isfile(path)) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } Plugin * p = new Plugin(core, path, name, this); @@ -906,16 +931,17 @@ bool PluginManager::addPlugin(string name) return true; } -vector PluginManager::listPlugins() +std::vector PluginManager::listPlugins() { - vector results; - vector files; + std::vector results; + std::vector files; Filesystem::listdir(getPluginPath(), files); for (auto file = files.begin(); file != files.end(); ++file) { - if (hasEnding(*file, plugin_suffix)) + string fname = file->filename().string(); + if (hasEnding(file->filename().string(), plugin_suffix)) { - string shortname = file->substr(0, file->find(plugin_suffix)); + string shortname = fname.substr(0, fname.find(plugin_suffix)); results.push_back(shortname); } } @@ -924,7 +950,7 @@ vector PluginManager::listPlugins() void PluginManager::refresh() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); for (auto f = files.begin(); f != files.end(); ++f) { @@ -935,13 +961,13 @@ void PluginManager::refresh() bool PluginManager::load (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name] && !addPlugin(name)) return false; Plugin *p = (*this)[name]; if (!p) { - Core::printerr("Plugin failed to register: %s\n", name.c_str()); + Core::printerr("Plugin failed to register: {}\n", name); return false; } return p->load(core->getConsole()); @@ -949,7 +975,7 @@ bool PluginManager::load (const string &name) bool PluginManager::loadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; auto files = listPlugins(); bool ok = true; // load all plugins in hack/plugins @@ -963,10 +989,10 @@ bool PluginManager::loadAll() bool PluginManager::unload (const string &name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) { - Core::printerr("Plugin does not exist: %s\n", name.c_str()); + Core::printerr("Plugin does not exist: {}\n", name); return false; } return (*this)[name]->unload(core->getConsole()); @@ -974,7 +1000,7 @@ bool PluginManager::unload (const string &name) bool PluginManager::unloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; // only try to unload plugins that are in all_plugins for (auto it = begin(); it != end(); ++it) @@ -989,7 +1015,7 @@ bool PluginManager::reload (const string &name) { // equivalent to "unload(name); load(name);" if plugin is recognized, // "load(name);" otherwise - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (!(*this)[name]) return load(name); if (!unload(name)) @@ -999,7 +1025,7 @@ bool PluginManager::reload (const string &name) bool PluginManager::reloadAll() { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; bool ok = true; if (!unloadAll()) ok = false; @@ -1010,8 +1036,8 @@ bool PluginManager::reloadAll() Plugin *PluginManager::getPluginByCommand(const std::string &command) { - lock_guard lock{*cmdlist_mutex}; - map ::iterator iter = command_map.find(command); + std::lock_guard lock{*cmdlist_mutex}; + std::map ::iterator iter = command_map.find(command); if (iter != command_map.end()) return iter->second; else @@ -1059,15 +1085,15 @@ void PluginManager::OnStateChange(color_ostream &out, state_change_event event) void PluginManager::registerCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for (size_t i = 0; i < cmds.size();i++) { std::string name = cmds[i].name; if (command_map.find(name) != command_map.end()) { - core->printerr("Plugin %s re-implements command \"%s\" (from plugin %s)\n", - p->getName().c_str(), name.c_str(), command_map[name]->getName().c_str()); + core->printerr("Plugin {} re-implements command \"{}\" (from plugin {})\n", + p->getName(), name, command_map[name]->getName()); continue; } command_map[name] = p; @@ -1076,8 +1102,8 @@ void PluginManager::registerCommands( Plugin * p ) void PluginManager::unregisterCommands( Plugin * p ) { - lock_guard lock{*cmdlist_mutex}; - vector & cmds = p->commands; + std::lock_guard lock{*cmdlist_mutex}; + std::vector & cmds = p->commands; for(size_t i = 0; i < cmds.size();i++) { command_map.erase(cmds[i].name); @@ -1093,12 +1119,12 @@ void PluginManager::doSaveData(color_ostream &out) if (World::IsSiteLoaded()) { cr = it->second->save_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save site data.\n", it->first); } cr = it->second->save_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to save world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to save world data.\n", it->first); } } @@ -1109,7 +1135,7 @@ void PluginManager::doLoadWorldData(color_ostream &out) command_result cr = it->second->load_world_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved world data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved world data.\n", it->first); } } @@ -1120,13 +1146,13 @@ void PluginManager::doLoadSiteData(color_ostream &out) command_result cr = it->second->load_site_data(out); if (cr != CR_OK && cr != CR_NOT_IMPLEMENTED) - out.printerr("Plugin %s has failed to load saved site data.\n", it->first.c_str()); + out.printerr("Plugin {} has failed to load saved site data.\n", it->first); } } Plugin *PluginManager::operator[] (std::string name) { - lock_guard lock{*plugin_mutex}; + std::lock_guard lock{*plugin_mutex}; if (all_plugins.find(name) == all_plugins.end()) { if (Filesystem::isfile(getPluginPath(name))) diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp deleted file mode 100644 index f8c0833a78..0000000000 --- a/library/Process-darwin.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "md5wrapper.h" -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(real_path, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << real_path << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char * typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char * typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -const char * -inheritance_strings[] = { - "SHARE", "COPY", "NONE", "DONATE_COPY", -}; - -const char * -behavior_strings[] = { - "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", -}; - -void Process::getMemRanges( vector & ranges ) -{ - static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); - - kern_return_t kr; - task_t the_task; - - the_task = mach_task_self(); - -#ifdef DFHACK64 - mach_vm_size_t vmsize; - mach_vm_address_t address; - vm_region_basic_info_data_64_t info; -#else - vm_size_t vmsize; - vm_address_t address; - vm_region_basic_info_data_t info; -#endif - mach_msg_type_number_t info_count; - vm_region_flavor_t flavor; - memory_object_name_t object; - - kr = KERN_SUCCESS; - address = 0; - - string cur_name; - void * cur_base = nullptr; - - do { -#ifdef DFHACK64 - flavor = VM_REGION_BASIC_INFO_64; - info_count = VM_REGION_BASIC_INFO_COUNT_64; - kr = mach_vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_64_t)&info, &info_count, &object); -#else - flavor = VM_REGION_BASIC_INFO; - info_count = VM_REGION_BASIC_INFO_COUNT; - kr = vm_region(the_task, &address, &vmsize, flavor, - (vm_region_info_t)&info, &info_count, &object); -#endif - - if (kr == KERN_SUCCESS) { - if (info.reserved==1) { - address += vmsize; - continue; - } - Dl_info dlinfo; - int dlcheck; - dlcheck = dladdr((const void*)address, &dlinfo); - if (dlcheck==0) { - dlinfo.dli_fname = ""; - } - - t_memrange temp; - strncpy( temp.name, dlinfo.dli_fname, 1023 ); - temp.name[1023] = 0; - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) address; - } - temp.base = cur_base; - temp.start = (void *) address; - temp.end = (void *) (address+vmsize); - temp.read = (info.protection & VM_PROT_READ); - temp.write = (info.protection & VM_PROT_WRITE); - temp.execute = (info.protection & VM_PROT_EXECUTE); - temp.shared = info.shared; - temp.valid = true; - ranges.push_back(temp); - - if (log_ranges) - { - fprintf(stderr, - "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", - (void*)address, - (void*)(address + vmsize), - size_t(vmsize >> 10), - (info.protection & VM_PROT_READ) ? 'r' : '-', - (info.protection & VM_PROT_WRITE) ? 'w' : '-', - (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', - (info.max_protection & VM_PROT_READ) ? 'r' : '-', - (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', - (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', - inheritance_strings[info.inheritance], - (info.shared) ? "shared" : "-", - behavior_strings[info.behavior], - info.user_wired_count, - info.reserved, - dlinfo.dli_fname); - } - - address += vmsize; - } else if (kr != KERN_INVALID_ADDRESS) { - - /*if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ - return; - } - } while (kr != KERN_INVALID_ADDRESS); - - -/* if (the_task != MACH_PORT_NULL) { - mach_port_deallocate(mach_task_self(), the_task); - }*/ -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -string Process::getPath() -{ - static string cached_path = ""; - if (cached_path.size()) - return cached_path; - char path[1024]; - char *real_path; - uint32_t size = sizeof(path); - if (getcwd(path, size)) - { - cached_path = string(path); - return cached_path; - } - if (_NSGetExecutablePath(path, &size) == 0) { - real_path = realpath(path, NULL); - } - else { - fprintf(stderr, "_NSGetExecutablePath failed!\n"); - cached_path = "."; - return cached_path; - } - std::string path_string(real_path); - int last_slash = path_string.find_last_of("/"); - cached_path = path_string.substr(0,last_slash); - return cached_path; -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp deleted file mode 100644 index 85e83f54f6..0000000000 --- a/library/Process-linux.cpp +++ /dev/null @@ -1,243 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" -#include "modules/Filesystem.h" -#include "md5wrapper.h" - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - -Process::Process(const VersionInfoFactory& known_versions) : identified(false), my_pe(0) -{ - const char * exe_link_name = "/proc/self/exe"; - - // valgrind replaces readlink for /proc/self/exe, but not open. - char self_exe[1024]; - memset(self_exe, 0, sizeof(self_exe)); - std::string self_exe_name; - if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) - self_exe_name = exe_link_name; - else - self_exe_name = self_exe; - - md5wrapper md5; - uint32_t length; - uint8_t first_kb [1024]; - memset(first_kb, 0, sizeof(first_kb)); - // get hash of the running DF process - my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); - // create linux process, add it to the vector - auto vinfo = known_versions.getVersionInfoByMD5(my_md5); - if(vinfo) - { - identified = true; - my_descriptor = std::make_shared(*vinfo); - } - else - { - char * wd = getcwd(NULL, 0); - cerr << "Unable to retrieve version information.\n"; - cerr << "File: " << exe_link_name << endl; - cerr << "MD5: " << my_md5 << endl; - cerr << "working dir: " << wd << endl; - cerr << "length:" << length << endl; - cerr << "1KB hexdump follows:" << endl; - for(int i = 0; i < 64; i++) - { - fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - first_kb[i*16], - first_kb[i*16+1], - first_kb[i*16+2], - first_kb[i*16+3], - first_kb[i*16+4], - first_kb[i*16+5], - first_kb[i*16+6], - first_kb[i*16+7], - first_kb[i*16+8], - first_kb[i*16+9], - first_kb[i*16+10], - first_kb[i*16+11], - first_kb[i*16+12], - first_kb[i*16+13], - first_kb[i*16+14], - first_kb[i*16+15] - ); - } - free(wd); - } -} - -Process::~Process() -{ - // Nothing to do here -} - -string Process::doReadClassName (void * vptr) -{ - char* typeinfo = Process::readPtr(((char *)vptr - sizeof(void*))); - char* typestring = Process::readPtr(typeinfo + sizeof(void*)); - string raw = readCString(typestring); - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -//FIXME: cross-reference with ELF segment entries? -void Process::getMemRanges( vector & ranges ) -{ - char buffer[1024]; - char permissions[5]; // r/-, w/-, x/-, p/s, 0 - - FILE *mapFile = ::fopen("/proc/self/maps", "r"); - if (!mapFile) - return; - - size_t start, end, offset, device1, device2, node; - - string cur_name; - void * cur_base = nullptr; - - while (fgets(buffer, 1024, mapFile)) - { - t_memrange temp; - temp.name[0] = 0; - sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", - &start, - &end, - (char*)&permissions, - &offset, &device1, &device2, &node, - (char*)temp.name); - if (cur_name != temp.name) { - cur_name = temp.name; - cur_base = (void *) start; - } - temp.base = cur_base; - temp.start = (void *) start; - temp.end = (void *) end; - temp.read = permissions[0] == 'r'; - temp.write = permissions[1] == 'w'; - temp.execute = permissions[2] == 'x'; - temp.shared = permissions[3] == 's'; - temp.valid = true; - ranges.push_back(temp); - } - - fclose(mapFile); -} - -uintptr_t Process::getBase() -{ - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool /*to_file*/) -{ - return offset; -} - -uint32_t Process::getTickCount() -{ - struct timeval tp; - gettimeofday(&tp, NULL); - return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); -} - -string Process::getPath() -{ - return Filesystem::get_initial_cwd(); -} - -int Process::getPID() -{ - return getpid(); -} - -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) -{ - int result; - int protect=0; - if(trgrange.read)protect|=PROT_READ; - if(trgrange.write)protect|=PROT_WRITE; - if(trgrange.execute)protect|=PROT_EXEC; - result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); - - return result==0; -} - -// returns -1 on error -void* Process::memAlloc(const int length) -{ - return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); -} - -int Process::memDealloc(void *ptr, const int length) -{ - return munmap(ptr, length); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - - if (prot & Process::MemProt::READ) - prot_native |= PROT_READ; - if (prot & Process::MemProt::WRITE) - prot_native |= PROT_WRITE; - if (prot & Process::MemProt::EXEC) - prot_native |= PROT_EXEC; - - return mprotect(ptr, length, prot_native); -} diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp deleted file mode 100644 index 2450eb87bd..0000000000 --- a/library/Process-windows.cpp +++ /dev/null @@ -1,454 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include "Error.h" -#include "Internal.h" -#include "MemAccess.h" -#include "Memory.h" -#include "MiscUtils.h" -#include "VersionInfo.h" -#include "VersionInfoFactory.h" - -#define _WIN32_WINNT 0x0600 -#define WINVER 0x0600 - -#define WIN32_LEAN_AND_MEAN -#include -#include - -using namespace DFHack; - -using std::string; -using std::map; -using std::vector; -using std::endl; -using std::cerr; - - -namespace DFHack -{ - class PlatformSpecific - { - public: - PlatformSpecific() - { - base = 0; - sections = 0; - }; - HANDLE my_handle; - uint32_t my_pid; - IMAGE_NT_HEADERS pe_header; - IMAGE_SECTION_HEADER * sections; - char * base; - }; -} - -Process::Process(const VersionInfoFactory& factory) : identified(false) -{ - HMODULE hmod = NULL; - DWORD needed; - bool found = false; - - d = new PlatformSpecific(); - // open process - d->my_pid = GetCurrentProcessId(); - d->my_handle = GetCurrentProcess(); - // try getting the first module of the process - if(EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) - { - return; //if enumprocessModules fails, give up - } - - // got base ;) - d->base = (char *)hmod; - - // read from this process - try - { - uint32_t pe_offset = readDWord(d->base+0x3C); - read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t *)&(d->pe_header)); - const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; - d->sections = (IMAGE_SECTION_HEADER *) malloc(sectionsSize); - read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t *)(d->sections)); - } - catch (std::exception &) - { - return; - } - my_pe = d->pe_header.FileHeader.TimeDateStamp; - auto vinfo = factory.getVersionInfoByPETimestamp(my_pe); - if(vinfo) - { - identified = true; - // give the process a data model and memory layout fixed for the base of first module - my_descriptor = std::make_shared(*vinfo); - my_descriptor->rebaseTo(getBase()); - } - else - { - fprintf(stderr, "Unable to retrieve version information.\nPE timestamp: 0x%x\n", my_pe); - fflush(stderr); - } -} - -Process::~Process() -{ - // destroy our rebased copy of the memory descriptor - if(d->sections != NULL) - free(d->sections); -} - -string Process::doReadClassName (void * vptr) -{ - char* rtti = readPtr((char *)vptr - sizeof(void*)); -#ifdef DFHACK64 - void* base; - if (!RtlPcToFileHeader(rtti, &base)) - return "dummy"; - char* typeinfo = (char *)base + readDWord(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x10); -#else - char* typeinfo = readPtr(rtti + 0xC); - std::string raw = readCString(typeinfo + 0x8); -#endif - if (raw.length() == 0) - return "dummy"; - - string status; - string demangled = cxx_demangle(raw, &status); - - if (demangled.length() == 0) { - return "dummy"; - } - - return demangled; -} - -/* -typedef struct _MEMORY_BASIC_INFORMATION -{ - void * BaseAddress; - void * AllocationBase; - uint32_t AllocationProtect; - size_t RegionSize; - uint32_t State; - uint32_t Protect; - uint32_t Type; -} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; -*/ -/* -//Internal structure used to store heap block information. -struct HeapBlock -{ - PVOID dwAddress; - DWORD dwSize; - DWORD dwFlags; - ULONG reserved; -}; -*/ - -static void GetDosNames(std::map &table) -{ - // Partially based on example from msdn: - // Translate path with device name to drive letters. - TCHAR szTemp[512]; - szTemp[0] = '\0'; - - if (GetLogicalDriveStrings(sizeof(szTemp)-1, szTemp)) - { - TCHAR szName[MAX_PATH]; - TCHAR szDrive[3] = " :"; - BOOL bFound = FALSE; - TCHAR* p = szTemp; - - do - { - // Copy the drive letter to the template string - *szDrive = *p; - - // Look up each device name - if (QueryDosDevice(szDrive, szName, MAX_PATH)) - table[szName] = szDrive; - - // Go to the next NULL character. - while (*p++); - } while (*p); // end of string - } -} - -void Process::getMemRanges( vector & ranges ) -{ - MEMORY_BASIC_INFORMATION MBI; - //map heaps; - uint64_t movingStart = 0; - PVOID LastAllocationBase = 0; - map nameMap; - map dosDrives; - - // get page size - SYSTEM_INFO si; - GetSystemInfo(&si); - uint64_t PageSize = si.dwPageSize; - - // get dos drive names - GetDosNames(dosDrives); - - ranges.clear(); - - HANDLE my_handle = GetCurrentProcess(); - - // enumerate heaps - // HeapNodes(d->my_pid, heaps); - // go through all the VM regions, convert them to our internal format - while (VirtualQueryEx(my_handle, (const void*) (movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) - { - movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); - if(movingStart % PageSize != 0) - movingStart = (movingStart / PageSize + 1) * PageSize; - - // Skip unallocated address space - if (MBI.State & MEM_FREE) - continue; - - // Find range and permissions - t_memrange temp; - memset(&temp, 0, sizeof(temp)); - - temp.start = (char *) MBI.BaseAddress; - temp.end = ((char *)MBI.BaseAddress + (uint64_t)MBI.RegionSize); - temp.valid = true; - - if (!(MBI.State & MEM_COMMIT)) - temp.valid = false; // reserved address space - else if (MBI.Protect & PAGE_EXECUTE) - temp.execute = true; - else if (MBI.Protect & PAGE_EXECUTE_READ) - temp.execute = temp.read = true; - else if (MBI.Protect & PAGE_EXECUTE_READWRITE) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) - temp.execute = temp.read = temp.write = true; - else if (MBI.Protect & PAGE_READONLY) - temp.read = true; - else if (MBI.Protect & PAGE_READWRITE) - temp.read = temp.write = true; - else if (MBI.Protect & PAGE_WRITECOPY) - temp.read = temp.write = true; - - // Merge areas with the same properties - if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) - { - auto &last = ranges.back(); - - if (last.end == temp.start && - last.valid == temp.valid && last.execute == temp.execute && - last.read == temp.read && last.write == temp.write) - { - last.end = temp.end; - continue; - } - } - -#if 1 - // Find the mapped file name - if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) - { - int vsize = strlen(temp.name); - - // Translate NT name to DOS name - for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) - { - int ksize = it->first.size(); - if (strncmp(temp.name, it->first.data(), ksize) != 0) - continue; - - memcpy(temp.name, it->second.data(), it->second.size()); - memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); - break; - } - } - else - temp.name[0] = 0; -#else - // Find the executable name - char *base = (char*)MBI.AllocationBase; - - if(nameMap.count(base)) - { - strncpy(temp.name, nameMap[base].c_str(), 1023); - } - else if(GetModuleBaseName(d->my_handle, (HMODULE)base, temp.name, 1024)) - { - std::string nm(temp.name); - - nameMap[base] = nm; - - // this is our executable! (could be generalized to pull segments from libs, but whatever) - if(d->base == base) - { - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - /*char sectionName[9]; - memcpy(sectionName,d->sections[i].Name,8); - sectionName[8] = 0; - string nm; - nm.append(temp.name); - nm.append(" : "); - nm.append(sectionName);*/ - nameMap[base + d->sections[i].VirtualAddress] = nm; - } - } - } - else - temp.name[0] = 0; -#endif - - // Push the entry - LastAllocationBase = MBI.AllocationBase; - ranges.push_back(temp); - } -} - -uintptr_t Process::getBase() -{ - if(d) - return (uintptr_t) d->base; - return DEFAULT_BASE_ADDR; // Memory.h -} - -int Process::adjustOffset(int offset, bool to_file) -{ - if (!d) - return -1; - - for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) - { - auto §ion = d->sections[i]; - - if (to_file) - { - unsigned delta = offset - section.VirtualAddress; - if (delta >= section.Misc.VirtualSize) - continue; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - return -1; - return (int)(section.PointerToRawData + delta); - } - else - { - unsigned delta = offset - section.PointerToRawData; - if (!section.PointerToRawData || delta >= section.SizeOfRawData) - continue; - if (delta >= section.Misc.VirtualSize) - return -1; - return (int)(section.VirtualAddress + delta); - } - } - - return -1; -} - -uint32_t Process::getTickCount() -{ - return GetTickCount(); -} - -string Process::getPath() -{ - HMODULE hmod; - DWORD junk; - char String[255]; - EnumProcessModules(d->my_handle, &hmod, 1 * sizeof(HMODULE), &junk); //get the module from the handle - GetModuleFileNameEx(d->my_handle,hmod,String,sizeof(String)); //get the filename from the module - string out(String); - return(out.substr(0,out.find_last_of("\\"))); -} - -int Process::getPID() -{ - return (int) GetCurrentProcessId(); -} - - -bool Process::setPermisions(const t_memrange & range,const t_memrange &trgrange) -{ - DWORD newprotect=0; - if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; - if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; - if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; - if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; - if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; - DWORD oldprotect=0; - bool result; - result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); - - return result; -} - -void* Process::memAlloc(const int length) -{ - void *ret; - // returns 0 on error - ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); - if (!ret) - ret = (void*)-1; - return ret; -} - -int Process::memDealloc(void *ptr, const int length) -{ - // can only free the whole region at once - // vfree returns 0 on error - return !VirtualFree(ptr, 0, MEM_RELEASE); -} - -int Process::memProtect(void *ptr, const int length, const int prot) -{ - int prot_native = 0; - DWORD old_prot = 0; - - // only support a few constant combinations - if (prot == 0) - prot_native = PAGE_NOACCESS; - else if (prot == Process::MemProt::READ) - prot_native = PAGE_READONLY; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) - prot_native = PAGE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READWRITE; - else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) - prot_native = PAGE_EXECUTE_READ; - else - return -1; - - return !VirtualProtect(ptr, length, prot_native, &old_prot); -} diff --git a/library/Process.cpp b/library/Process.cpp new file mode 100644 index 0000000000..1ff63622f0 --- /dev/null +++ b/library/Process.cpp @@ -0,0 +1,829 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#include "Format.h" +#include "MemAccess.h" +#include "Memory.h" +#include "MemoryPatcher.h" +#include "MiscUtils.h" +#include "VersionInfo.h" +#include "VersionInfoFactory.h" + +#include "modules/Filesystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef LINUX_BUILD +#include +#include +#include +#include +#include + +#ifdef _DARWIN +#include +#include +#include +#include +#include +#include +#include +#endif /* _DARWIN */ + +#include "md5wrapper.h" +#endif /* LINUX_BUILD */ + +#ifdef WIN32 +#define _WIN32_WINNT 0x0600 +#define WINVER 0x0600 + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#include + +#endif /* WIN32 */ + +using namespace DFHack; + +using std::string; +using std::map; +using std::vector; +using std::endl; +using std::cerr; + +#ifdef WIN32 +namespace DFHack +{ + class PlatformSpecific + { + public: + PlatformSpecific() + { + base = 0; + sections = 0; + }; + HANDLE my_handle; + uint32_t my_pid; + IMAGE_NT_HEADERS pe_header; + IMAGE_SECTION_HEADER * sections; + char * base; + }; +} + +#endif /* WIN32 */ +Process::Process(const VersionInfoFactory& known_versions) : identified(false) +{ +#ifndef WIN32 + std::string self_exe_name; +#ifndef _DARWIN + const char * exe_link_name = "/proc/self/exe"; + + // valgrind replaces readlink for /proc/self/exe, but not open. + char self_exe[1024]; + memset(self_exe, 0, sizeof(self_exe)); + if (readlink(exe_link_name, self_exe, sizeof(self_exe) - 1) < 0) + self_exe_name = exe_link_name; + else + self_exe_name = self_exe; +#else /* _DARWIN */ + char path[1024]; + char *exec_link_name; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + exec_link_name = realpath(path, NULL); + self_exe_name = std::string(exec_link_name); + } +#endif /* _DARWIN */ +#else /* WIN32 */ + HMODULE hmod = NULL; + DWORD needed; + bool found = false; + + d = new PlatformSpecific(); + // open process + d->my_pid = GetCurrentProcessId(); + d->my_handle = GetCurrentProcess(); + // try getting the first module of the process + if (EnumProcessModules(d->my_handle, &hmod, sizeof(hmod), &needed) == 0) + { + return; //if enumprocessModules fails, give up + } + // got base ;) + d->base = (char*)hmod; + + // read from this process + try + { + uint32_t pe_offset = readDWord(d->base + 0x3C); + read(d->base + pe_offset, sizeof(d->pe_header), (uint8_t*)&(d->pe_header)); + const size_t sectionsSize = sizeof(IMAGE_SECTION_HEADER) * d->pe_header.FileHeader.NumberOfSections; + d->sections = (IMAGE_SECTION_HEADER*)std::malloc(sectionsSize); + read(d->base + pe_offset + sizeof(d->pe_header), sectionsSize, (uint8_t*)(d->sections)); + } + catch (std::exception&) + { + return; + } +#endif /* WIN32 */ + +#ifndef WIN32 + my_pe = 0; + md5wrapper md5; + uint32_t length; + uint8_t first_kb [1024]; + memset(first_kb, 0, sizeof(first_kb)); + // get hash of the running DF process + my_md5 = md5.getHashFromFile(self_exe_name, length, (char *) first_kb); + // create linux process, add it to the vector + auto vinfo = known_versions.getVersionInfoByMD5(my_md5); +#else /* WIN32 */ + my_pe = d->pe_header.FileHeader.TimeDateStamp; + auto vinfo = known_versions.getVersionInfoByPETimestamp(my_pe); +#endif /* WIN32 */ + if(vinfo) + { + identified = true; + my_descriptor = std::make_shared(*vinfo); +#ifdef WIN32 + // give the process a data model and memory layout fixed for the base of first module + my_descriptor->rebaseTo(getBase()); +#endif /* WIN32 */ + } + else + { +#ifndef WIN32 + char * wd = getcwd(NULL, 0); +#endif /* ! WIN32 */ + cerr << "Unable to retrieve version information.\n"; +#ifndef WIN32 + cerr << "File: " << exe_link_name << endl; + cerr << "MD5: " << my_md5 << endl; + cerr << "working dir: " << wd << endl; + cerr << "length:" << length << endl; + cerr << "1KB hexdump follows:" << endl; + for(int i = 0; i < 64; i++) + { + fmt::print(std::cerr, "{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}\n", + first_kb[i*16], + first_kb[i*16+1], + first_kb[i*16+2], + first_kb[i*16+3], + first_kb[i*16+4], + first_kb[i*16+5], + first_kb[i*16+6], + first_kb[i*16+7], + first_kb[i*16+8], + first_kb[i*16+9], + first_kb[i*16+10], + first_kb[i*16+11], + first_kb[i*16+12], + first_kb[i*16+13], + first_kb[i*16+14], + first_kb[i*16+15] + ); + } + free(wd); +#else /* WIN32 */ + cerr << "PE timestamp: " << fmt::format("{:#0x}", my_pe) << endl; +#endif /* WIN32 */ + } +} + +Process::~Process() +{ +#ifndef WIN32 + // Nothing to do here +#else /* WIN32 */ + // destroy our rebased copy of the memory descriptor + if(d->sections != NULL) + free(d->sections); +#endif /* WIN32 */ +} + +string Process::doReadClassName (void * vptr) +{ + if (!checkValidAddress(vptr)) + throw std::runtime_error(fmt::format("invalid vtable ptr {}", vptr)); + + char* rtti = Process::readPtr(((char*)vptr - sizeof(void*))); +#ifndef WIN32 + char* typestring = Process::readPtr(rtti + sizeof(void*)); +#else /* WIN32 */ +#ifdef DFHACK64 + void* base; + if (!RtlPcToFileHeader(rtti, &base)) + return "dummy"; + char* typeinfo = (char *)base + readDWord(rtti + 0xC); + char* typestring = typeinfo + 0x10; +#else + char* typeinfo = readPtr(rtti + 0xC); + char* typestring = typeinfo + 0x8; +#endif +#endif /* WIN32 */ + std::string raw = readCString(typestring); + if (raw.length() == 0) + return "dummy"; + + string status; + string demangled = cxx_demangle(raw, &status); + + if (demangled.length() == 0) { + return "dummy"; + } + + return demangled; +} + +#ifndef WIN32 +#ifndef _DARWIN +//FIXME: cross-reference with ELF segment entries? +#else /* _DARWIN */ +const char* +inheritance_strings[] = { + "SHARE", "COPY", "NONE", "DONATE_COPY", +}; + +const char* +behavior_strings[] = { + "DEFAULT", "RANDOM", "SEQUENTIAL", "RESQNTL", "WILLNEED", "DONTNEED", +}; + +#endif /* _DARWIN */ +void Process::getMemRanges(vector& ranges) +{ +#ifndef _DARWIN + char buffer[1024]; + char permissions[5]; // r/-, w/-, x/-, p/s, 0 +#else /* _DARWIN */ + static bool log_ranges = (getenv("DFHACK_LOG_MEM_RANGES") != NULL); + + kern_return_t kr; + task_t the_task; +#endif /* _DARWIN */ + +#ifndef _DARWIN + FILE* mapFile = ::fopen("/proc/self/maps", "r"); + if (!mapFile) + return; +#else /* _DARWIN */ + the_task = mach_task_self(); +#endif /* _DARWIN */ + +#ifndef _DARWIN + size_t start, end, offset, device1, device2, node; +#else /* _DARWIN */ +#ifdef DFHACK64 + mach_vm_size_t vmsize; + mach_vm_address_t address; + vm_region_basic_info_data_64_t info; +#else + vm_size_t vmsize; + vm_address_t address; + vm_region_basic_info_data_t info; +#endif + mach_msg_type_number_t info_count; + vm_region_flavor_t flavor; + memory_object_name_t object; + + kr = KERN_SUCCESS; + address = 0; +#endif /* _DARWIN */ + + string cur_name; + void* cur_base = nullptr; + +#ifndef _DARWIN + while (fgets(buffer, 1024, mapFile)) + { +#else /* _DARWIN */ + do { +#ifdef DFHACK64 + flavor = VM_REGION_BASIC_INFO_64; + info_count = VM_REGION_BASIC_INFO_COUNT_64; + kr = mach_vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_64_t)&info, &info_count, &object); +#else + flavor = VM_REGION_BASIC_INFO; + info_count = VM_REGION_BASIC_INFO_COUNT; + kr = vm_region(the_task, &address, &vmsize, flavor, + (vm_region_info_t)&info, &info_count, &object); +#endif + + if (kr == KERN_INVALID_ADDRESS) + break; + if (kr != KERN_SUCCESS) + { + + /*if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ + return; + } + if (info.reserved == 1) { + address += vmsize; + continue; + } + Dl_info dlinfo; + int dlcheck; + dlcheck = dladdr((const void*)address, &dlinfo); + if (dlcheck == 0) { + dlinfo.dli_fname = ""; + } + +#endif /* _DARWIN */ + t_memrange temp; +#ifndef _DARWIN + temp.name[0] = 0; + sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", + &start, + &end, + (char*)&permissions, + &offset, &device1, &device2, &node, + (char*)temp.name); +#else /* _DARWIN */ + strncpy(temp.name, dlinfo.dli_fname, 1023); + temp.name[1023] = 0; +#endif /* _DARWIN */ + if (cur_name != temp.name) { + cur_name = temp.name; +#ifndef _DARWIN + cur_base = (void*)start; +#else /* _DARWIN */ + cur_base = (void*)address; +#endif /* _DARWIN */ + } + temp.base = cur_base; +#ifndef _DARWIN + temp.start = (void*)start; + temp.end = (void*)end; + temp.read = permissions[0] == 'r'; + temp.write = permissions[1] == 'w'; + temp.execute = permissions[2] == 'x'; + temp.shared = permissions[3] == 's'; +#else /* _DARWIN */ + temp.start = (void*)address; + temp.end = (void*)(address + vmsize); + temp.read = (info.protection & VM_PROT_READ); + temp.write = (info.protection & VM_PROT_WRITE); + temp.execute = (info.protection & VM_PROT_EXECUTE); + temp.shared = info.shared; +#endif /* _DARWIN */ + temp.valid = true; + ranges.push_back(temp); +#ifndef _DARWIN + } +#endif /* ! _DARWIN */ + +#ifndef _DARWIN + fclose(mapFile); +#else /* _DARWIN */ + if (log_ranges) + { + fprintf(stderr, + "%p-%p %8zuK %c%c%c/%c%c%c %11s %6s %10s uwir=%hu sub=%u dlname: %s\n", + (void*)address, + (void*)(address + vmsize), + size_t(vmsize >> 10), + (info.protection & VM_PROT_READ) ? 'r' : '-', + (info.protection & VM_PROT_WRITE) ? 'w' : '-', + (info.protection & VM_PROT_EXECUTE) ? 'x' : '-', + (info.max_protection & VM_PROT_READ) ? 'r' : '-', + (info.max_protection & VM_PROT_WRITE) ? 'w' : '-', + (info.max_protection & VM_PROT_EXECUTE) ? 'x' : '-', + inheritance_strings[info.inheritance], + (info.shared) ? "shared" : "-", + behavior_strings[info.behavior], + info.user_wired_count, + info.reserved, + dlinfo.dli_fname); + } + + address += vmsize; + } while (kr != KERN_INVALID_ADDRESS); + + +/* if (the_task != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), the_task); + }*/ +#endif /* _DARWIN */ +} +#else /* WIN32 */ +/* +typedef struct _MEMORY_BASIC_INFORMATION +{ + void * BaseAddress; + void * AllocationBase; + uint32_t AllocationProtect; + size_t RegionSize; + uint32_t State; + uint32_t Protect; + uint32_t Type; +} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; +*/ +/* +//Internal structure used to store heap block information. +struct HeapBlock +{ + PVOID dwAddress; + DWORD dwSize; + DWORD dwFlags; + ULONG reserved; +}; +*/ + +static void GetDosNames(std::map& table) +{ + // Partially based on example from msdn: + // Translate path with device name to drive letters. + TCHAR szTemp[512]; + szTemp[0] = '\0'; + + if (GetLogicalDriveStrings(sizeof(szTemp) - 1, szTemp)) + { + TCHAR szName[MAX_PATH]; + TCHAR szDrive[3] = " :"; + BOOL bFound = FALSE; + TCHAR* p = szTemp; + + do + { + // Copy the drive letter to the template string + *szDrive = *p; + + // Look up each device name + if (QueryDosDevice(szDrive, szName, MAX_PATH)) + table[szName] = szDrive; + + // Go to the next NULL character. + while (*p++); + } while (*p); // end of string + } +} + +void Process::getMemRanges(vector& ranges) +{ + MEMORY_BASIC_INFORMATION MBI; + //map heaps; + uint64_t movingStart = 0; + PVOID LastAllocationBase = 0; + map nameMap; + map dosDrives; + + // get page size + SYSTEM_INFO si; + GetSystemInfo(&si); + uint64_t PageSize = si.dwPageSize; + + // get dos drive names + GetDosNames(dosDrives); + + ranges.clear(); + + HANDLE my_handle = GetCurrentProcess(); + + // enumerate heaps + // HeapNodes(d->my_pid, heaps); + // go through all the VM regions, convert them to our internal format + while (VirtualQueryEx(my_handle, (const void*)(movingStart), &MBI, sizeof(MBI)) == sizeof(MBI)) + { + t_memrange temp; + movingStart = ((uint64_t)MBI.BaseAddress + MBI.RegionSize); + if (movingStart % PageSize != 0) + movingStart = (movingStart / PageSize + 1) * PageSize; + + // Skip unallocated address space + if (MBI.State & MEM_FREE) + continue; + + // Find range and permissions + memset(&temp, 0, sizeof(temp)); + + temp.start = (char*)MBI.BaseAddress; + temp.end = ((char*)MBI.BaseAddress + (uint64_t)MBI.RegionSize); + temp.valid = true; + + if (!(MBI.State & MEM_COMMIT)) + temp.valid = false; // reserved address space + else if (MBI.Protect & PAGE_EXECUTE) + temp.execute = true; + else if (MBI.Protect & PAGE_EXECUTE_READ) + temp.execute = temp.read = true; + else if (MBI.Protect & PAGE_EXECUTE_READWRITE) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_EXECUTE_WRITECOPY) + temp.execute = temp.read = temp.write = true; + else if (MBI.Protect & PAGE_READONLY) + temp.read = true; + else if (MBI.Protect & PAGE_READWRITE) + temp.read = temp.write = true; + else if (MBI.Protect & PAGE_WRITECOPY) + temp.read = temp.write = true; + + // Merge areas with the same properties + if (!ranges.empty() && LastAllocationBase == MBI.AllocationBase) + { + auto& last = ranges.back(); + + if (last.end == temp.start && + last.valid == temp.valid && last.execute == temp.execute && + last.read == temp.read && last.write == temp.write) + { + last.end = temp.end; + continue; + } + } + + // Find the mapped file name + if (GetMappedFileName(my_handle, temp.start, temp.name, 1024)) + { + int vsize = strlen(temp.name); + + // Translate NT name to DOS name + for (auto it = dosDrives.begin(); it != dosDrives.end(); ++it) + { + int ksize = it->first.size(); + if (strncmp(temp.name, it->first.data(), ksize) != 0) + continue; + + memcpy(temp.name, it->second.data(), it->second.size()); + memmove(temp.name + it->second.size(), temp.name + ksize, vsize + 1 - ksize); + break; + } + } + else + temp.name[0] = 0; + + // Push the entry + LastAllocationBase = MBI.AllocationBase; + ranges.push_back(temp); + } +} +#endif + +bool Process::checkValidAddress(void* ptr) +{ + uintptr_t addr = reinterpret_cast(ptr); + auto validate = [&] (t_memrange& r) { + uintptr_t lo = reinterpret_cast(r.start); + uintptr_t hi = reinterpret_cast(r.end); + return addr >= lo && addr < hi; + }; + std::vector mr; + getMemRanges(mr); + bool valid = std::any_of(mr.begin(), mr.end(), validate); + return valid; +} + +uintptr_t Process::getBase() +{ +#if WIN32 + if(d) + return (uintptr_t) d->base; +#endif /* WIN32 */ + return DEFAULT_BASE_ADDR; // Memory.h +} + +int Process::adjustOffset(int offset, [[maybe_unused]] bool to_file) +{ +#ifndef WIN32 + return offset; +#else /* WIN32 */ + if (!d) + return -1; + + for(int i = 0; i < d->pe_header.FileHeader.NumberOfSections; i++) + { + auto §ion = d->sections[i]; + + if (to_file) + { + unsigned delta = offset - section.VirtualAddress; + if (delta >= section.Misc.VirtualSize) + continue; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + return -1; + return (int)(section.PointerToRawData + delta); + } + else + { + unsigned delta = offset - section.PointerToRawData; + if (!section.PointerToRawData || delta >= section.SizeOfRawData) + continue; + if (delta >= section.Misc.VirtualSize) + return -1; + return (int)(section.VirtualAddress + delta); + } + } + + return -1; +#endif /* WIN32 */ +} + +uint32_t Process::getTickCount() +{ +#ifndef WIN32 + struct timeval tp; + gettimeofday(&tp, NULL); + return (tp.tv_sec * 1000) + (tp.tv_usec / 1000); +#else /* WIN32 */ + return GetTickCount(); +#endif /* WIN32 */ +} + +[[deprecated]] std::filesystem::path Process::getPath() +{ +#if defined(WIN32) || !defined(_DARWIN) + return Filesystem::get_initial_cwd(); +#else /* _DARWIN */ + static string cached_path = ""; + if (cached_path.size()) + return cached_path; + char path[1024]; + char *real_path; + uint32_t size = sizeof(path); + if (getcwd(path, size)) + { + cached_path = string(path); + return cached_path; + } + if (_NSGetExecutablePath(path, &size) == 0) { + real_path = realpath(path, NULL); + } + else { + fprintf(stderr, "_NSGetExecutablePath failed!\n"); + cached_path = "."; + return cached_path; + } + std::string path_string(real_path); + int last_slash = path_string.find_last_of("/"); + cached_path = path_string.substr(0,last_slash); + return cached_path; +#endif /* _DARWIN */ +} + +int Process::getPID() +{ +#ifndef WIN32 + return getpid(); +#else /* WIN32 */ + return (int) GetCurrentProcessId(); +#endif /* WIN32 */ +} + +#ifdef WIN32 + +#endif /* WIN32 */ +bool Process::setPermissions(const t_memrange & range,const t_memrange &trgrange) +{ +#ifndef WIN32 + int result; + int protect=0; + if(trgrange.read)protect|=PROT_READ; + if(trgrange.write)protect|=PROT_WRITE; + if(trgrange.execute)protect|=PROT_EXEC; + result=mprotect((void *)range.start, (size_t)range.end-(size_t)range.start,protect); +#else /* WIN32 */ + DWORD newprotect=0; + if(trgrange.read && !trgrange.write && !trgrange.execute)newprotect=PAGE_READONLY; + if(trgrange.read && trgrange.write && !trgrange.execute)newprotect=PAGE_READWRITE; + if(!trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE; + if(trgrange.read && !trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READ; + if(trgrange.read && trgrange.write && trgrange.execute)newprotect=PAGE_EXECUTE_READWRITE; + DWORD oldprotect=0; + bool result; + result=VirtualProtect((LPVOID)range.start,(char *)range.end-(char *)range.start,newprotect,&oldprotect); +#endif /* WIN32 */ + +#ifndef WIN32 + return result==0; +#else /* WIN32 */ + return result; +#endif /* WIN32 */ +} + +bool Process::flushCache(const void* target, size_t count) +{ +#ifndef WIN32 +#ifndef _DARWIN + __builtin___clear_cache((char*)target, (char*)target + count - 1); + return true; /* assume always succeeds, as the builtin has no return type */ +#else /* _DARWIN */ + // FIXME: implement cache flush for MacOS (???) + return false; +#endif /* _DARWIN */ +#else /* WIN32 */ + return 0 != FlushInstructionCache(d->my_handle, (LPCVOID)target, count); +#endif /* WIN32 */ +} + +#ifndef WIN32 +// returns -1 on error +#endif /* ! WIN32 */ +void* Process::memAlloc(const int length) +{ +#ifndef WIN32 +#ifndef _DARWIN + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); +#else /* _DARWIN */ + return mmap(0, length, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); +#endif /* _DARWIN */ +#else /* WIN32 */ + void *ret; + // returns 0 on error + ret = VirtualAlloc(0, length, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); + if (!ret) + ret = (void*)-1; + return ret; +#endif /* WIN32 */ +} + +int Process::memDealloc(void *ptr, const int length) +{ +#ifndef WIN32 + return munmap(ptr, length); +#else /* WIN32 */ + // can only free the whole region at once + // vfree returns 0 on error + return !VirtualFree(ptr, 0, MEM_RELEASE); +#endif /* WIN32 */ +} + +int Process::memProtect(void *ptr, const int length, const int prot) +{ + int prot_native = 0; + +#ifndef WIN32 + if (prot & Process::MemProt::READ) + prot_native |= PROT_READ; + if (prot & Process::MemProt::WRITE) + prot_native |= PROT_WRITE; + if (prot & Process::MemProt::EXEC) + prot_native |= PROT_EXEC; +#else /* WIN32 */ + // only support a few constant combinations + if (prot == 0) + prot_native = PAGE_NOACCESS; + else if (prot == Process::MemProt::READ) + prot_native = PAGE_READONLY; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE)) + prot_native = PAGE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::WRITE | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READWRITE; + else if (prot == (Process::MemProt::READ | Process::MemProt::EXEC)) + prot_native = PAGE_EXECUTE_READ; + else + return -1; +#endif /* WIN32 */ + +#ifndef WIN32 + return mprotect(ptr, length, prot_native); +#else /* WIN32 */ + DWORD old_prot = 0; + return !VirtualProtect(ptr, length, prot_native, &old_prot); +#endif /* WIN32 */ +} + +bool Process::patchMemory(void* target, const void* src, size_t count) +{ + MemoryPatcher patcher(this); + + return patcher.write(target, src, count); +} diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index 20990428dc..c917a3232a 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -36,24 +36,25 @@ POSSIBILITY OF SUCH DAMAGE. */ -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include -#include #include -#include +#include +#include "ColorText.h" +#include "CoreDefs.h" #include "RemoteClient.h" -#include -#include "MiscUtils.h" -#include -#include -#include +#include "ActiveSocket.h" +#include "Host.h" +#include "SimpleSocket.h" -#include #include "json/json.h" @@ -180,7 +181,7 @@ bool RemoteClient::connect(int port) if (!socket->Open("localhost", port)) { - default_output().printerr("Could not connect to localhost:%d\n", port); + default_output().printerr("Could not connect to localhost:{}\n", port); return false; } @@ -335,8 +336,8 @@ bool RemoteFunctionBase::bind(color_ostream &out, RemoteClient *client, if (p_client == client && this->name == name && this->plugin == plugin) return true; - out.printerr("Function already bound to %s::%s\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Function already bound to {}::{}\n", + this->plugin, this->name); return false; } @@ -372,15 +373,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, { if (!isValid()) { - out.printerr("Calling an unbound RPC function %s::%s.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("Calling an unbound RPC function {}:{}.\n", + this->plugin, this->name); return CR_NOT_IMPLEMENTED; } if (!p_client->socket->IsSocketValid()) { - out.printerr("In call to %s::%s: invalid socket.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: invalid socket.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -388,15 +389,15 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (send_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: message too large: %d.\n", - this->plugin.c_str(), this->name.c_str(), send_size); + out.printerr("In call to {}:{}: message too large: {}.\n", + this->plugin, this->name, send_size); return CR_LINK_FAILURE; } if (!sendRemoteMessage(p_client->socket, id, input, true)) { - out.printerr("In call to %s::%s: I/O error in send.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in send.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -410,8 +411,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, &header, sizeof(header))) { - out.printerr("In call to %s::%s: I/O error in receive header.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: I/O error in receive header.\n", + this->plugin, this->name); return CR_LINK_FAILURE; } @@ -422,8 +423,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In call to %s::%s: invalid received size %d.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: invalid received size {}.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -431,8 +432,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (!readFullBuffer(p_client->socket, buf, header.size)) { - out.printerr("In call to %s::%s: I/O error in receive %d bytes of data.\n", - this->plugin.c_str(), this->name.c_str(), header.size); + out.printerr("In call to {}:{}: I/O error in receive {} bytes of data.\n", + this->plugin, this->name, header.size); return CR_LINK_FAILURE; } @@ -440,8 +441,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, case RPC_REPLY_RESULT: if (!output->ParseFromArray(buf, header.size)) { - out.printerr("In call to %s::%s: error parsing received result.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: error parsing received result.\n", + this->plugin, this->name); delete[] buf; return CR_LINK_FAILURE; } @@ -454,8 +455,8 @@ command_result RemoteFunctionBase::execute(color_ostream &out, if (text_data.ParseFromArray(buf, header.size)) text_decoder.decode(&text_data); else - out.printerr("In call to %s::%s: received invalid text data.\n", - this->plugin.c_str(), this->name.c_str()); + out.printerr("In call to {}:{}: received invalid text data.\n", + this->plugin, this->name); break; default: diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 4e26b06497..aa3deb201d 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -35,27 +35,36 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include +#ifdef WIN32 +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include #include +#include #include -#include #include -#include +#include +#include + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "Debug.h" +#include "MiscUtils.h" +#include "PluginManager.h" +#include "ActiveSocket.h" +#include "Host.h" +#include "RemoteClient.h" #include "RemoteServer.h" #include "RemoteTools.h" - #include "PassiveSocket.h" -#include "PluginManager.h" -#include "MiscUtils.h" -#include "Debug.h" - -#include -#include -#include +#include "SimpleSocket.h" #include #include @@ -189,14 +198,14 @@ ServerFunctionBase *ServerConnection::findFunction(color_ostream &out, const std Plugin *plug = Core::getInstance().plug_mgr->getPluginByName(plugin); if (!plug) { - out.printerr("No such plugin: %s\n", plugin.c_str()); + out.printerr("No such plugin: {}\n", plugin); return NULL; } svc = plug->rpc_connect(out); if (!svc) { - out.printerr("Plugin %s doesn't export any RPC methods.\n", plugin.c_str()); + out.printerr("Plugin {} doesn't export any RPC methods.\n", plugin); return NULL; } @@ -299,7 +308,7 @@ void ServerConnection::threadFn() if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - out.printerr("In RPC server: invalid received size %d.\n", header.size); + out.printerr("In RPC server: invalid received size {}.\n", header.size); break; } @@ -307,7 +316,7 @@ void ServerConnection::threadFn() if (!readFullBuffer(socket, buf.get(), header.size)) { - out.printerr("In RPC server: I/O error in receive %d bytes of data.\n", header.size); + out.printerr("In RPC server: I/O error in receive {} bytes of data.\n", header.size); break; } @@ -323,17 +332,17 @@ void ServerConnection::threadFn() if (!fn) { - stream.printerr("RPC call of invalid id %d\n", header.id); + stream.printerr("RPC call of invalid id {}\n", header.id); } else { if (((fn->flags & SF_ALLOW_REMOTE) != SF_ALLOW_REMOTE) && strcmp(socket->GetClientAddr(), "127.0.0.1") != 0) { - stream.printerr("In call to %s: forbidden host: %s\n", fn->name, socket->GetClientAddr()); + stream.printerr("In call to {}: forbidden host: {}\n", fn->name, socket->GetClientAddr()); } else if (!fn->in()->ParseFromArray(buf.get(), header.size)) { - stream.printerr("In call to %s: could not decode input args.\n", fn->name); + stream.printerr("In call to {}: could not decode input args.\n", fn->name); } else { @@ -364,7 +373,7 @@ void ServerConnection::threadFn() if (out_size > RPCMessageHeader::MAX_MESSAGE_SIZE) { - stream.printerr("In call to %s: reply too large: %d.\n", + stream.printerr("In call to {}: reply too large: {}.\n", (fn ? fn->name : "UNKNOWN"), out_size); res = CR_LINK_FAILURE; } @@ -489,7 +498,7 @@ void ServerMainImpl::threadFn(std::promise promise, int port) break; case CSimpleSocket::SocketFirewallError: case CSimpleSocket::SocketProtocolError: - WARN(socket).print("Connection failed: %s\n", server.socket.DescribeError()); + WARN(socket).print("Connection failed: {}\n", server.socket.DescribeError()); break; default: break; diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index fbace1ae2e..30e24c51b5 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -35,57 +35,58 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -#include -#include -#include -#include -#include -#include -#include -#include - #include "RemoteTools.h" -#include "PluginManager.h" + +#include "ColorText.h" +#include "Core.h" +#include "CoreDefs.h" +#include "DataDefs.h" +#include "DFHackVersion.h" +#include "LuaTools.h" #include "MiscUtils.h" +#include "PluginManager.h" #include "VersionInfo.h" -#include "DFHackVersion.h" #include "modules/Materials.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" -#include "LuaTools.h" - -#include "DataDefs.h" -#include "df/plotinfost.h" #include "df/adventurest.h" -#include "df/world.h" -#include "df/world_data.h" -#include "df/unit.h" -#include "df/unit_misc_trait.h" -#include "df/unit_soul.h" -#include "df/unit_skill.h" +#include "df/creature_raw.h" +#include "df/global_objects.h" +#include "df/historical_entity.h" +#include "df/historical_figure.h" +#include "df/incident.h" +#include "df/inorganic_raw.h" +#include "df/language_name.h" #include "df/material.h" #include "df/matter_state.h" -#include "df/inorganic_raw.h" -#include "df/creature_raw.h" -#include "df/plant_raw.h" #include "df/nemesis_record.h" -#include "df/historical_figure.h" -#include "df/historical_entity.h" -#include "df/squad.h" +#include "df/plant_raw.h" +#include "df/plotinfost.h" +#include "df/profession.h" #include "df/squad_position.h" -#include "df/incident.h" +#include "df/squad.h" +#include "df/unit_misc_trait.h" +#include "df/unit_skill.h" +#include "df/unit_soul.h" +#include "df/unit.h" +#include "df/world_data.h" +#include "df/world.h" #include "BasicApi.pb.h" +#include #include #include -#include - +#include +#include +#include #include +#include +#include +#include using namespace DFHack; using namespace df::enums; @@ -336,22 +337,22 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit, } } - if (unit->curse.add_tags1.whole || - unit->curse.add_tags2.whole || - unit->curse.rem_tags1.whole || - unit->curse.rem_tags2.whole || - unit->curse.name_visible) + if (unit->uwss_add_caste_flag.whole || + unit->uwss_remove_caste_flag.whole || + unit->uwss_add_property.whole || + unit->uwss_remove_property.whole || + unit->uwss_use_display_name) { auto curse = info->mutable_curse(); - curse->set_add_tags1(unit->curse.add_tags1.whole); - curse->set_rem_tags1(unit->curse.rem_tags1.whole); - curse->set_add_tags2(unit->curse.add_tags2.whole); - curse->set_rem_tags2(unit->curse.rem_tags2.whole); + curse->set_add_tags1(unit->uwss_add_caste_flag.whole); + curse->set_rem_tags1(unit->uwss_add_property.whole); + curse->set_add_tags2(unit->uwss_remove_caste_flag.whole); + curse->set_rem_tags2(unit->uwss_remove_property.whole); - if (unit->curse.name_visible) - describeNameTriple(curse->mutable_name(), unit->curse.name, - unit->curse.name_plural, unit->curse.name_adjective); + if (unit->uwss_use_display_name) + describeNameTriple(curse->mutable_name(), unit->uwss_display_name_sing, + unit->uwss_display_name_plur, unit->uwss_display_name_adj); } for (size_t i = 0; i < unit->burrows.size(); i++) @@ -458,7 +459,7 @@ static command_result ListEnums(color_ostream &stream, BITFIELD(cie_add_tag_mask1); BITFIELD(cie_add_tag_mask2); - describe_bitfield(out->mutable_death_info_flags()); + describe_bitfield(out->mutable_death_info_flags()); ENUM(profession); @@ -529,7 +530,7 @@ static command_result ListMaterials(color_ostream &stream, if (in->builtin()) { - for (int i = 0; i < MaterialInfo::NUM_BUILTIN; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) listMaterial(out, i, -1, mask); } @@ -548,7 +549,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::CREATURE_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::CREATURE_1+j, i, mask); } } @@ -560,7 +561,7 @@ static command_result ListMaterials(color_ostream &stream, auto praw = vec[i]; for (size_t j = 0; j < praw->material.size(); j++) - listMaterial(out, MaterialInfo::PLANT_BASE+j, i, mask); + listMaterial(out, df::builtin_mats::PLANT_1+j, i, mask); } } @@ -696,16 +697,16 @@ command_result CoreService::BindMethod(color_ostream &stream, if (!fn) { - stream.printerr("RPC method not found: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("RPC method not found: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } if (fn->p_in_template->GetTypeName() != in->input_msg() || fn->p_out_template->GetTypeName() != in->output_msg()) { - stream.printerr("Requested wrong signature for RPC method: %s::%s\n", - in->plugin().c_str(), in->method().c_str()); + stream.printerr("Requested wrong signature for RPC method: {}::{}\n", + in->plugin(), in->method()); return CR_FAILURE; } diff --git a/library/TileTypes.cpp b/library/TileTypes.cpp index 7636d5bab2..9f8a72ade6 100644 --- a/library/TileTypes.cpp +++ b/library/TileTypes.cpp @@ -61,9 +61,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No shape %s in %s.\n", - enum_item_key(shape).c_str(), enum_item_key(mat).c_str() + fmt::print( + stderr, "NOTE: No shape {} in {}.\n", + enum_item_key(shape), enum_item_key(mat) ); } @@ -93,10 +93,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No special %s in %s:%s.\n", - enum_item_key(special).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str() + fmt::print( + stderr, "NOTE: No special {} in {}:{}.\n", + enum_item_key(special), enum_item_key(mat), + enum_item_key(shape) ); } @@ -139,10 +139,9 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No direction '%s' in %s:%s:%s.\n", - dir.c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str() + fmt::print( + stderr, "NOTE: No direction '{}' in {}:{}:{}.\n", + dir, enum_item_key(mat), enum_item_key(shape), enum_item_key(special) ); } @@ -162,10 +161,10 @@ static df::tiletype find_match( { if (warn) { - fprintf( - stderr, "NOTE: No variant '%s' in %s:%s:%s:%s.\n", - enum_item_key(variant).c_str(), enum_item_key(mat).c_str(), - enum_item_key(shape).c_str(), enum_item_key(special).c_str(), dir.c_str() + fmt::print( + stderr, "NOTE: No variant '{}' in {}:{}:{}:{}.\n", + enum_item_key(variant), enum_item_key(mat), + enum_item_key(shape), enum_item_key(special), dir ); } @@ -214,8 +213,8 @@ static void init_tables() tile_to_mat[tiletype_material::STONE][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in STONE.\n", - enum_item_key(tt).c_str()); + fmt::print(stderr, "No match for tile {} in STONE.\n", + enum_item_key(tt)); } else { @@ -233,8 +232,8 @@ static void init_tables() tile_to_mat[mat][tt] = ttm; if (ttm == tiletype::Void) - fprintf(stderr, "No match for tile %s in %s.\n", - enum_item_key(tt).c_str(), enum_item_key(mat).c_str()); + fmt::print(stderr, "No match for tile {} in {}.\n", + enum_item_key(tt), enum_item_key(mat)); } } } diff --git a/library/Types.cpp b/library/Types.cpp index ffed86eb59..21437723f1 100644 --- a/library/Types.cpp +++ b/library/Types.cpp @@ -22,35 +22,23 @@ must not be misrepresented as being the original software. distribution. */ -#include "Internal.h" -#include "Export.h" #include "MiscUtils.h" -#include "Error.h" #include "Types.h" #include "modules/Filesystem.h" #include "df/general_ref.h" +#include "df/general_ref_type.h" +#include "df/global_objects.h" #include "df/specific_ref.h" +#include "df/specific_ref_type.h" -#ifndef LINUX_BUILD - #include - #include "wdirent.h" -#else - #include - #include - #include - #include -#endif +#include +#include +#include -#include -#include -#include -#include - - -int DFHack::getdir(std::string dir, std::vector &files) +int DFHack::getdir(std::filesystem::path dir, std::vector &files) { return DFHack::Filesystem::listdir(dir, files); } diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp index 797161240e..2ade207cba 100644 --- a/library/VTableInterpose.cpp +++ b/library/VTableInterpose.cpp @@ -32,6 +32,7 @@ distribution. #include "Core.h" #include "DataFuncs.h" #include "MemAccess.h" +#include "MemoryPatcher.h" #include "VersionInfo.h" #include "VTableInterpose.h" @@ -210,7 +211,7 @@ void DFHack::addr_to_method_pointer_(void *pptr, void *addr) void *virtual_identity::get_vmethod_ptr(int idx) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return vtable[idx]; } @@ -218,7 +219,7 @@ void *virtual_identity::get_vmethod_ptr(int idx) const bool virtual_identity::set_vmethod_ptr(MemoryPatcher &patcher, int idx, void *ptr) const { assert(idx >= 0); - void **vtable = (void**)vtable_ptr; + void **vtable = (void**)vtable_ptr(); if (!vtable) return NULL; return patcher.write(&vtable[idx], &ptr, sizeof(void*)); } @@ -311,7 +312,7 @@ VMethodInterposeLinkBase::VMethodInterposeLinkBase(const virtual_identity *host, * - interpose_method comes from method_pointer_to_addr_ */ - fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %p (%s)\n", + fmt::print(stderr, "Bad VMethodInterposeLinkBase arguments: {} {} ({})\n", vmethod_idx, interpose_method, name_str); fflush(stderr); abort(); @@ -326,16 +327,13 @@ VMethodInterposeLinkBase::~VMethodInterposeLinkBase() VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(const virtual_identity *id) { - auto pitem = id->interpose_list.find(vmethod_idx); - if (pitem == id->interpose_list.end()) - return NULL; - auto item = pitem->second; + auto item = id->get_interpose(vmethod_idx); if (!item) - return NULL; + return nullptr; if (item->host != id) - return NULL; + return nullptr; while (item->prev && item->prev->host == id) item = item->prev; @@ -364,7 +362,7 @@ bool VMethodInterposeLinkBase::find_child_hosts(const virtual_identity *cur, voi child_next.insert(base); found = true; } - else if (child->vtable_ptr) + else if (child->vtable_ptr()) { void *cptr = child->get_vmethod_ptr(vmethod_idx); if (cptr != vmptr) @@ -401,7 +399,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) { // Otherwise, drop the link to that child: assert(child_hosts.count(from) != 0 && - from->interpose_list[vmethod_idx] == this); // while mutating this gets cleaned up below so machts nichts + from->get_interpose(vmethod_idx) == this); // while mutating this gets cleaned up below so machts nichts // Find and restore the original vmethod ptr auto last = this; @@ -413,7 +411,7 @@ void VMethodInterposeLinkBase::on_host_delete(const virtual_identity *from) // Unlink the chains child_hosts.erase(from); - from->interpose_list.erase(vmethod_idx); + from->set_interpose(vmethod_idx,nullptr); } } @@ -427,7 +425,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (is_applied()) return true; - if (!host->vtable_ptr) + if (!host->vtable_ptr()) { std::cerr << "VMethodInterposeLinkBase::apply(" << enable << "): " << name() << ": no vtable pointer: " << host->getName() << endl; @@ -435,10 +433,10 @@ bool VMethodInterposeLinkBase::apply(bool enable) } // Retrieve the current vtable entry - auto l = host->interpose_list.find(vmethod_idx); + auto l = host->get_interpose(vmethod_idx); - VMethodInterposeLinkBase* old_link = (l != host->interpose_list.end()) ? (l->second) : nullptr; - VMethodInterposeLinkBase* next_link = NULL; + VMethodInterposeLinkBase* old_link = (l != nullptr) ? l : nullptr; + VMethodInterposeLinkBase* next_link = nullptr; while (old_link && old_link->host == host && old_link->priority > priority) { @@ -473,7 +471,7 @@ bool VMethodInterposeLinkBase::apply(bool enable) if (next_link) next_link->prev = this; else - host->interpose_list[vmethod_idx] = this; + host->set_interpose(vmethod_idx,this); child_hosts.clear(); child_next.clear(); @@ -532,9 +530,9 @@ bool VMethodInterposeLinkBase::apply(bool enable) for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == old_link); // acceptable due to assign below + assert(nhost->get_interpose(vmethod_idx) == old_link); // acceptable due to assign below nhost->set_vmethod_ptr(patcher, vmethod_idx, interpose_method); - nhost->interpose_list[vmethod_idx] = this; + nhost->set_interpose(vmethod_idx, this); } return true; @@ -573,7 +571,7 @@ void VMethodInterposeLinkBase::remove() MemoryPatcher patcher; // Remove from the list in the identity and vtable - host->interpose_list[vmethod_idx] = prev; + host->set_interpose(vmethod_idx, prev); host->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); for (auto it = child_next.begin(); it != child_next.end(); ++it) @@ -589,8 +587,8 @@ void VMethodInterposeLinkBase::remove() for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it) { auto nhost = *it; - assert(nhost->interpose_list[vmethod_idx] == this); // acceptable due to assign below - nhost->interpose_list[vmethod_idx] = prev; + assert(nhost->get_interpose(vmethod_idx) == this); // acceptable due to assign below + nhost->set_interpose(vmethod_idx,prev); nhost->set_vmethod_ptr(patcher, vmethod_idx, saved_chain); if (prev) prev->child_hosts.insert(nhost); diff --git a/library/VersionInfoFactory.cpp b/library/VersionInfoFactory.cpp index b1f0472c61..94e2560e37 100644 --- a/library/VersionInfoFactory.cpp +++ b/library/VersionInfoFactory.cpp @@ -29,6 +29,7 @@ distribution. #include #include #include +#include #include "VersionInfoFactory.h" #include "VersionInfo.h" @@ -209,7 +210,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "md5-hash") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): MD5: %s\n", cstr_name, cstr_os, cstr_value); + std::cerr << fmt::format("{} ({}): MD5: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addMD5(cstr_value); @@ -217,7 +218,7 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) else if (type == "binary-timestamp") { const char *cstr_value = pMemEntry->Attribute("value"); - fprintf(stderr, "%s (%s): PE: %s\n", cstr_name, cstr_os, cstr_value); + std::cerr << fmt::format("{} ({}): PE: {}\n", cstr_name, cstr_os, cstr_value ? cstr_value : "NULL"); if(!cstr_value) throw Error::SymbolsXmlUnderspecifiedEntry(cstr_name); mem->addPE(strtol(cstr_value, 0, 16)); @@ -226,9 +227,9 @@ void VersionInfoFactory::ParseVersion (TiXmlElement* entry, VersionInfo* mem) } // method // load the XML file with offsets -bool VersionInfoFactory::loadFile(string path_to_xml) +bool VersionInfoFactory::loadFile(std::filesystem::path path_to_xml) { - TiXmlDocument doc( path_to_xml.c_str() ); + TiXmlDocument doc( path_to_xml.string().c_str() ); std::cerr << "Loading " << path_to_xml << " ... "; //bool loadOkay = doc.LoadFile(); if (!doc.LoadFile()) diff --git a/library/dfhack-run.cpp b/library/dfhack-run.cpp index df10bf1485..171032a39c 100644 --- a/library/dfhack-run.cpp +++ b/library/dfhack-run.cpp @@ -119,8 +119,8 @@ int main (int argc, char *argv[]) if (rv == CR_OK) { for (int i = 0; i < run_call.out()->value_size(); i++) - printf("%s%s", (i>0?"\t":""), run_call.out()->value(i).c_str()); - printf("\n"); + fmt::print("{}{}", (i>0?"\t":""), run_call.out()->value(i)); + fmt::print("\n"); } } else diff --git a/library/git-describe.cmake.in b/library/git-describe.cmake.in index d7d469fe8a..ad3221a4dd 100644 --- a/library/git-describe.cmake.in +++ b/library/git-describe.cmake.in @@ -1,48 +1,61 @@ # Generated from git-describe.cmake.in set(DFHACK_BUILD_ID "@DFHACK_BUILD_ID@") -if(NOT(EXISTS "${GIT_EXECUTABLE}")) - message(FATAL_ERROR "git-describe failed: could not find git at ${GIT_EXECUTABLE}") -endif() +# Fallbacks used when the source tree is not a git checkout or when revision +# information cannot be obtained. +set(DFHACK_GIT_DESCRIPTION "@DFHACK_VERSION@-unknown") +set(DFHACK_GIT_COMMIT "unknown") +set(DFHACK_GIT_XML_EXPECTED_COMMIT "unknown") +set(DFHACK_GIT_XML_COMMIT "unknown") +set(DFHACK_GIT_TAGGED FALSE) +set(DFHACK_GIT_XML_MATCH FALSE) -execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=8 --long - WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" - OUTPUT_VARIABLE DFHACK_GIT_DESCRIPTION - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE DFHACK_GIT_DESCRIPTION_RESULT) -execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" - OUTPUT_VARIABLE DFHACK_GIT_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=8 --exact-match - WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" - RESULT_VARIABLE DFHACK_GIT_TAGGED_RESULT - OUTPUT_QUIET ERROR_QUIET) +if(EXISTS "${dfhack_SOURCE_DIR}/.git") + if(NOT GIT_EXECUTABLE OR NOT EXISTS "${GIT_EXECUTABLE}") + message(WARNING "git-describe: could not find git; version information will be unavailable") + else() + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=8 --long + WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" + OUTPUT_VARIABLE DFHACK_GIT_DESCRIPTION + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE DFHACK_GIT_DESCRIPTION_RESULT) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" + OUTPUT_VARIABLE DFHACK_GIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=8 --exact-match + WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" + RESULT_VARIABLE DFHACK_GIT_TAGGED_RESULT + OUTPUT_QUIET ERROR_QUIET) -# Check library/xml for matching commits -execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD:library/xml - WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" - OUTPUT_VARIABLE DFHACK_GIT_XML_EXPECTED_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY "${dfhack_SOURCE_DIR}/library/xml" - OUTPUT_VARIABLE DFHACK_GIT_XML_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE) + # Check library/xml for matching commits + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD:library/xml + WORKING_DIRECTORY "${dfhack_SOURCE_DIR}" + OUTPUT_VARIABLE DFHACK_GIT_XML_EXPECTED_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(EXISTS "${dfhack_SOURCE_DIR}/library/xml/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY "${dfhack_SOURCE_DIR}/library/xml" + OUTPUT_VARIABLE DFHACK_GIT_XML_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() -if(NOT(${DFHACK_GIT_DESCRIPTION_RESULT} EQUAL 0)) - message(FATAL_ERROR "git-describe failed: ${DFHACK_GIT_DESCRIPTION_RESULT}") -endif() + if(DFHACK_GIT_TAGGED_RESULT STREQUAL "0") + set(DFHACK_GIT_TAGGED TRUE) + endif() -if(${DFHACK_GIT_TAGGED_RESULT} EQUAL 0) - set(DFHACK_GIT_TAGGED TRUE) -else() - set(DFHACK_GIT_TAGGED FALSE) -endif() + if(NOT "${DFHACK_GIT_XML_COMMIT}" STREQUAL "" + AND NOT "${DFHACK_GIT_XML_COMMIT}" STREQUAL "unknown" + AND "${DFHACK_GIT_XML_COMMIT}" STREQUAL "${DFHACK_GIT_XML_EXPECTED_COMMIT}") + set(DFHACK_GIT_XML_MATCH TRUE) + endif() -if(${DFHACK_GIT_XML_COMMIT} STREQUAL ${DFHACK_GIT_XML_EXPECTED_COMMIT}) - set(DFHACK_GIT_XML_MATCH TRUE) -else() - set(DFHACK_GIT_XML_MATCH FALSE) + if(NOT DFHACK_GIT_DESCRIPTION_RESULT STREQUAL "0") + # e.g. a clone with no reachable tags; fall back to the commit hash + message(WARNING "git-describe: 'git describe' failed; using commit hash as description") + set(DFHACK_GIT_DESCRIPTION "${DFHACK_GIT_COMMIT}") + endif() + endif() endif() configure_file(${dfhack_SOURCE_DIR}/library/include/git-describe.h.in ${dfhack_SOURCE_DIR}/library/include/git-describe.h) diff --git a/library/include/BitArray.h b/library/include/BitArray.h index 3679323eb8..3de148c57a 100644 --- a/library/include/BitArray.h +++ b/library/include/BitArray.h @@ -23,67 +23,105 @@ distribution. */ #pragma once -#include "Export.h" #include "Error.h" -#include -#include -#include -#include -#include -#include + +#include +#include +#include #include +#include +#include +#include + namespace DFHack { template class BitArray { + private: + // note that these are mandated by the implementation of flagarrayst in DF code, and must be exactly as below + using buffer_type = unsigned char; + using size_type = int32_t; + + buffer_type* _bits; + size_type _size; + + void resize(size_type newsize, const BitArray* replacement) + { + if (newsize == _size) + return; + + if (newsize == 0) + { + delete[] _bits; + _bits = nullptr; + _size = 0; + return; + } + + buffer_type* old_data = _bits; + + _bits = new buffer_type[newsize]; + + buffer_type* copysrc = replacement ? replacement->_bits : old_data; + size_type copysize = replacement ? replacement->_size : _size; + + if (copysrc) + std::memcpy(_bits, copysrc, std::min(copysize, newsize)); + + if (newsize > _size) + std::memset(_bits + _size, 0, newsize - _size); + + delete[] old_data; + + _size = newsize; + } + + void extend(T index) + { + size_type newsize = (index + 7 ) / 8; + if (newsize > _size) + resize(newsize); + } + public: - BitArray() : bits(NULL), size(0) {} - BitArray(const BitArray &other) : bits(NULL), size(0) + BitArray() : _bits(nullptr), _size(0) {} + BitArray(const BitArray &other) : _bits(nullptr), _size(0) { - *this = other; + resize(other._size, &other); } ~BitArray() { - free(bits); + delete [] _bits; } - explicit BitArray(T last) : bits(NULL), size(0) { + explicit BitArray(T last) : _bits(nullptr), _size(0) { extend(last); } - explicit BitArray(unsigned bytes) : bits(NULL), size(0) { + explicit BitArray(unsigned bytes) : _bits(nullptr), _size(0) { resize(bytes); } - void clear_all ( void ) + size_type size() const { return _size; } + buffer_type* bits() const { return _bits; } + + void resize( size_type newsize ) { - if(bits) - memset(bits, 0, size); + resize(newsize, nullptr); } - void resize (unsigned newsize) + + void clear_all ( void ) { - if (newsize == size) - return; - uint8_t* mem = (uint8_t *) realloc(bits, newsize); - if(!mem && newsize != 0) - throw std::bad_alloc(); - bits = mem; - if (newsize > size) - memset(bits+size, 0, newsize-size); - size = newsize; - } - BitArray &operator= (const BitArray &other) - { - resize(other.size); - memcpy(bits, other.bits, size); - return *this; + if(_bits) + memset(_bits, 0, _size); } - void extend (T index) + + BitArray& operator= (const BitArray& other) { - unsigned newsize = (index / 8) + 1; - if (newsize > size) - resize(newsize); + resize(other._size, &other); + return *this; } + void set (T index, bool value = true) { if(!value) @@ -91,71 +129,74 @@ namespace DFHack clear(index); return; } - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] |= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] |= bit; } + void clear (T index) { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - bits[byte] &= ~bit; + _bits[byte] &= ~bit; } } + void toggle (T index) { - uint32_t byte = index / 8; + size_type byte = index / 8; extend(index); - //if(byte < size) - { - uint8_t bit = 1 << (index % 8); - bits[byte] ^= bit; - } + uint8_t bit = 1 << (index % 8); + _bits[byte] ^= bit; } + bool is_set (T index) const { - uint32_t byte = index / 8; - if(byte < size) + size_type byte = index / 8; + if(byte < _size) { uint8_t bit = 1 << (index % 8); - return bit & bits[byte]; + return bit & _bits[byte]; } else return false; } + /// WARNING: this can truncate long bit arrays - uint32_t as_int () + template + I as_int () const { - if(!bits) + if(!_bits) return 0; - if(size >= 4) - return *(uint32_t *)bits; - uint32_t target = 0; - memcpy (&target, bits,size); + if (_size >= sizeof(I)) + // FIXME (C++23): should be std::start_lifetime_as + return *reinterpret_cast(_bits); + I target = 0; + std::memcpy(&target, _bits, _size); return target; } + /// WARNING: this can be truncated / only overwrite part of the data - bool operator =(uint32_t data) + template + bool operator =(I data) { - if(!bits) + if(!_bits) return false; - if (size >= 4) + if (_size >= sizeof(I)) { - (*(uint32_t *)bits) = data; + *reinterpret_cast(_bits) = data; return true; } - memcpy(bits, &data, size); + std::memcpy(_bits, &data, _size); return true; } + friend std::ostream& operator<< (std::ostream &out, BitArray &ba) { std::stringstream sstr; - for (int i = 0; i < ba.size * 8; i++) + for (int i = 0; i < ba._size * 8; i++) { if(ba.is_set((T)i)) sstr << "1 "; @@ -165,23 +206,44 @@ namespace DFHack out << sstr.str(); return out; } - uint8_t * bits; - uint32_t size; }; template class DfArray { + private: T *m_data; unsigned short m_size; + + void resize(unsigned short new_size, const DfArray* replacement) + { + if (new_size == m_size) + return; + + T* old_data = m_data; + + m_data = (T*) new T[new_size]; + + T* copysrc = replacement ? replacement->m_data : old_data; + unsigned short copysize = replacement ? replacement->m_size : m_size; + + if (copysrc) + std::memcpy(m_data, copysrc, sizeof(T) * std::min(copysize, new_size)); + + if (new_size > m_size) + std::memset(m_data + m_size, 0, sizeof(T) * (new_size - m_size)); + + delete[] old_data; + + m_size = new_size; + } public: - DfArray() : m_data(NULL), m_size(0) {} - ~DfArray() { free(m_data); } + DfArray() : m_data(nullptr), m_size(0) {} + ~DfArray() { delete[] m_data; } - DfArray(const DfArray &other) : m_data(NULL), m_size(0) + DfArray(const DfArray &other) : m_data(nullptr), m_size(0) { - resize(other.m_size); - memcpy(m_data, other.m_data,m_size*sizeof(T)); + resize(other.m_size, &other); } typedef T value_type; @@ -198,28 +260,12 @@ namespace DFHack void resize(unsigned new_size) { - if (new_size == m_size) - return; - if(!m_data) - { - m_data = (T*) malloc(sizeof(T)*new_size); - } - else - { - T* mem = (T*) realloc(m_data, sizeof(T)*new_size); - if(!mem && new_size != 0) - throw std::bad_alloc(); - m_data = mem; - } - if (new_size > m_size) - memset(m_data+sizeof(T)*m_size, 0, sizeof(T)*(new_size - m_size)); - m_size = new_size; + resize(new_size, nullptr); } DfArray &operator= (const DfArray &other) { - resize(other.size()); - memcpy(data(), other.data(), sizeof(T)*size()); + resize(other.size(), &other); return *this; } @@ -318,6 +364,22 @@ namespace DFHack return cur->item; } + I * operator->() + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + + I * operator->() const + { + CHECK_NULL_POINTER(root); + CHECK_NULL_POINTER(cur); + + return cur->item; + } + operator const_iterator() const { return const_iterator(*this); @@ -547,6 +609,8 @@ namespace DFHack template struct DfOtherVectors { + using dfhack_other_vectors = void; + std::vector & operator[](O other_id) { CHECK_INVALID_ARGUMENT(size_t(other_id) < sizeof(T) / sizeof(std::vector)); diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f484e61d4b..98bba1447e 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -24,6 +24,7 @@ distribution. #pragma once #include "Export.h" +#include "Format.h" #include #include @@ -65,7 +66,7 @@ namespace DFHack class DFHACK_EXPORT color_ostream : public std::ostream { public: - typedef DFHack::color_value color_value; + using color_value = DFHack::color_value; private: color_value cur_color; @@ -104,13 +105,24 @@ namespace DFHack color_ostream(); virtual ~color_ostream(); - /// Print a formatted string, like printf - void print(const char *format, ...) Wformat(printf,2,3); - void vprint(const char *format, va_list args) Wformat(printf,2,0); + template + void print(fmt::format_string format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + flush_buffer(false); + add_text(cur_color, str); + } - /// Print a formatted string, like printf, in red - void printerr(const char *format, ...) Wformat(printf,2,3); - void vprinterr(const char *format, va_list args) Wformat(printf,2,0); + template + void printerr(fmt::format_string format, Args&& ... args) + { + auto str = fmt::format(format, std::forward(args)...); + if (log_errors_to_stderr) { + std::cerr << str; + } + flush_buffer(false); + add_text(COLOR_LIGHTRED, str); + } /// Get color color_value color() { return cur_color; } @@ -122,6 +134,9 @@ namespace DFHack virtual bool is_console() { return false; } virtual color_ostream *proxy_target() { return NULL; } + virtual bool can_clear() const { return false; } + virtual void clear() {} + static bool log_errors_to_stderr; }; @@ -175,4 +190,5 @@ namespace DFHack void decode(dfproto::CoreTextNotification *data); }; + } diff --git a/library/include/Commands.h b/library/include/Commands.h new file mode 100644 index 0000000000..163d2e164f --- /dev/null +++ b/library/include/Commands.h @@ -0,0 +1,30 @@ +#pragma once + +#include "ColorText.h" +#include "CoreDefs.h" +#include "Core.h" + +#include +#include + +namespace DFHack +{ + namespace Commands + { + command_result help(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result load(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result enable(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result plug(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result type(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result keybinding(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result alias(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result fpause(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result clear(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result kill_lua(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result show(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result hide(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result sc_script(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + command_result dump_rpc(color_ostream& con, Core& core, const std::string& first, const std::vector& parts); + } +} diff --git a/library/include/Console.h b/library/include/Console.h index a4ca8c5452..cbf7d58511 100644 --- a/library/include/Console.h +++ b/library/include/Console.h @@ -33,6 +33,7 @@ distribution. #include #include #include +#include namespace DFHack { @@ -43,7 +44,7 @@ namespace DFHack { this->capacity = capacity; } - bool load (const char * filename) + bool load (std::filesystem::path filename) { std::string reader; std::ifstream infile(filename); @@ -58,7 +59,7 @@ namespace DFHack } return true; } - bool save (const char * filename) + bool save (std::filesystem::path filename) { if (!history.size()) return true; @@ -140,8 +141,9 @@ namespace DFHack /// shutdown the console. NOT thread-safe bool shutdown( void ); + bool can_clear() const { return true; } /// Clear the console, along with its scrollback - void clear(); + void clear() override; /// Position cursor at x,y. 1,1 = top left corner void gotoxy(int x, int y); /// Enable or disable the caret/cursor diff --git a/library/include/CoordTemplate.h b/library/include/CoordTemplate.h new file mode 100644 index 0000000000..fc98477bfe --- /dev/null +++ b/library/include/CoordTemplate.h @@ -0,0 +1,227 @@ +#pragma once + +#include +#include +#include + +#include "DataDefs.h" +#include "DataFuncs.h" +#include "Export.h" +#include "HashUtil.h" + +namespace DFHack +{ + template + constexpr T coord_default_initializer = static_cast(-30000); + + template + constexpr T coord_default_initializer = static_cast(std::numeric_limits::infinity()); + + template > + struct Coord2d + { + T x; T y; + + Coord2d() : x(initializer), y(initializer) {} + Coord2d(T x, T y) : x(x), y(y) {} + + template + explicit Coord2d(const Coord2d &other) : x(static_cast(other.x)), y(static_cast(other.y)) {}; + + bool isValid() const { return x >= 0; } + + void clear() + { + x = y = initializer; + } + + bool operator==(const Coord2d& other) const + { + return x == other.x && y == other.y; + } + bool operator!=(const Coord2d& other) const + { + return x != other.x || y != other.y; + } + bool operator<(const Coord2d& other) const + { + return x < other.x || (x == other.x && y < other.y); + } + + Coord2d operator+(const Coord2d& other) const + { + return {T(x + other.x), T(y + other.y)}; + } + Coord2d operator-(const Coord2d& other) const + { + return {T(x - other.x), T(y - other.y)}; + } + Coord2d operator/(T number) const + { + return {T(x / number), T(y / number)}; + } + Coord2d operator*(T number) const + { + return {T(x * number), T(y * number)}; + } + Coord2d operator%(T number) const requires std::integral + { + return {T(x % number), T(y % number)}; + } + Coord2d operator&(T number) const requires std::integral + { + return {T(x & number), T(y & number)}; + } + + std::size_t operator()() const + { + return hash_value(x, y); + } + + // dot product + T dotp(const Coord2d& other) const + { + return x * other.x + y * other.y; + } + + // linear interpolation between two points + Coord2d lerp(const Coord2d& other, T t) const requires std::floating_point + { + return *this + (other - *this) * t; + } + + // orthogonal projection of the point onto the line (segment) AB + Coord2d project_onto_line( + const Coord2d& A, + const Coord2d& B, + bool clamp = false + ) const requires std::floating_point + { + auto P = *this; + auto AB = B - A; + auto AP = P - A; + auto t = AP.dotp(AB) / AB.dotp(AB); + return A + AB * (clamp ? std::clamp(t, 0.0, 1.0) : t); + } + }; + + template , typename U = DFHack::Coord2d> + static const struct_field_info coord2d_fields[] = { + { struct_field_info::PRIMITIVE, "x", offsetof(U, x), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "y", offsetof(U, y), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::OBJ_METHOD, "isValid", 0, df::wrap_function(&U::isValid), 0, 0 }, + { struct_field_info::OBJ_METHOD, "clear", 0, df::wrap_function(&U::clear), 0, 0 } , + { struct_field_info::END } + }; + + template , typename U = DFHack::Coord2d> + static inline const struct_identity coord2d_identity{sizeof(U), &df::allocator_fn, nullptr, "coord2d", nullptr, coord2d_fields}; + + template > + struct Coord3d + { + T x; T y; T z; + + Coord3d() : x(initializer), y(initializer), z(initializer) {} + Coord3d(Coord2d c, T z) : x(c.x), y(c.y), z(z) {} + Coord3d(T x, T y, T z) : x(x), y(y), z(z) {} + + operator Coord2d() const + { + return {x,y}; + } + + bool isValid() const { return x >= 0; } + + void clear() + { + x = y = z = initializer; + } + + bool operator==(const Coord3d& other) const + { + return x == other.x && y == other.y && z == other.z; + } + bool operator!=(const Coord3d& other) const + { + return x != other.x || y != other.y || z != other.z; + } + bool operator<(const Coord3d& other) const + { + return x < other.x || (x == other.x && (y < other.y || (y == other.y && z < other.z))); + } + + Coord3d operator+(const Coord3d& other) const + { + return {T(x + other.x), T(y + other.y), T(z + other.z)}; + } + Coord3d operator-(const Coord3d& other) const + { + return {T(x - other.x), T(y - other.y), T(z - other.z)}; + } + Coord3d operator/(T number) const + { + return {T(x / number), T(y / number), T(z / number)}; + } + Coord3d operator*(T number) const + { + return {T(x * number), T(y * number), T(z * number)}; + } + Coord3d operator%(T number) const requires std::integral + { + return {T(x % number), T(y % number), T(z % number)}; + } + Coord3d operator&(T number) const requires std::integral + { + return {T(x & number), T(y & number), T(z & number)}; + } + + std::size_t operator()() const + { + return hash_value(x, y, z); + } + + // special weirdness used by the dig plugin + + Coord3d operator-(T number) const + { + return Coord3d(x, y, z - number); + } + Coord3d operator+(T number) const + { + return Coord3d(x, y, z + number); + } + }; + + template , typename U = DFHack::Coord3d> + static const struct_field_info coord3d_fields[] = { + { struct_field_info::PRIMITIVE, "x", offsetof(U, x), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "y", offsetof(U, y), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "z", offsetof(U, z), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::OBJ_METHOD, "isValid", 0, df::wrap_function(&U::isValid), 0, 0 }, + { struct_field_info::OBJ_METHOD, "clear", 0, df::wrap_function(&U::clear), 0, 0 } , + { struct_field_info::END } + }; + + template , typename U = DFHack::Coord3d> + static inline const struct_identity coord3d_identity{sizeof(U), &df::allocator_fn, nullptr, "coord", nullptr, coord3d_fields}; +} + +namespace df +{ + template + struct DFHACK_EXPORT identity_traits> + { + static const bool is_primitive = false; + static const compound_identity* get() { return &DFHack::coord2d_identity; } + }; + + template + struct DFHACK_EXPORT identity_traits> + { + static const bool is_primitive = false; + static const compound_identity* get() { return &DFHack::coord3d_identity; + } + }; + +} diff --git a/library/include/Core.h b/library/include/Core.h index 556e5e1b03..0245d79df7 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -29,44 +29,47 @@ distribution. #include "Export.h" #include "Hooks.h" -#include "modules/Graphic.h" +#include "modules/Filesystem.h" +#include #include #include #include #include #include #include -#include #include #include #include #include #include -#define DFH_MOD_SHIFT 1 -#define DFH_MOD_CTRL 2 -#define DFH_MOD_ALT 4 - struct WINDOW; struct lua_State; namespace df { struct viewscreen; + struct world_data; + struct map_block; } namespace DFHack { + constexpr auto DFH_MOD_SHIFT = 1; + constexpr auto DFH_MOD_CTRL = 2; + constexpr auto DFH_MOD_ALT = 4; + constexpr auto DFH_MOD_SUPER = 8; + class Process; class Module; - class Materials; struct VersionInfo; class VersionInfoFactory; class PluginManager; class Core; class ServerMain; class CoreSuspender; + class HotkeyManager; namespace Lua { namespace Core { DFHACK_EXPORT void Reset(color_ostream &out, const char *where); @@ -150,47 +153,40 @@ namespace DFHack friend void ::dfhooks_update(); friend void ::dfhooks_prerender(); friend bool ::dfhooks_sdl_event(SDL_Event* event); + friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: - /// Get the single Core instance or make one. + /// Get the current active Core instance. will assert if none exists + /// Use noInstance() to check first if unsure static Core& getInstance(); + static bool noInstance() { return active_instance == nullptr; } + /// check if the activity lock is owned by this thread bool isSuspended(void); + /// check if this is DF's render thread, which can never acquire the + /// activity lock and therefore can never call into Lua + bool isRenderThread() { return df_render_thread == std::this_thread::get_id(); } /// Is everything OK? bool isValid(void) { return !errorstate; } - /// get the materials module - Materials * getMaterials(); - /// get the graphic module - Graphic * getGraphic(); - /// sets the current hotkey command - bool setHotkeyCmd( std::string cmd ); - /// removes the hotkey command and gives it to the caller thread - std::string getHotkeyCmd( bool &keep_going ); - - /// adds a named pointer (for later or between plugins) - void RegisterData(void *p,std::string key); - /// returns a named pointer. - void *GetData(std::string key); - command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters, bool no_autocomplete = false); - command_result runCommand(color_ostream &out, const std::string &command); - bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); + command_result runCommand(color_ostream& out, const std::string& command); + + bool loadScriptFile(color_ostream &out, std::filesystem::path 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); + bool addScriptPath(std::filesystem::path path, bool search_before = false); + bool setModScriptPaths(const std::vector & mod_script_paths); + bool removeScriptPath(std::filesystem::path path); + std::filesystem::path findScript(std::string name); + void getScriptPaths(std::vector *dest); - bool getSuppressDuplicateKeyboardEvents(); + bool getSuppressDuplicateKeyboardEvents() const; void setSuppressDuplicateKeyboardEvents(bool suppress); void setMortalMode(bool value); + bool getMortalMode(); void setArmokTools(const std::vector &tool_names); + bool isArmokTool(const std::string& name); - bool ClearKeyBindings(std::string keyspec); - bool AddKeyBinding(std::string keyspec, std::string cmdline); - std::vector ListKeyBindings(std::string keyspec); int8_t getModstate() { return modstate; } bool AddAlias(const std::string &name, const std::vector &command, bool replace = false); @@ -201,10 +197,12 @@ namespace DFHack std::map> ListAliases(); std::string GetAliasCommand(const std::string &name, bool ignore_params = false); - std::string getHackPath(); + // note that this isn't valid until after DFHack is initialized by DF calling `dfhooks_init` + // that means that it's invalid during at-init static initialization + std::filesystem::path getHackPath(); - bool isWorldLoaded() { return (last_world_data_ptr != NULL); } - bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } + bool isWorldLoaded() { return (last_world_data_ptr != nullptr); } + bool isMapLoaded() { return (last_local_map_ptr != nullptr && last_world_data_ptr != nullptr); } static df::viewscreen *getTopViewscreen(); @@ -213,10 +211,22 @@ namespace DFHack std::unique_ptr p; std::shared_ptr vinfo; - static void print(const char *format, ...) Wformat(printf,1,2); - static void printerr(const char *format, ...) Wformat(printf,1,2); + template + static void print(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.print(format, std::forward(args)...); + } + + template + static void printerr(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.printerr(format, std::forward(args)...); + } - PluginManager *getPluginManager() { return plug_mgr; } + PluginManager* getPluginManager() const { return plug_mgr; } + HotkeyManager* getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -228,20 +238,59 @@ namespace DFHack return State; } - private: - DFHack::Console con; + static command_result enableLuaScript(color_ostream& out, const std::string_view name, bool enabled); + + const std::vector getStateChangeScripts() const + { + return state_change_scripts; + } + + void addStateChangeScript(const StateChangeScript& script) + { + state_change_scripts.push_back(script); + } + + bool removeStateChangeScript(const StateChangeScript& script) + { + auto it = std::find(state_change_scripts.begin(), state_change_scripts.end(), script); + if (it != state_change_scripts.end()) + { + state_change_scripts.erase(it); + return true; + } + return false; + } + + // Note that this path should be treated as potentially changeable over the life of a Core instance + // Consumers should not cache this path in long-lived local variables + const std::filesystem::path getConfigPath() + { + return Filesystem::getInstallDir() / "dfhack-config"; + } + + const std::filesystem::path getConfigDefaultsPath() + { + return getHackPath() / "data" / "dfhack-config-defaults"; + } Core(); ~Core(); + private: + static Core* active_instance; + + DFHack::Console con; + + struct Private; std::unique_ptr d; - bool InitMainThread(); + bool InitMainThread(std::filesystem::path path); bool InitSimulationThread(); int Update (void); int Shutdown (void); bool DFH_SDL_Event(SDL_Event* event); + void DFH_SDL_Loop(); bool ncurses_wgetch(int in, int & out); bool DFH_ncurses_key(int key); @@ -251,11 +300,13 @@ namespace DFHack void onStateChange(color_ostream &out, state_change_event event); void handleLoadAndUnloadScripts(color_ostream &out, state_change_event event); - Core(Core const&); // Don't Implement - void operator=(Core const&); // Don't implement + bool loadScriptPaths(color_ostream& out, bool silent = false); + + Core(Core const&) = delete; + void operator=(Core const&) = delete; // report error to user while failing - void fatal (std::string output, const char * title = NULL); + void fatal (std::string output, const char * title = nullptr); // 1 = fatal failure bool errorstate; @@ -264,50 +315,28 @@ namespace DFHack // FIXME: shouldn't be kept around like this std::unique_ptr vif; - // Module storage - struct - { - Materials * pMaterials; - Graphic * pGraphic; - } s_mods; - std::vector> allModules; - DFHack::PluginManager * plug_mgr; + DFHack::PluginManager *plug_mgr; + + // Hotkey Manager + DFHack::HotkeyManager *hotkey_mgr; - std::vector script_paths[3]; + std::vector script_paths[3]; std::mutex script_path_mutex; - // hotkey-related stuff - struct KeyBinding { - int modifiers; - std::vector command; - std::string cmdline; - std::string focus; - }; int8_t modstate; bool suppress_duplicate_keyboard_events; - bool mortal_mode; + std::atomic mortal_mode; std::unordered_set armok_tools; - std::map > key_bindings; - std::string hotkey_cmd; - enum hotkey_set_t { - NO, - SET, - SHUTDOWN, - }; - hotkey_set_t hotkey_set; - std::mutex HotkeyMutex; - std::condition_variable HotkeyCond; + std::mutex armok_mutex; std::map> aliases; std::recursive_mutex alias_mutex; - bool SelectHotkey(int key, int modifiers); - // for state change tracking - void *last_world_data_ptr; + df::world_data *last_world_data_ptr; // for state change tracking - void *last_local_map_ptr; + df::map_block**** last_local_map_ptr; friend struct Screen::Hide; df::viewscreen *top_viewscreen; bool last_pause_state; @@ -316,9 +345,6 @@ namespace DFHack // Additional state change scripts std::vector state_change_scripts; - std::mutex misc_data_mutex; - std::map misc_data_map; - /*! * \defgroup core_suspend CoreSuspender state handling serialization to * DF memory. @@ -339,6 +365,8 @@ namespace DFHack uint32_t unpaused_ms; // reset to 0 on map load + std::filesystem::path hack_path; + friend class CoreService; friend class ServerConnection; friend class CoreSuspender; @@ -509,4 +537,12 @@ namespace DFHack operator bool() const { return owns_lock(); } }; + // unclassified functions related to core + + void help_helper(color_ostream& con, const std::string& entry_name); + std::string dfhack_version_desc(); + bool is_builtin(color_ostream& con, const std::string& command); + std::string sc_event_name(state_change_event id); + state_change_event sc_event_id(std::string name); + } diff --git a/library/include/Crashlog.h b/library/include/Crashlog.h new file mode 100644 index 0000000000..c3d1f6aabd --- /dev/null +++ b/library/include/Crashlog.h @@ -0,0 +1,53 @@ +#pragma once + +struct lua_State; + +namespace DFHack { + +/** + * Installs/shuts down the platform crash reporter. Implemented + * per-platform (Crashlog.cpp on Linux, Crashlog-windows.cpp on Windows). + */ +void dfhack_crashlog_init(); +void dfhack_crashlog_shutdown(); + +/** + * Support for post-mortem crash reporting. + * + * The platform crash reporter (see Crashlog.cpp / Crashlog-windows.cpp) + * consults this to produce a Lua stack traceback when a fatal exception + * occurs while Lua code is executing. + */ +namespace Crashlog { + + /** + * The lua_State currently executing a DFHack Lua entry point + * (Lua::SafeCall, Lua::SafeResume, ...) on the calling thread, or + * nullptr if this thread is not inside a Lua call. + * + * Note that a thread's active state may be a coroutine rather than the + * main state, which is exactly what a traceback wants. + */ + lua_State *active_state(); + + /** + * Sets the calling thread's active lua_State, returning the previous + * value. Callers should generally prefer ActiveStateGuard. + */ + lua_State *set_active_state(lua_State *state); + + /** + * RAII helper to maintain active_state() around a Lua entry point. + */ + class ActiveStateGuard { + lua_State *previous; + public: + explicit ActiveStateGuard(lua_State *state) : + previous(set_active_state(state)) {} + ~ActiveStateGuard() { set_active_state(previous); } + + ActiveStateGuard(const ActiveStateGuard&) = delete; + ActiveStateGuard& operator=(const ActiveStateGuard&) = delete; + }; +} +} diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index c6a7eb8272..fd7fdc6521 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,18 +24,22 @@ distribution. #pragma once +#include +#include #include -#include #include +#include #include -#include #include +#include #include #include "BitArray.h" #include "Export.h" +#include "Format.h" +#include "HashUtil.h" -typedef struct lua_State lua_State; +struct lua_State; /* * Definitions of DFHack namespace structs used by generated headers. @@ -71,7 +75,9 @@ namespace DFHack PTRFLAG_HAS_BAD_POINTERS = 2, }; - typedef void *(*TAllocateFn)(void*,const void*); + using TAllocateFn = void *(*)(void*, const void*); + + template class type_identity_for; class DFHACK_EXPORT type_identity { const size_t size; @@ -121,43 +127,49 @@ namespace DFHack constructed_identity(size_t size, const TAllocateFn alloc) : type_identity(size), allocator(alloc) {}; - virtual bool can_allocate() const { return (allocator != NULL); } - virtual void *do_allocate() const { return allocator(NULL,NULL); } - virtual bool do_copy(void *tgt, const void *src) const { return allocator(tgt,src) == tgt; } - virtual bool do_destroy(void *obj) const { return allocator(NULL,obj) == obj; } + virtual bool can_allocate() const override { return (allocator != nullptr); } + virtual void *do_allocate() const override { return allocator(nullptr,nullptr); } + virtual bool do_copy(void *tgt, const void *src) const override { return allocator(tgt,src) == tgt; } + virtual bool do_destroy(void *obj) const override { return allocator(nullptr,obj) == obj; } public: - virtual bool isPrimitive() const { return false; } - virtual bool isConstructed() const { return true; } + virtual bool isPrimitive() const override { return false; } + virtual bool isConstructed() const override { return true; } - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; + virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const override; + virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const override; }; class DFHACK_EXPORT compound_identity : public constructed_identity { - static compound_identity *list; - mutable compound_identity *next; + static std::list* list; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; + static std::vector* top_scope; const char *dfhack_name; - mutable compound_identity *scope_parent; - mutable std::vector scope_children; - static std::vector top_scope; + + static void ensure_compound_identity_init(); protected: + const compound_identity *const scope_parent; + compound_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name); - virtual void doInit(Core *core); + virtual void doInit(Core *core) const; public: const char *getName() const { return dfhack_name; } - virtual const std::string getFullName() const; + virtual const std::string getFullName() const override; - const compound_identity *getScopeParent() const { return scope_parent; } - const std::vector &getScopeChildren() const { return scope_children; } - static const std::vector &getTopScope() { return top_scope; } + const compound_identity *getScopeParent() const { return (*parent_map)[this]; } + const std::vector &getScopeChildren() const { return (*children_map)[this]; } + static const std::vector &getTopScope() { return *top_scope; } static void Init(Core *core); + + bool is_equivalent(const compound_identity* other) const; + }; // Bitfields @@ -176,24 +188,24 @@ namespace DFHack const int num_bits; protected: - virtual bool can_allocate() const { return true; } - virtual void *do_allocate() const { return do_allocate_pod(); } - virtual bool do_copy(void *tgt, const void *src) const { do_copy_pod(tgt, src); return true; } - virtual bool do_destroy(void *obj) const { return do_destroy_pod(obj); } + virtual bool can_allocate() const override { return true; } + virtual void *do_allocate() const override { return do_allocate_pod(); } + virtual bool do_copy(void *tgt, const void *src) const override { do_copy_pod(tgt, src); return true; } + virtual bool do_destroy(void *obj) const override { return do_destroy_pod(obj); } public: bitfield_identity(size_t size, const compound_identity *scope_parent, const char *dfhack_name, int num_bits, const bitfield_item_info *bits); - virtual identity_type type() const { return IDTYPE_BITFIELD; } + virtual identity_type type() const override { return IDTYPE_BITFIELD; } - virtual bool isConstructed() const { return false; } + virtual bool isConstructed() const override { return false; } int getNumBits() const { return num_bits; } const bitfield_item_info *getBits() const { return bits; } - virtual void build_metatable(lua_State *state) const; + virtual void build_metatable(lua_State *state) const override; }; class struct_identity; @@ -201,7 +213,7 @@ namespace DFHack class DFHACK_EXPORT enum_identity : public compound_identity { public: struct ComplexData { - std::map value_index_map; + std::unordered_map value_index_map; std::vector index_value_map; ComplexData(std::initializer_list values); size_t size() const { @@ -222,10 +234,10 @@ namespace DFHack const struct_identity *attr_type; protected: - virtual bool can_allocate() const { return true; } - virtual void *do_allocate() const; - virtual bool do_copy(void *tgt, const void *src) const { do_copy_pod(tgt, src); return true; } - virtual bool do_destroy(void *obj) const { return do_destroy_pod(obj); } + virtual bool can_allocate() const override { return true; } + virtual void *do_allocate() const override; + virtual bool do_copy(void *tgt, const void *src) const override { do_copy_pod(tgt, src); return true; } + virtual bool do_destroy(void *obj) const override { return do_destroy_pod(obj); } public: enum_identity(size_t size, @@ -237,7 +249,7 @@ namespace DFHack const void *attrs, const struct_identity *attr_type); enum_identity(const enum_identity *enum_type, const type_identity *override_base_type); - virtual identity_type type() const { return IDTYPE_ENUM; } + virtual identity_type type() const override { return IDTYPE_ENUM; } int64_t getFirstItem() const { return first_item_value; } int64_t getLastItem() const { return last_item_value; } @@ -249,16 +261,16 @@ namespace DFHack const void *getAttrs() const { return attrs; } const struct_identity *getAttrType() const { return attr_type; } - virtual bool isPrimitive() const { return true; } - virtual bool isConstructed() const { return false; } + virtual bool isPrimitive() const override { return true; } + virtual bool isConstructed() const override { return false; } - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; + virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const override; + virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const override; }; struct struct_field_info_extra { - enum_identity *index_enum; - type_identity *ref_target; + const enum_identity *index_enum; + const type_identity *ref_target; const char *union_tag_field; const char *union_tag_attr; const char *original_name; @@ -286,52 +298,63 @@ namespace DFHack }; class DFHACK_EXPORT struct_identity : public compound_identity { - mutable struct_identity *parent; - mutable std::vector children; - bool has_children; + static std::unordered_map* parent_map; + static std::unordered_map>* children_map; const struct_field_info *fields; + static void ensure_struct_identity_init(); + struct_identity* parent; + protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; public: struct_identity(size_t size, TAllocateFn alloc, const compound_identity *scope_parent, const char *dfhack_name, const struct_identity *parent, const struct_field_info *fields); - virtual identity_type type() const { return IDTYPE_STRUCT; } + virtual identity_type type() const override { return IDTYPE_STRUCT; } - const struct_identity *getParent() const { return parent; } - const std::vector &getChildren() const { return children; } - bool hasChildren() const { return has_children; } + const struct_identity *getParent() const { return (*parent_map)[this]; } + const std::vector &getChildren() const { return (*children_map)[this]; } + bool hasChildren() const { return (*children_map)[this].size() > 0; } const struct_field_info *getFields() const { return fields; } bool is_subclass(const struct_identity *subtype) const; - virtual void build_metatable(lua_State *state) const; + virtual void build_metatable(lua_State *state) const override; + + bool is_equivalent(const struct_identity* other) const; }; - class DFHACK_EXPORT global_identity : public struct_identity { + class DFHACK_EXPORT global_identity_base : public struct_identity { public: - global_identity(const struct_field_info *fields) - : struct_identity(0,NULL,NULL,"global",NULL,fields) {} + using struct_identity::struct_identity; - virtual identity_type type() const { return IDTYPE_GLOBAL; } + virtual identity_type type() const override { return IDTYPE_GLOBAL; } - virtual void build_metatable(lua_State *state) const; + virtual void build_metatable(lua_State *state) const override; + }; + + // Placeholder type wrapped by the identity of the global "object", + // which does not correspond to an actual C++ type. + struct global_object { + using df_identity_base = global_identity_base; }; + using global_identity = type_identity_for; + class DFHACK_EXPORT union_identity : public struct_identity { public: union_identity(size_t size, TAllocateFn alloc, - compound_identity *scope_parent, const char *dfhack_name, - struct_identity *parent, const struct_field_info *fields); + const compound_identity *scope_parent, const char *dfhack_name, + const struct_identity *parent, const struct_field_info *fields); - virtual identity_type type() const { return IDTYPE_UNION; } + virtual identity_type type() const override { return IDTYPE_UNION; } - virtual void build_metatable(lua_State *state) const; + virtual void build_metatable(lua_State *state) const override; }; class DFHACK_EXPORT other_vectors_identity : public struct_identity { @@ -348,62 +371,90 @@ namespace DFHack const enum_identity *getIndexEnum() const { return index_enum; } - virtual void build_metatable(lua_State *state) const; + virtual void build_metatable(lua_State *state) const override; }; #ifdef _MSC_VER - typedef void *virtual_ptr; + using virtual_ptr = void*; #else - typedef virtual_class *virtual_ptr; + using virtual_ptr = virtual_class*; #endif class DFHACK_EXPORT VMethodInterposeLinkBase; class MemoryPatcher; class DFHACK_EXPORT virtual_identity : public struct_identity { - static std::map known; + public: + using interpose_t = VMethodInterposeLinkBase*; + using interpose_list_t = std::unordered_map; - const char *original_name; + private: + static std::unordered_map> *name_lookup; + static std::unordered_map* known; + static std::unordered_map* vtable_ptr_map; + static std::unordered_map* interpose_list_map; - mutable void *vtable_ptr; + const char *original_name; bool is_plugin; friend class VMethodInterposeLinkBase; - mutable std::map interpose_list; + + static void ensure_virtual_identity_init(); protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; static void *get_vtable(virtual_ptr instance_ptr) { return *(void**)instance_ptr; } - bool can_allocate() const { return struct_identity::can_allocate() && (vtable_ptr != NULL); } + void* vtable_ptr() const + { + auto& lst = (*vtable_ptr_map); + auto it = lst.find(this); + return it != lst.end() ? it->second : nullptr; + } + + bool can_allocate() const override { return struct_identity::can_allocate() && (vtable_ptr() != nullptr); } void *get_vmethod_ptr(int index) const; bool set_vmethod_ptr(MemoryPatcher &patcher, int index, void *ptr) const; + interpose_list_t& get_interpose_list() const { return (*interpose_list_map)[this]; } + interpose_t get_interpose(int index) const { + auto &lst = get_interpose_list(); + auto it = lst.find(index); + return it != lst.end() ? it->second : nullptr; + } + void set_interpose(int index, interpose_t link) const { + auto &lst = get_interpose_list(); + if (link) + lst[index] = link; + else + lst.erase(index); + } + public: virtual_identity(size_t size, const TAllocateFn alloc, const char *dfhack_name, const char *original_name, const virtual_identity *parent, const struct_field_info *fields, bool is_plugin = false); - ~virtual_identity(); + ~virtual_identity() override; - virtual identity_type type() const { return IDTYPE_CLASS; } + virtual identity_type type() const override { return IDTYPE_CLASS; } const char *getOriginalName() const { return original_name ? original_name : getName(); } public: - static virtual_identity *get(virtual_ptr instance_ptr); + static const virtual_identity *get(virtual_ptr instance_ptr); - static virtual_identity *find(void *vtable); - static virtual_identity *find(const std::string &name); + static const virtual_identity *find(void *vtable); + static const virtual_identity *find(std::string_view name); bool is_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - if (vtable_ptr) { + if (vtable_ptr()) { void *vtable = get_vtable(instance_ptr); - if (vtable == vtable_ptr) return true; + if (vtable == vtable_ptr()) return true; if (!hasChildren()) return false; } return is_subclass(get(instance_ptr)); @@ -411,20 +462,20 @@ namespace DFHack bool is_direct_instance(virtual_ptr instance_ptr) const { if (!instance_ptr) return false; - return vtable_ptr ? (vtable_ptr == get_vtable(instance_ptr)) - : (this == get(instance_ptr)); + auto vp = vtable_ptr(); + return vp ? (vp == get_vtable(instance_ptr)) : (this == get(instance_ptr)); } template static P get_vmethod_ptr(P selector); public: - bool can_instantiate() { return can_allocate(); } - virtual_ptr instantiate() { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } + bool can_instantiate() const { return can_allocate(); } + virtual_ptr instantiate() const { return can_instantiate() ? (virtual_ptr)do_allocate() : NULL; } static virtual_ptr clone(virtual_ptr obj); public: // Strictly for use in virtual class constructors - void adjust_vtable(virtual_ptr obj, virtual_identity *main); + void adjust_vtable(virtual_ptr obj, const virtual_identity *main) const; }; template @@ -474,6 +525,7 @@ inline int linear_index(const DFHack::enum_list_attr &lst, const st namespace df { + using DFHack::type_identity_for; using DFHack::type_identity; using DFHack::compound_identity; using DFHack::virtual_ptr; @@ -494,56 +546,86 @@ namespace df using DFHack::DfLinkedList; using DFHack::DfOtherVectors; - template - typename std::enable_if< - std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - *(T*)out = *(const T*)in; - return out; - } + /* + * + * Allocator functions are used to allocate, deallocate, and copy-assign objects + * + * When out is non-null, the object pointed to by in is copy-assigned over the object + * pointed to by out, if possible. When assignment is possible, out is returned. + * When assignment is not possible, nothing is done and nullptr is returned. + * The type must be copy-assignable for this to work; move-assignment is not + * supported. Callers can determine if the assignment succeeded by checking for the + * return value matching out (or simply being not null). + * + * When only in is non-null, the object pointed to by in is destroyed and deallocated, + * and in is returned. Note that the return value points to deallocated memory + * and should not be dereferenced. + * + * When both out and in are null, a new object is constructed and returned. + * + * Calling an allocator function with out non-null and in null is undefined behavior. + * + */ - template - typename std::enable_if< - !std::is_copy_assignable::value, - void* - >::type allocator_try_assign(void *out, const void *in) { - // assignment is not possible; do nothing - return NULL; - } + using df_pool_id_t = size_t; + template concept pooled_object = requires () { { T::pool_id } -> std::convertible_to; }; + template concept copy_assignable = std::assignable_from && std::assignable_from; + + template + void *allocator_fn(void *out, const void *in) { + constexpr df_pool_id_t invalid_pool_id = static_cast(-1); + // unerase type + T* _out = out ? reinterpret_cast(out) : nullptr; + const T* _in = in ? reinterpret_cast(in) : nullptr; + + if (_out) + { + if constexpr (copy_assignable) + { + *_out = *_in; + return out; + } + else + { + return nullptr; + } + } + else if (_in) + { + if constexpr (pooled_object) + { + if (_in->pool_id != invalid_pool_id) + { + throw std::runtime_error("Pool-allocated type cannot be deallocated with allocator_fn"); + } + } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - template - void *allocator_fn(void *out, const void *in) { - if (out) { return allocator_try_assign(out, in); } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); - } + delete _in; #pragma GCC diagnostic pop - - template - void *allocator_nodel_fn(void *out, const void *in) { - if (out) { *(T*)out = *(const T*)in; return out; } - else if (in) { return NULL; } - else return new T(); - } - - template - void *allocator_noassign_fn(void *out, const void *in) { - if (out) { return NULL; } - else if (in) { delete (T*)in; return (T*)in; } - else return new T(); + return const_cast(in); + } + else + return new T(); } template struct identity_traits {}; + /* + * Compound types (structs, unions, classes) are recognized either by an + * explicit df_identity_base typedef that derives from compound_identity + * (emitted by the code generator), or by the _identity member itself + * being convertible to a compound identity. + */ template - requires requires () { { &T::_identity } -> std::convertible_to; } + requires (requires { typename T::df_identity_base; } && + std::is_base_of_v) || + requires () { { &T::_identity } -> std::convertible_to; } struct identity_traits { static const bool is_primitive = false; - static compound_identity *get() { return &T::_identity; } + static const compound_identity *get() { return &T::_identity; } }; template @@ -568,6 +650,10 @@ namespace df enum_field &operator=(EnumType ev) { value = IntType(ev); return *this; } + explicit operator IntType () const { return IntType(value); } + template + explicit operator T () const { return static_cast(IntType(value)); } + }; template @@ -615,6 +701,25 @@ namespace df static const bool is_method = true; }; + template + struct return_type { + using type = RT; + static const bool is_method = false; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; + + template + struct return_type { + using type = RT; + using class_type = CT; + static const bool is_method = true; + }; } @@ -632,98 +737,87 @@ namespace DFHack { /** * Return the next item in the enum, wrapping to the first one at the end if 'wrap' is true (otherwise an invalid item). */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) + template concept complex_enum = (df::enum_traits::is_complex); + + template + inline auto next_enum_item(T v, bool wrap = true) -> typename df::enum_traits::enum_type { - typedef df::enum_traits traits; - typedef typename traits::base_type base_type; - base_type iv = base_type(v); - if (iv < traits::last_item_value) - { - return T(iv + 1); - } - else + using traits = df::enum_traits; + + if constexpr (complex_enum) { - if (wrap) - return traits::first_item; + const auto& complex = traits::complex; + const auto it = complex.value_index_map.find(v); + if (it != complex.value_index_map.end()) + { + if (!wrap && it->second + 1 == complex.size()) + { + return T(traits::last_item_value + 1); + } + size_t next_index = (it->second + 1) % complex.size(); + return T(complex.index_value_map[next_index]); + } else return T(traits::last_item_value + 1); } - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - typename df::enum_traits::enum_type - >::type next_enum_item(T v, bool wrap = true) - { - typedef df::enum_traits traits; - const auto &complex = traits::complex; - const auto it = complex.value_index_map.find(v); - if (it != complex.value_index_map.end()) + else { - if (!wrap && it->second + 1 == complex.size()) + using base_type = typename traits::base_type; + base_type iv = base_type(v); + if (iv < traits::last_item_value) { - return T(traits::last_item_value + 1); + return T(iv + 1); + } + else + { + if (wrap) + return traits::first_item; + else + return T(traits::last_item_value + 1); } - size_t next_index = (it->second + 1) % complex.size(); - return T(complex.index_value_map[next_index]); } - else - return T(traits::last_item_value + 1); } /** * Check if the value is valid for its enum type. */ - template - inline typename std::enable_if< - !df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) + template + inline bool is_valid_enum_item(T v) { - return df::enum_traits::is_valid(v); + if constexpr (complex_enum) + { + const auto& complex = df::enum_traits::complex; + return complex.value_index_map.find(v) != complex.value_index_map.end(); + } + else + { + return df::enum_traits::is_valid(v); + } } - template - inline typename std::enable_if< - df::enum_traits::is_complex, - bool - >::type is_valid_enum_item(T v) - { - const auto &complex = df::enum_traits::complex; - return complex.value_index_map.find(v) != complex.value_index_map.end(); - } /** * Return the enum item key string pointer, or NULL if none. */ template - inline typename std::enable_if< - !df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL; - } - - template - inline typename std::enable_if< - df::enum_traits::is_complex, - const char * - >::type enum_item_raw_key(T val) { - typedef df::enum_traits traits; - const auto &value_index_map = traits::complex.value_index_map; - auto it = value_index_map.find(val); - if (it != value_index_map.end()) - return traits::key_table[it->second]; + const char* enum_item_raw_key(T val) { + using traits = df::enum_traits; + if constexpr (complex_enum) + { + const auto& value_index_map = traits::complex.value_index_map; + auto it = value_index_map.find(val); + if (it != value_index_map.end()) + return traits::key_table[it->second]; + else + return nullptr; + } else - return NULL; + { + return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : nullptr; + } } + /** * Return the enum item key string pointer, or "?" if none. */ @@ -754,7 +848,7 @@ namespace DFHack { */ template inline bool find_enum_item(T *var, const std::string &name) { - typedef df::enum_traits traits; + using traits = df::enum_traits; int size = traits::last_item_value-traits::first_item_value+1; int idx = findEnumItem(name, size, traits::key_table); if (idx < 0) return false; @@ -777,7 +871,7 @@ namespace DFHack { template inline bool find_bitfield_field(unsigned *idx, const std::string &name, const T* = NULL) { typedef df::bitfield_traits traits; - return findBitfieldField(&idx, name, traits::bit_count, traits::bits); + return findBitfieldField(idx, name, traits::bit_count, traits::bits); } /** @@ -877,7 +971,7 @@ namespace DFHack { inline void flagarray_to_string(std::vector *pvec, const BitArray &val) { typedef df::enum_traits traits; int size = traits::last_item_value-traits::first_item_value+1; - flagarrayToString(pvec, val.bits, val.size, + flagarrayToString(pvec, val.bits(), val.size(), (int)traits::first_item_value, size, traits::key_table); } @@ -905,12 +999,13 @@ namespace DFHack { DFHACK_EXPORT const struct_field_info *find_union_tag(const struct_identity *structure, const struct_field_info *union_field); } + #define ENUM_ATTR(enum,attr,val) (df::enum_traits::attrs(val).attr) #define ENUM_ATTR_STR(enum,attr,val) DFHack::ifnull(ENUM_ATTR(enum,attr,val),"?") #define ENUM_KEY_STR(enum,val) (DFHack::enum_item_key(val)) #define ENUM_FIRST_ITEM(enum) (df::enum_traits::first_item) #define ENUM_LAST_ITEM(enum) (df::enum_traits::last_item) - +#define ENUM_AS_STR(val) (DFHack::enum_item_key(val)) #define ENUM_NEXT_ITEM(enum,val) \ (DFHack::next_enum_item(val)) #define FOR_ENUM_ITEMS(enum,iter) \ @@ -921,20 +1016,5 @@ namespace DFHack { */ // Global object pointers -#include "df/global_objects.h" - #define DF_GLOBAL_VALUE(name,defval) (df::global::name ? *df::global::name : defval) #define DF_GLOBAL_FIELD(name,fname,defval) (df::global::name ? df::global::name->fname : defval) - -// A couple of headers that have to be included at once -#include "df/coord2d.h" -#include "df/coord.h" - -namespace std { - template <> - struct hash { - std::size_t operator()(const df::coord& c) const { - return c(); - } - }; -} diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index e8bfc7e284..27ad5f2e7f 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -24,10 +24,6 @@ distribution. #pragma once -#include -#include -#include -#include #include #include "ColorText.h" @@ -48,15 +44,53 @@ namespace df { template concept isPrimitive = identity_traits::is_primitive || std::is_enum_v || std::is_void_v; + namespace detail { + /* + * Marshalling for wrapped-function arguments and return values that + * avoids virtual dispatch through the type identity. Types with a + * value representation (numbers, strings, enums, etc.) are handled + * directly by the lua_*_value overloads; pointers use the static + * pointer_identity_base implementation with the target identity + * supplied explicitly; anything else falls back to the identity's + * virtual method. + */ + template + inline void marshal_from_lua(lua_State *L, int idx, T &out) + { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; + if constexpr (requires { DFHack::detail::lua_write_value(L, UPVAL_METHOD_NAME, out, idx); }) + DFHack::detail::lua_write_value(L, UPVAL_METHOD_NAME, out, idx); + else if constexpr (std::is_same_v, DFHack::pointer_identity_base>) + { + const type_identity *target = nullptr; + if constexpr (requires { df::identity_traits>::get(); }) + target = df::identity_traits>::get(); + DFHack::pointer_identity_base::lua_write(L, UPVAL_METHOD_NAME, &out, target, idx); + } + else + df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &out, idx); + } + + template + inline void marshal_to_lua(lua_State *L, T &v) + { + if constexpr (requires { DFHack::detail::lua_read_value(L, v); }) + DFHack::detail::lua_read_value(L, v); + else + df::identity_traits::get()->lua_read(L, DFHack::LuaWrapper::UPVAL_METHOD_NAME, &v); + } + } + // handle call-by-reference function arguments by converting pointers to a reference // will throw if lua code tries to pass a null pointer template requires std::is_reference_v T get_from_lua_state(lua_State* L, int idx) { using DFHack::LuaWrapper::field_error; + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; using Ptr = std::add_pointer_t>; Ptr ptr{}; - df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &ptr, idx); + detail::marshal_from_lua(L, idx, ptr); if (ptr == nullptr) { field_error(L, UPVAL_METHOD_NAME, "cannot convert null pointer to reference", "call"); @@ -70,7 +104,7 @@ namespace df { T get_from_lua_state(lua_State* L, int idx) { T val{}; - df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &val, idx); + detail::marshal_from_lua(L, idx, val); return val; } @@ -85,7 +119,7 @@ namespace df { else { RT rv = std::invoke(fun, extra..., (get_from_lua_state(L, base+I))...); - df::identity_traits::get()->lua_read(L, UPVAL_METHOD_NAME, &rv); + detail::marshal_to_lua(L, rv); } } @@ -119,6 +153,7 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; @@ -128,29 +163,79 @@ namespace df { struct function_wrapper { static const int num_args = sizeof...(AT)+1; static void execute(lua_State *L, int base, RT(CT::*mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; CT *self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); call_and_push(L, base, mem_fun, self); }; }; + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(DFHack::color_ostream& out, AT...)) { + cur_lua_ostream_argument out(L); + call_and_push(L, base, fun, out); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT); + static void execute(lua_State* L, int base, RT(fun)(AT...)) { + call_and_push(L, base, fun); + } + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...)) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + + template + struct function_wrapper { + static const int num_args = sizeof...(AT) + 1; + static void execute(lua_State* L, int base, RT(CT::* mem_fun)(AT...) const) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; + CT* self = (CT*)DFHack::LuaWrapper::get_object_addr(L, base++, UPVAL_METHOD_NAME, "invoke"); + call_and_push(L, base, mem_fun, self); + }; + }; + template class function_identity : public function_identity_base { T ptr; + const std::string name; + const bool depr; + const std::string depr_message; public: using wrapper = function_wrapper; - function_identity(T ptr, bool vararg) - : function_identity_base(wrapper::num_args, vararg), ptr(ptr) {}; + function_identity(T ptr, std::string_view name, bool vararg, std::string_view depr = {}) + : function_identity_base(wrapper::num_args, vararg), ptr(ptr), name(name), depr(!depr.empty()), depr_message(depr) + {}; - virtual void invoke(lua_State *state, int base) const { wrapper::execute(state, base, ptr); } + virtual void invoke(lua_State *state, int base) const override + { + if (depr) + { + DFHack::LuaWrapper::notify_deprecated(state, name, depr_message); + } + wrapper::execute(state, base, ptr); + } }; template - inline function_identity_base *wrap_function(T ptr, bool vararg = false) { + inline function_identity_base* wrap_function(T ptr, std::string_view name = {}, bool vararg = false, std::string_view depr = {}) + { using RT = return_type::type; if constexpr (isPrimitive) - return new function_identity(ptr, vararg); + return new function_identity(ptr, name, vararg, depr); else return nullptr; }} diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 02bc8b3201..9e014b5556 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -24,18 +24,23 @@ distribution. #pragma once +#include +#include #include #include #include #include -#include #include +#include +#include #include +#include #include #include -#include +#include #include "DataDefs.h" +#include "LuaWrapper.h" namespace std { class condition_variable; @@ -60,24 +65,25 @@ namespace DFHack function_identity_base(int num_args, bool vararg = false) : type_identity(0), num_args(num_args), vararg(vararg) {}; - virtual identity_type type() const { return IDTYPE_FUNCTION; } + virtual identity_type type() const override { return IDTYPE_FUNCTION; } int getNumArgs() const { return num_args; } bool adjustArgs() const { return vararg; } - const std::string getFullName() const { return "function"; } + const std::string getFullName() const override { return "function"; } virtual void invoke(lua_State *state, int base) const = 0; - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; + virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const override; + virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const override; }; - class DFHACK_EXPORT primitive_identity : public type_identity { - public: - primitive_identity(size_t size) : type_identity(size) {}; + class DFHACK_EXPORT primitive_identity_base : public type_identity { + protected: + primitive_identity_base(size_t size) : type_identity(size) {}; - virtual identity_type type() const { return IDTYPE_PRIMITIVE; } + public: + virtual identity_type type() const override { return IDTYPE_PRIMITIVE; } }; class DFHACK_EXPORT opaque_identity : public constructed_identity { @@ -87,31 +93,32 @@ namespace DFHack opaque_identity(size_t size, TAllocateFn alloc, const std::string &name) : constructed_identity(size, alloc), name(name) {}; - virtual const std::string getFullName() const { return name; } - virtual identity_type type() const { return IDTYPE_OPAQUE; } + virtual const std::string getFullName() const override { return name; } + virtual identity_type type() const override { return IDTYPE_OPAQUE; } }; - class DFHACK_EXPORT pointer_identity : public primitive_identity { + class DFHACK_EXPORT pointer_identity_base : public primitive_identity_base { const type_identity *target; public: - pointer_identity(const type_identity *target = NULL) - : primitive_identity(sizeof(void*)), target(target) {}; + pointer_identity_base(const type_identity *target = NULL) + : primitive_identity_base(sizeof(void*)), target(target) {}; - virtual identity_type type() const { return IDTYPE_POINTER; } + virtual identity_type type() const override { return IDTYPE_POINTER; } const type_identity *getTarget() const { return target; } - const std::string getFullName() const; + const std::string getFullName() const override; static void lua_read(lua_State *state, int fname_idx, void *ptr, const type_identity *target); static void lua_write(lua_State *state, int fname_idx, void *ptr, const type_identity *target, int val_index); - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; + virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const override; + virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const override; }; class DFHACK_EXPORT container_identity : public constructed_identity { + protected: const type_identity *item; const enum_identity *ienum; @@ -119,12 +126,12 @@ namespace DFHack container_identity(size_t size, const TAllocateFn alloc, const type_identity *item, const enum_identity *ienum = NULL) : constructed_identity(size, alloc), item(item), ienum(ienum) {}; - virtual identity_type type() const { return IDTYPE_CONTAINER; } + virtual identity_type type() const override { return IDTYPE_CONTAINER; } - const std::string getFullName() const { return getFullName(item); } + const std::string getFullName() const override { return getFullName(item); } - virtual void build_metatable(lua_State *state) const; - virtual bool isContainer() const { return true; } + virtual void build_metatable(lua_State *state) const override; + virtual bool isContainer() const override { return true; } const type_identity *getItemType() const { return item; } const type_identity *getIndexEnumType() const { return ienum; } @@ -160,15 +167,15 @@ namespace DFHack const type_identity *item, const enum_identity *ienum = NULL) : container_identity(size, alloc, item, ienum) {}; - virtual identity_type type() const { return IDTYPE_PTR_CONTAINER; } + virtual identity_type type() const override { return IDTYPE_PTR_CONTAINER; } - const std::string getFullName(const type_identity *item) const; + const std::string getFullName(const type_identity *item) const override; - virtual void lua_item_reference(lua_State *state, int fname_idx, void *ptr, int idx) const; - virtual void lua_item_read(lua_State *state, int fname_idx, void *ptr, int idx) const; - virtual void lua_item_write(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const; + virtual void lua_item_reference(lua_State *state, int fname_idx, void *ptr, int idx) const override; + virtual void lua_item_read(lua_State *state, int fname_idx, void *ptr, int idx) const override; + virtual void lua_item_write(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const override; - virtual bool lua_insert2(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const; + virtual bool lua_insert2(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const override; }; class DFHACK_EXPORT bit_container_identity : public container_identity { @@ -176,16 +183,16 @@ namespace DFHack bit_container_identity(size_t size, TAllocateFn alloc, const enum_identity *ienum = NULL) : container_identity(size, alloc, NULL, ienum) {}; - virtual identity_type type() const { return IDTYPE_BIT_CONTAINER; } + virtual identity_type type() const override { return IDTYPE_BIT_CONTAINER; } - const std::string getFullName(const type_identity *item) const; + const std::string getFullName(const type_identity *item) const override; - virtual void lua_item_reference(lua_State *state, int fname_idx, void *ptr, int idx) const; - virtual void lua_item_read(lua_State *state, int fname_idx, void *ptr, int idx) const; - virtual void lua_item_write(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const; + virtual void lua_item_reference(lua_State *state, int fname_idx, void *ptr, int idx) const override; + virtual void lua_item_read(lua_State *state, int fname_idx, void *ptr, int idx) const override; + virtual void lua_item_write(lua_State *state, int fname_idx, void *ptr, int idx, int val_index) const override; protected: - virtual void *item_pointer(const type_identity *, void *, int) const { return NULL; } + virtual void *item_pointer(const type_identity *, void *, int) const override { return NULL; } virtual bool get_item(void *ptr, int idx) const = 0; virtual void set_item(void *ptr, int idx, bool val) const = 0; @@ -195,155 +202,31 @@ namespace DFHack namespace df { using DFHack::function_identity_base; - using DFHack::primitive_identity; + using DFHack::primitive_identity_base; using DFHack::opaque_identity; - using DFHack::pointer_identity; + using DFHack::pointer_identity_base; using DFHack::container_identity; using DFHack::ptr_container_identity; using DFHack::bit_container_identity; - class DFHACK_EXPORT number_identity_base : public primitive_identity { + class DFHACK_EXPORT number_identity_base : public primitive_identity_base { const char *name; public: number_identity_base(size_t size, const char *name) - : primitive_identity(size), name(name) {}; + : primitive_identity_base(size), name(name) {}; - const std::string getFullName() const { return name; } - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const = 0; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const = 0; + const std::string getFullName() const override { return name; } + virtual bool isInteger() const { return false; } }; - class DFHACK_EXPORT integer_identity_base : public number_identity_base { - public: - integer_identity_base(size_t size, const char *name) - : number_identity_base(size, name) {} - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; - - protected: - virtual int64_t read(void *ptr) const = 0; - virtual void write(void *ptr, int64_t val) const = 0; - }; - - class DFHACK_EXPORT float_identity_base : public number_identity_base { - public: - float_identity_base(size_t size, const char *name) - : number_identity_base(size, name) {} - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; - - protected: - virtual double read(void *ptr) const = 0; - virtual void write(void *ptr, double val) const = 0; - }; - - template - class integer_identity : public integer_identity_base { - public: - integer_identity(const char *name) : integer_identity_base(sizeof(T), name) {} - - protected: - virtual int64_t read(void *ptr) const { return int64_t(*(T*)ptr); } - virtual void write(void *ptr, int64_t val) const { *(T*)ptr = T(val); } - }; - - template - class float_identity : public float_identity_base { - public: - float_identity(const char *name) : float_identity_base(sizeof(T), name) {} - - protected: - virtual double read(void *ptr) const { return double(*(T*)ptr); } - virtual void write(void *ptr, double val) const { *(T*)ptr = T(val); } - }; - - class DFHACK_EXPORT bool_identity : public primitive_identity { - public: - bool_identity() : primitive_identity(sizeof(bool)) {}; - - const std::string getFullName() const { return "bool"; } - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; - }; - - class DFHACK_EXPORT ptr_string_identity : public primitive_identity { - public: - ptr_string_identity() : primitive_identity(sizeof(char*)) {}; - - const std::string getFullName() const { return "char*"; } - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; - }; - - class DFHACK_EXPORT stl_string_identity : public DFHack::constructed_identity { - public: - stl_string_identity() - : constructed_identity(sizeof(std::string), &allocator_fn) - {}; - - const std::string getFullName() const { return "string"; } - - virtual DFHack::identity_type type() const { return DFHack::IDTYPE_PRIMITIVE; } - - virtual bool isPrimitive() const { return true; } - - virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const; - virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; - }; - - class DFHACK_EXPORT stl_ptr_vector_identity : public ptr_container_identity { - public: - typedef std::vector container; - - /* - * This class assumes that std::vector is equivalent - * in layout and behavior to std::vector for any T. - */ - - stl_ptr_vector_identity(const type_identity *item = NULL, const enum_identity *ienum = NULL) - : ptr_container_identity(sizeof(container), &df::allocator_fn, item, ienum) - {}; - - const std::string getFullName(const type_identity *item) const { - return "vector" + ptr_container_identity::getFullName(item); - } - - virtual DFHack::identity_type type() const { return DFHack::IDTYPE_STL_PTR_VECTOR; } - - virtual bool resize(void *ptr, int size) const { - (*(container*)ptr).resize(size); - return true; - } - virtual bool erase(void *ptr, int size) const { - auto &ct = *(container*)ptr; - ct.erase(ct.begin()+size); - return true; - } - virtual bool insert(void *ptr, int idx, void *item) const { - auto &ct = *(container*)ptr; - ct.insert(ct.begin()+idx, item); - return true; - } - - protected: - virtual int item_count(void *ptr, CountMode) const { - return (int)((container*)ptr)->size(); - }; - virtual void *item_pointer(const type_identity *, void *ptr, int idx) const { - return &(*(container*)ptr)[idx]; - } - }; - -// Due to export issues, this stuff can only work in the main dll -#ifdef BUILD_DFHACK_LIB - class buffer_container_identity : public container_identity { + /* + * Identity for statically-sized arrays (C arrays and std::array). + * Unlike other container identities the item count is fixed, + * so instances can also serve as the identity of ad-hoc buffers. + */ + class DFHACK_EXPORT buffer_container_identity : public container_identity { int size; public: @@ -355,231 +238,648 @@ namespace df : container_identity(0, NULL, item, ienum), size(size) {} - size_t byte_size() const { return getItemType()->byte_size()*size; } + size_t byte_size() const override { return getItemType()->byte_size()*size; } - const std::string getFullName(const type_identity *item) const; + using container_identity::getFullName; + const std::string getFullName(const type_identity *item) const override; int getSize() const { return size; } - virtual DFHack::identity_type type() const { return DFHack::IDTYPE_BUFFER; } + virtual DFHack::identity_type type() const override { return DFHack::IDTYPE_BUFFER; } static const buffer_container_identity base_instance; protected: - virtual int item_count(void *ptr, CountMode) const { return size; } - virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const { + virtual int item_count(void *ptr, CountMode) const override { return size; } + virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const override { return ((uint8_t*)ptr) + idx * item->byte_size(); } }; -#endif - - template - class stl_container_identity : public container_identity { - const char *name; +} - public: - stl_container_identity(const char *name, const type_identity *item, const enum_identity *ienum = NULL) - : container_identity(sizeof(T), &allocator_fn, item, ienum), name(name) - {} +namespace DFHack +{ + namespace detail + { + /* + * Compile-time type inspection used by type_identity_for to pick + * the appropriate behavior for the wrapped C++ type. + */ - const std::string getFullName(const type_identity *item) const { - return name + container_identity::getFullName(item); - } + template struct is_std_array : std::false_type {}; + template struct is_std_array> : std::true_type {}; - virtual bool resize(void *ptr, int size) const { - (*(T*)ptr).resize(size); - return true; - } - virtual bool erase(void *ptr, int size) const { - auto &ct = *(T*)ptr; - ct.erase(ct.begin()+size); - return true; - } - virtual bool insert(void *ptr, int idx, void *item) const { - auto &ct = *(T*)ptr; - ct.insert(ct.begin()+idx, *(typename T::value_type*)item); - return true; - } + template struct is_bit_array : std::false_type {}; + template struct is_bit_array> : std::true_type {}; - protected: - virtual int item_count(void *ptr, CountMode) const { return (int)((T*)ptr)->size(); } - virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const { - return &(*(T*)ptr)[idx]; - } - }; + template struct is_enum_field : std::false_type {}; + template struct is_enum_field> : std::true_type {}; -#ifdef BUILD_DFHACK_LIB - template - class ro_stl_container_identity : public container_identity { - protected: - const char *name; + // C strings are represented by char* and const char* + template + concept c_string = std::is_same_v || std::is_same_v; - public: - ro_stl_container_identity(const char *name, const type_identity *item, const enum_identity *ienum = NULL) - : container_identity(sizeof(T), &allocator_fn, item, ienum), name(name) - {} + /* + * Container capability probes. These describe the behavioral axes + * that the container identity implementation dispatches on, so any + * type with a vector-like, array-like, bit-like, or associative + * interface is handled without being listed explicitly. + */ + template + concept has_value_type = requires { typename T::value_type; }; + + // element count is available either as size() or as a size member + template + concept sized = requires(const T& t) { { t.size() } -> std::convertible_to; } + || requires(const T& t) { { t.size } -> std::convertible_to; }; + + // dense random-access storage: t[i] yields a reference whose + // address is a value_type* (the vector/array contract) + template + concept random_access = has_value_type && sized && requires(T& t, int i) { + { &t[i] } -> std::convertible_to; + }; - const std::string getFullName(const type_identity *item) const { - return name + container_identity::getFullName(item); - } + // indexable bit elements through a proxy object (vector and similar) + template + concept bit_indexable = has_value_type && sized && !random_access && + requires(T& t, int i, bool v) { + { t[i] } -> std::convertible_to; + t[i] = v; + }; + + // bit elements through explicit accessors; by convention such + // containers report byte counts from size() and resize() (BitArray). + // BitArray indexes by the enum type and is matched by name. + template + concept bit_accessors = is_bit_array::value || + (sized && requires(T& t, int i, bool v) { + { t.is_set(i) } -> std::convertible_to; + t.set(i, v); + }); + + // key-mapped iteration (std::map, std::unordered_map, and similar) + template + concept mapped = has_value_type && requires { typename T::mapped_type; } && + requires(T& t) { t.begin(); t.end(); }; + + // iterable but not index-addressable: read-only sequential access + // (std::set and similar) + template + concept ro_sequence = has_value_type && sized && + requires(T& t) { t.begin(); t.end(); } && + !random_access && !bit_indexable && !mapped; + + // an aggregate exposing a fixed { size, items[] } table (enum_list_attr) + template + concept item_list = sized && requires(const T& t, int i) { t.items[i]; }; + + // bit containers of either flavor; mapped types that happen to + // satisfy the proxy probes (e.g. map) are excluded + template + concept bit_container = (bit_indexable || bit_accessors) && !mapped; + + // sequence containers with mutable, index-addressable elements + template + concept seq_container = random_access && + !std::is_pointer_v; + + // containers of pointer elements use pointer-container semantics + // and are stored internally as containers of void* + template + concept ptr_container = random_access && + std::is_pointer_v; + + // read-only containers accessed by iteration + template + concept assoc_container = ro_sequence || mapped; + + template + concept any_container = seq_container || ptr_container || + bit_container || assoc_container || item_list; + + // mutable-sequence operations + template + concept resizable = requires(T& t, int n) { t.resize(n); }; + + template + concept index_erasable = requires(T& t, int i) { t.erase(t.begin() + i); }; + + template + concept index_insertable = has_value_type && + requires(T& t, int i, typename T::value_type v) { t.insert(t.begin() + i, v); }; + + template + concept stl_string = std::is_same_v; + + template + concept fs_path = std::is_same_v; + + // df::bitfield_traits exists only for generated bitfield types + template + concept df_bitfield = requires { typename df::bitfield_traits::base_type; }; + + // generated compound types (struct/union/class) carry a static + // _identity member + template + concept has_identity_member = requires { T::_identity; }; + + // DfOtherVectors marks its descendants with a nested typedef + template + concept other_vectors = requires { typename T::dfhack_other_vectors; }; + + // an explicit df_identity_base typedef always wins; this lets the + // code generator (and hand-written types) pick the base directly + template + concept has_explicit_base = requires { typename T::df_identity_base; }; + + // The underlying storage actually manipulated by a container identity. + // Pointer-element containers are manipulated as containers of void*, + // and BitArray is assumed layout-equivalent to BitArray, + // matching the assumptions of the original code. + template struct container_storage { using type = T; }; + template struct container_storage> { using type = BitArray; }; + template class C, typename E, typename... A> + requires (sizeof(C) > 0) + struct container_storage> { using type = C; }; + template using container_storage_t = typename container_storage::type; + + template struct base_tag { using type = B; }; - virtual bool is_readonly() const { return true; } - virtual bool resize(void *ptr, int size) const { return false; } - virtual bool erase(void *ptr, int size) const { return false; } - virtual bool insert(void *ptr, int idx, void *item) const { return false; } + /* + * The intermediate base for container-like T; implements the + * container_identity virtuals using constexpr dispatch on T. + */ + template + class container_impl : public std::conditional_t< + bit_container, bit_container_identity, + std::conditional_t, ptr_container_identity, container_identity>> { + using cbase = std::conditional_t< + bit_container, bit_container_identity, + std::conditional_t, ptr_container_identity, container_identity>>; + using storage = container_storage_t; + + const char *name; + const type_identity *key_id; + + // item tables like enum_list_attr describe static external + // data and have no allocator + static constexpr TAllocateFn alloc_fn() { + if constexpr (item_list) + return NULL; + else + return &df::allocator_fn; + } + + static size_t element_count(const storage &ct) { + if constexpr (requires { { ct.size() } -> std::convertible_to; }) + return (size_t)ct.size(); + else + return (size_t)ct.size; + } + + public: + // sequence and read-only containers + container_impl(const char *name, const type_identity *item, const enum_identity *ienum = NULL) + requires (!bit_container && !mapped) + : cbase(sizeof(storage), alloc_fn(), item, ienum), name(name), key_id(NULL) + {} + + // bit containers take an index enum instead of an item identity + container_impl(const char *name, const enum_identity *ienum) + requires bit_container + : cbase(sizeof(storage), alloc_fn(), ienum), name(name), key_id(NULL) + {} + + // mapped containers additionally take a key identity + container_impl(const char *name, const type_identity *key, const type_identity *item) + requires mapped + : cbase(sizeof(storage), alloc_fn(), item, NULL), name(name), key_id(key) + {} + + const std::string getFullName() const override { return getFullName(this->item); } + + const std::string getFullName(const type_identity *item) const override { + if constexpr (mapped) + return std::string(name) + "<" + key_id->getFullName() + ", " + item->getFullName() + ">"; + else if constexpr (is_bit_array::value) + return "BitArray<>"; + else + return std::string(name) + cbase::getFullName(item); + } + + virtual bool is_readonly() const override { + return assoc_container; + } + + virtual bool resize(void *ptr, int size) const override { + if constexpr (bit_accessors) { + ((storage*)ptr)->resize((size+7)/8); + return true; + } + else if constexpr (resizable) { + (*(storage*)ptr).resize(size); + return true; + } + else + return false; + } + virtual bool erase(void *ptr, int index) const override { + if constexpr (index_erasable) { + auto &ct = *(storage*)ptr; + ct.erase(ct.begin()+index); + return true; + } + else + return false; + } + virtual bool insert(void *ptr, int index, void *pitem) const override { + if constexpr (ptr_container && index_insertable) { + auto &ct = *(storage*)ptr; + ct.insert(ct.begin()+index, pitem); + return true; + } + else if constexpr (!bit_container && index_insertable) { + auto &ct = *(storage*)ptr; + ct.insert(ct.begin()+index, *(typename T::value_type*)pitem); + return true; + } + else + return false; + } + virtual bool lua_insert2(lua_State* state, int fname_idx, void* ptr, int idx, int val_index) const override { + if constexpr (seq_container && index_insertable) { + using VT = typename T::value_type; + VT tmp{}; + auto id = (type_identity*)lua_touserdata(state, DFHack::LuaWrapper::UPVAL_ITEM_ID); + auto pitem = DFHack::LuaWrapper::get_object_internal(state, id, val_index, false); + bool useTemporary = (!pitem && id->isPrimitive()); + + if (useTemporary) + { + pitem = &tmp; + id->lua_write(state, fname_idx, pitem, val_index); + } + + if (id != this->item || !pitem) + DFHack::LuaWrapper::field_error(state, fname_idx, "incompatible object type", "insert"); + + return insert(ptr, idx, pitem); + } + else + return cbase::lua_insert2(state, fname_idx, ptr, idx, val_index); + } + + protected: + virtual int item_count(void *ptr, container_identity::CountMode cnt) const override { + if constexpr (bit_accessors) + return cnt == container_identity::COUNT_LEN ? (int)(element_count(*(storage*)ptr) * 8) : -1; + else if constexpr (item_list) + return cnt == container_identity::COUNT_WRITE ? 0 : (int)element_count(*(storage*)ptr); + else + return (int)element_count(*(storage*)ptr); + } + virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const override { + if constexpr (bit_container) + return NULL; + else if constexpr (item_list) + return (void*)&((storage*)ptr)->items[idx]; + else if constexpr (mapped) { + auto iter = (*(storage*)ptr).begin(); + for (; idx > 0; idx--) ++iter; + return (void*)&iter->second; + } + else if constexpr (ro_sequence) { + auto iter = (*(storage*)ptr).begin(); + for (; idx > 0; idx--) ++iter; + return (void*)&*iter; + } + else + return &(*(storage*)ptr)[idx]; + } + // get_item/set_item only exist in bit_container_identity; for + // other bases these are ordinary (unused) member functions, and + // they implicitly override the virtuals for bit containers. + bool get_item(void *ptr, int idx) const { + if constexpr (bit_accessors) + return ((storage*)ptr)->is_set(idx); + else if constexpr (bit_indexable) + return (*(storage*)ptr)[idx]; + else + return false; + } + void set_item(void *ptr, int idx, bool val) const { + if constexpr (bit_accessors) + ((storage*)ptr)->set(idx, val); + else if constexpr (bit_indexable) + (*(storage*)ptr)[idx] = val; + } + }; - protected: - virtual int item_count(void *ptr, CountMode) const { return (int)((T*)ptr)->size(); } - virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const { - auto iter = (*(T*)ptr).begin(); - for (; idx > 0; idx--) ++iter; - return (void*)&*iter; + template + constexpr auto select_identity_base() { + if constexpr (has_explicit_base) + return base_tag{}; + else if constexpr (std::is_enum_v || is_enum_field::value) + return base_tag{}; + else if constexpr (df_bitfield) + return base_tag{}; + else if constexpr (has_identity_member) { + if constexpr (std::is_union_v) + return base_tag{}; + else if constexpr (other_vectors) + return base_tag{}; + else if constexpr (std::is_polymorphic_v) + return base_tag{}; + else + return base_tag{}; + } + else if constexpr (c_string) + return base_tag{}; + else if constexpr (std::is_same_v) + return base_tag{}; + else if constexpr (std::is_pointer_v) + return base_tag{}; + else if constexpr (std::is_arithmetic_v) + return base_tag{}; + else if constexpr (stl_string || fs_path) + return base_tag{}; + else if constexpr (std::is_array_v || is_std_array::value) + return base_tag{}; + else if constexpr (any_container) + return base_tag>{}; + else if constexpr (std::is_class_v) + return base_tag{}; + else + static_assert(!sizeof(T*), "type_identity_for: no identity category for this type"); } - }; - - template - class ro_stl_assoc_container_identity : public ro_stl_container_identity { - const type_identity *key_identity; - const type_identity *item_identity; - - public: - ro_stl_assoc_container_identity(const char *name, const type_identity *key, const type_identity *item) - : ro_stl_container_identity(name, item), - key_identity(key), - item_identity(item) - {} + } - virtual const std::string getFullName(const type_identity*) const override { - return std::string(ro_stl_assoc_container_identity::name) + "<" + key_identity->getFullName() + ", " + item_identity->getFullName() + ">"; - } + template + using identity_base_of_t = typename decltype(detail::select_identity_base())::type; - protected: - virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const override { - auto iter = (*(T*)ptr).begin(); - for (; idx > 0; idx--) ++iter; - return (void*)&iter->second; - } - }; + DFHACK_EXPORT void lua_read_path(lua_State *state, void *ptr); + DFHACK_EXPORT void lua_write_path(lua_State *state, int fname_idx, void *ptr, int val_index); - class bit_array_identity : public bit_container_identity { - public: + namespace detail { /* - * This class assumes that BitArray is equivalent - * in layout and behavior to BitArray for any T. + * Per-category overrides of type_identity properties, selected at + * compile time by constraint resolution. Categories with no + * matching overload fall back to the identity base class's + * implementation in type_identity_for's virtuals. */ - typedef BitArray container; + // type() overrides + template requires (stl_string || fs_path) + constexpr identity_type type_override() { return IDTYPE_PRIMITIVE; } + template requires ptr_container + constexpr identity_type type_override() { return IDTYPE_STL_PTR_VECTOR; } - bit_array_identity(const enum_identity *ienum = NULL) - : bit_container_identity(sizeof(container), &allocator_fn, ienum) - {} + // getFullName() overrides + template requires c_string + constexpr const char *name_override() { return "char*"; } + template requires stl_string + constexpr const char *name_override() { return "string"; } + template requires fs_path + constexpr const char *name_override() { return "path"; } - virtual const std::string getFullName(const type_identity *item) const { - return "BitArray<>"; - } + // isPrimitive() override + template requires (stl_string || fs_path) + constexpr bool primitive_override() { return true; } - virtual bool resize(void *ptr, int size) const { - ((container*)ptr)->resize((size+7)/8); - return true; + /* + * Marshalling of primitive values between C++ objects and Lua. + * Overload resolution on the exact deduced type of the C++ object + * selects the implementation, so support for a new value type is + * added by writing a new overload here. Types with no matching + * overload fall back to the identity base class's implementation + * (reference semantics). All parameters are deduced, so implicit + * conversions of the wrapped type are never considered. + */ + template requires std::same_as + inline void lua_read_value(lua_State *state, const T &v) { + lua_pushboolean(state, v); } - - protected: - virtual int item_count(void *ptr, CountMode cnt) const { - return cnt == COUNT_LEN ? ((container*)ptr)->size * 8 : -1; + template requires (std::integral && !std::same_as) + inline void lua_read_value(lua_State *state, const T &v) { + lua_pushinteger(state, int64_t(v)); } - virtual bool get_item(void *ptr, int idx) const { - return ((container*)ptr)->is_set(idx); + template requires std::floating_point + inline void lua_read_value(lua_State *state, const T &v) { + lua_pushnumber(state, double(v)); } - virtual void set_item(void *ptr, int idx, bool val) const { - ((container*)ptr)->set(idx, val); + // enums marshal through their underlying type, matching + // enum_identity's forwarding to base_type + template requires std::is_enum_v + inline void lua_read_value(lua_State *state, const T &v) { + lua_read_value(state, static_cast>(v)); } - }; -#endif - - class DFHACK_EXPORT stl_bit_vector_identity : public bit_container_identity { - public: - typedef std::vector container; - - stl_bit_vector_identity(const enum_identity *ienum = NULL) - : bit_container_identity(sizeof(container), &df::allocator_fn, ienum) - {} - - const std::string getFullName(const type_identity *item) const { - return "vector" + bit_container_identity::getFullName(item); + template requires c_string + inline void lua_read_value(lua_State *state, const T &v) { + if (v) + lua_pushstring(state, v); + else + lua_pushnil(state); } - - virtual bool resize(void *ptr, int size) const { - (*(container*)ptr).resize(size); - return true; + template requires stl_string + inline void lua_read_value(lua_State *state, const T &v) { + lua_pushlstring(state, v.data(), v.size()); + } + template requires fs_path + inline void lua_read_value(lua_State *state, const T &v) { + lua_read_path(state, const_cast(&v)); } - protected: - virtual int item_count(void *ptr, CountMode) const { - return (int)((container*)ptr)->size(); + template requires std::same_as + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + if (lua_isboolean(state, val_index) || lua_isnil(state, val_index)) + out = lua_toboolean(state, val_index); + else if (lua_isnumber(state, val_index)) + out = lua_tointeger(state, val_index) != 0; + else + LuaWrapper::field_error(state, fname_idx, "boolean or number expected", "write"); } - virtual bool get_item(void *ptr, int idx) const { - return (*(container*)ptr)[idx]; + template requires (std::integral && !std::same_as) + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + int is_num = 0; + auto value = lua_tointegerx(state, val_index, &is_num); + if (!is_num) + LuaWrapper::field_error(state, fname_idx, "integer expected", "write"); + out = T(value); } - virtual void set_item(void *ptr, int idx, bool val) const { - (*(container*)ptr)[idx] = val; + template requires std::floating_point + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + if (!lua_isnumber(state, val_index)) + LuaWrapper::field_error(state, fname_idx, "number expected", "write"); + out = T(lua_tonumber(state, val_index)); } - }; + template requires std::is_enum_v + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + std::underlying_type_t tmp; + lua_write_value(state, fname_idx, tmp, val_index); + out = T(tmp); + } + template requires c_string + inline void lua_write_value(lua_State *state, int fname_idx, T &, int) { + LuaWrapper::field_error(state, fname_idx, "raw pointer string", "write"); + } + template requires stl_string + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + size_t size; + const char *bytes = lua_tolstring(state, val_index, &size); + if (!bytes) + LuaWrapper::field_error(state, fname_idx, "string expected", "write"); + out.assign(bytes, size); + } + template requires fs_path + inline void lua_write_value(lua_State *state, int fname_idx, T &out, int val_index) { + lua_write_path(state, fname_idx, &out, val_index); + } + } + + /* + * The identity of the C++ type T. The base class is selected by + * compile-time inspection of T (or an explicit df_identity_base + * typedef in T), and the remaining behavior is implemented here + * with constexpr dispatch on the properties of T. + */ + template + class type_identity_for : public identity_base_of_t { + using base = identity_base_of_t; -#ifdef BUILD_DFHACK_LIB - template - class enum_list_attr_identity : public container_identity { public: - typedef enum_list_attr container; + // forwards everything to the base class constructor; covers all + // compound identities, arrays, and opaque types + template + requires (sizeof...(Args) > 0 && std::is_constructible_v) + explicit type_identity_for(Args&&... args) : base(std::forward(args)...) {} + + // numbers take only a name; the size comes from T + explicit type_identity_for(const char *name) + requires std::is_same_v + : base(sizeof(T), name) {} + + // C strings + type_identity_for() + requires detail::c_string + : base(sizeof(T)) {} + + // std::string and std::filesystem::path + type_identity_for() + requires (detail::stl_string || detail::fs_path) + : base(sizeof(T), &df::allocator_fn) {} + + // pointers derive the target from identity_traits, or take an + // explicit target (used for void*) + type_identity_for() + requires (std::is_pointer_v && std::is_same_v) + : base(df::identity_traits>::get()) {} + + // named sequence/set containers + type_identity_for(const char *name, const type_identity *item, const enum_identity *ienum = NULL) + requires (std::is_same_v> && + !detail::bit_container && !detail::mapped) + : base(name, item, ienum) {} + + // named mapped containers additionally take a key identity + type_identity_for(const char *name, const type_identity *key, const type_identity *item) + requires detail::mapped + : base(name, key, item) {} + + // containers of pointer elements + type_identity_for(const type_identity *item = NULL, const enum_identity *ienum = NULL) + requires detail::ptr_container + : base("vector", item, ienum) {} + + // std::vector and BitArray + type_identity_for(const enum_identity *ienum = NULL) + requires detail::bit_container + : base("vector", ienum) {} + + // the global object wraps no actual type + explicit type_identity_for(const struct_field_info *fields) + requires std::is_same_v + : base(0, NULL, NULL, "global", NULL, fields) {} + + virtual identity_type type() const override { + if constexpr (requires { detail::type_override(); }) + return detail::type_override(); + else + return base::type(); + } - enum_list_attr_identity(const type_identity *item) - : container_identity(sizeof(container), NULL, item, NULL) - {} + virtual const std::string getFullName() const override { + if constexpr (requires { detail::name_override(); }) + return detail::name_override(); + else + return base::getFullName(); + } - const std::string getFullName(const type_identity *item) const { - return "enum_list_attr" + container_identity::getFullName(item); + virtual bool isPrimitive() const override { + if constexpr (requires { detail::primitive_override(); }) + return detail::primitive_override(); + else + return base::isPrimitive(); } - protected: - virtual int item_count(void *ptr, CountMode cm) const { - return cm == COUNT_WRITE ? 0 : (int)((container*)ptr)->size; + // isInteger only exists in number_identity_base; for other bases + // this is an ordinary member function, and it implicitly overrides + // the virtual for numbers. + bool isInteger() const { + return std::is_integral_v && !std::is_same_v; } - virtual void *item_pointer(const type_identity *item, void *ptr, int idx) const { - return (void*)&((container*)ptr)->items[idx]; + + virtual void lua_read(lua_State *state, int fname_idx, void *ptr) const override { + if constexpr (requires { detail::lua_read_value(state, *(const T*)ptr); }) + detail::lua_read_value(state, *(const T*)ptr); + else + base::lua_read(state, fname_idx, ptr); + } + + virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const override { + if constexpr (requires { detail::lua_write_value(state, fname_idx, *(T*)ptr, val_index); }) + detail::lua_write_value(state, fname_idx, *(T*)ptr, val_index); + else + base::lua_write(state, fname_idx, ptr, val_index); } }; -#endif +} -#define NUMBER_IDENTITY_TRAITS(category, type) \ +namespace df +{ + using DFHack::type_identity_for; + +#define NUMBER_IDENTITY_TRAITS(type) \ template<> struct DFHACK_EXPORT identity_traits { \ static const bool is_primitive = true; \ - static const category##_identity identity; \ - static const category##_identity_base *get() { return &identity; } \ + static const type_identity_for identity; \ + static const type_identity_for *get() { return &identity; } \ }; -#define INTEGER_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(integer, type) -#define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type) - // the space after the use of "type" in OPAQUE_IDENTITY_TRAITS is _required_ // without it the macro generates a syntax error when type is a template specification #define OPAQUE_IDENTITY_TRAITS(...) \ template<> struct DFHACK_EXPORT identity_traits<__VA_ARGS__ > { \ - static const opaque_identity identity; \ - static const opaque_identity *get() { return &identity; } \ - }; - - INTEGER_IDENTITY_TRAITS(char); - INTEGER_IDENTITY_TRAITS(signed char); - INTEGER_IDENTITY_TRAITS(unsigned char); - INTEGER_IDENTITY_TRAITS(short); - INTEGER_IDENTITY_TRAITS(unsigned short); - INTEGER_IDENTITY_TRAITS(int); - INTEGER_IDENTITY_TRAITS(unsigned int); - INTEGER_IDENTITY_TRAITS(long); - INTEGER_IDENTITY_TRAITS(unsigned long); - INTEGER_IDENTITY_TRAITS(long long); - INTEGER_IDENTITY_TRAITS(unsigned long long); - FLOAT_IDENTITY_TRAITS(float); - FLOAT_IDENTITY_TRAITS(double); + static const type_identity_for<__VA_ARGS__ > identity; \ + static const type_identity_for<__VA_ARGS__ > *get() { return &identity; } \ + }; + + NUMBER_IDENTITY_TRAITS(char); + NUMBER_IDENTITY_TRAITS(signed char); + NUMBER_IDENTITY_TRAITS(unsigned char); + NUMBER_IDENTITY_TRAITS(short); + NUMBER_IDENTITY_TRAITS(unsigned short); + NUMBER_IDENTITY_TRAITS(int); + NUMBER_IDENTITY_TRAITS(unsigned int); + NUMBER_IDENTITY_TRAITS(long); + NUMBER_IDENTITY_TRAITS(unsigned long); + NUMBER_IDENTITY_TRAITS(long long); + NUMBER_IDENTITY_TRAITS(unsigned long long); + NUMBER_IDENTITY_TRAITS(wchar_t); + NUMBER_IDENTITY_TRAITS(float); + NUMBER_IDENTITY_TRAITS(double); + NUMBER_IDENTITY_TRAITS(bool); + OPAQUE_IDENTITY_TRAITS(wchar_t*); OPAQUE_IDENTITY_TRAITS(std::condition_variable); OPAQUE_IDENTITY_TRAITS(std::fstream); OPAQUE_IDENTITY_TRAITS(std::mutex); @@ -591,62 +891,60 @@ namespace df OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(std::filesystem::file_time_type); #ifdef BUILD_DFHACK_LIB template struct DFHACK_EXPORT identity_traits> { static opaque_identity *get() { - typedef std::shared_ptr type; + using type = std::shared_ptr; static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; - static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); + static opaque_identity identity(sizeof(type), allocator_fn, name); return &identity; } }; #endif - template<> struct DFHACK_EXPORT identity_traits { + template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; - static const bool_identity identity; - static const bool_identity *get() { return &identity; } + static const type_identity_for identity; + static const type_identity_for *get() { return &identity; } }; - template<> struct DFHACK_EXPORT identity_traits { + template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; - static const stl_string_identity identity; - static const stl_string_identity *get() { return &identity; } + static const type_identity_for identity; + static const type_identity_for* get() { return &identity; } }; - template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; - static const ptr_string_identity identity; - static const ptr_string_identity *get() { return &identity; } + static const type_identity_for identity; + static const type_identity_for *get() { return &identity; } }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; - static const ptr_string_identity identity; - static const ptr_string_identity *get() { return &identity; } + static const type_identity_for identity; + static const type_identity_for *get() { return &identity; } }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; - static const pointer_identity identity; - static const pointer_identity *get() { return &identity; } + static const type_identity_for identity; + static const type_identity_for *get() { return &identity; } }; template<> struct DFHACK_EXPORT identity_traits > { - static const stl_ptr_vector_identity identity; - static const stl_ptr_vector_identity *get() { return &identity; } + static const type_identity_for > identity; + static const type_identity_for > *get() { return &identity; } }; template<> struct DFHACK_EXPORT identity_traits > { - static const stl_bit_vector_identity identity; - static const stl_bit_vector_identity *get() { return &identity; } + static const type_identity_for > identity; + static const type_identity_for > *get() { return &identity; } }; #undef NUMBER_IDENTITY_TRAITS -#undef INTEGER_IDENTITY_TRAITS -#undef FLOAT_IDENTITY_TRAITS #undef OPAQUE_IDENTITY_TRAITS // Container declarations @@ -659,7 +957,7 @@ namespace df template struct identity_traits { static const bool is_primitive = true; - static const pointer_identity *get(); + static const pointer_identity_base *get(); }; #ifdef BUILD_DFHACK_LIB @@ -667,13 +965,18 @@ namespace df static const container_identity *get(); }; + template struct identity_traits> + { + static const container_identity* get(); + }; + template struct identity_traits > { static const container_identity *get(); }; #endif template struct identity_traits > { - static const stl_ptr_vector_identity *get(); + static const ptr_container_identity *get(); }; // explicit specializations for these two types @@ -705,8 +1008,13 @@ namespace df static const container_identity *get(); }; + template struct identity_traits > + { + static const container_identity* get(); + }; + template<> struct identity_traits > { - static const bit_array_identity identity; + static const type_identity_for > identity; static const bit_container_identity *get() { return &identity; } }; @@ -728,47 +1036,53 @@ namespace df #ifdef BUILD_DFHACK_LIB template inline const enum_identity *identity_traits >::get() { - static const enum_identity identity(identity_traits::get(), identity_traits::get()); + static const type_identity_for > identity(identity_traits::get(), identity_traits::get()); return &identity; } #endif template - inline const pointer_identity *identity_traits::get() { - static const pointer_identity identity(identity_traits::get()); + inline const pointer_identity_base *identity_traits::get() { + static const type_identity_for identity; return &identity; } #ifdef BUILD_DFHACK_LIB template inline const container_identity *identity_traits::get() { - static const buffer_container_identity identity(sz, identity_traits::get()); + static const type_identity_for identity(sz, identity_traits::get()); + return &identity; + } + + template + inline const container_identity* identity_traits>::get() + { + static const type_identity_for > identity(static_cast(sz), df::identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef std::vector container; - static const stl_container_identity identity("vector", identity_traits::get()); + static const type_identity_for > identity("vector", identity_traits::get()); return &identity; } #endif template - inline const stl_ptr_vector_identity *identity_traits >::get() { - static const stl_ptr_vector_identity identity(identity_traits::get()); + inline const ptr_container_identity *identity_traits >::get() { + static const type_identity_for > identity(identity_traits::get()); return &identity; } // explicit specializations for these two types // for availability in plugins - extern const DFHACK_EXPORT stl_container_identity > stl_vector_int32_t_identity; + extern const DFHACK_EXPORT type_identity_for > stl_vector_int32_t_identity; inline const container_identity* identity_traits >::get() { return &stl_vector_int32_t_identity; } - extern const DFHACK_EXPORT stl_container_identity > stl_vector_int16_t_identity; + extern const DFHACK_EXPORT type_identity_for > stl_vector_int16_t_identity; inline const container_identity* identity_traits >::get() { return &stl_vector_int16_t_identity; } @@ -776,48 +1090,50 @@ namespace df #ifdef BUILD_DFHACK_LIB template inline const container_identity *identity_traits >::get() { - typedef std::deque container; - static const stl_container_identity identity("deque", identity_traits::get()); + static const type_identity_for > identity("deque", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef std::set container; - static const ro_stl_container_identity identity("set", identity_traits::get()); + static const type_identity_for > identity("set", identity_traits::get()); + return &identity; + } + + template + inline const container_identity* identity_traits >::get() + { + static const type_identity_for > identity("unordered_set", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::map container; - static const ro_stl_assoc_container_identity identity("map", identity_traits::get(), identity_traits::get()); + static const type_identity_for > identity("map", identity_traits::get(), identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::unordered_map container; - static const ro_stl_assoc_container_identity identity("unordered_map", identity_traits::get(), identity_traits::get()); + static const type_identity_for > identity("unordered_map", identity_traits::get(), identity_traits::get()); return &identity; } template inline const bit_container_identity *identity_traits >::get() { - static const bit_array_identity identity(identity_traits::get()); + static const type_identity_for > identity(identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef DfArray container; - static const stl_container_identity identity("DfArray", identity_traits::get()); + static const type_identity_for > identity("DfArray", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - static const enum_list_attr_identity identity(identity_traits::get()); + static const type_identity_for > identity("enum_list_attr", identity_traits::get()); return &identity; } #endif diff --git a/library/include/Debug.h b/library/include/Debug.h index 48e661acc4..632d4910b6 100644 --- a/library/include/Debug.h +++ b/library/include/Debug.h @@ -183,7 +183,7 @@ class DFHACK_EXPORT DebugCategory final { }; /*! - * Fetch a steam object proxy object for output. It also adds standard + * Fetch a stream object proxy object for output. It also adds standard * message components like time and plugin and category names to the line. * * User must make sure that the line is terminated with a line end. @@ -194,6 +194,13 @@ class DFHACK_EXPORT DebugCategory final { */ ostream_proxy_prefix getStream(const level msgLevel) const { + // if the core instance is unavailable, use stderr as a fallback + if (Core::noInstance()) + { + static color_ostream_wrapper fallback{std::cerr}; + return {*this,fallback,msgLevel}; + } + return {*this,Core::getInstance().getConsole(),msgLevel}; } /*! diff --git a/library/include/Error.h b/library/include/Error.h index a4624d5f72..550e1b0198 100644 --- a/library/include/Error.h +++ b/library/include/Error.h @@ -25,7 +25,6 @@ distribution. #pragma once #include -#include #include #include "Export.h" diff --git a/library/include/Format.h b/library/include/Format.h new file mode 100644 index 0000000000..52d4e0e3c6 --- /dev/null +++ b/library/include/Format.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef USE_FMTLIB + +#include +#include +#include + +#else + +#include + +namespace fmt = std; + +#endif diff --git a/library/include/HashUtil.h b/library/include/HashUtil.h new file mode 100644 index 0000000000..7e2619d504 --- /dev/null +++ b/library/include/HashUtil.h @@ -0,0 +1,46 @@ +#ifndef HASHUTIL_H +#define HASHUTIL_H + +// some utility classes for dealing with hashes + +namespace +{ + // borrowed from Boost + template + inline void hash_combine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b97f4a7c15ull + (seed << 12) + (seed >> 4); + } + + template + inline std::size_t hash_value(const Args&... args) + { + std::size_t seed = 0; + (hash_combine(seed, args), ...); + return seed; + } + + template + struct overload : Bases ... + { + using is_transparent = void; + using Bases::operator() ...; + }; + + struct char_pointer_hash + { + auto operator()(const char* ptr) const noexcept + { + return std::hash{}(ptr); + } + }; + + using transparent_string_hash = overload< + std::hash, + std::hash, + char_pointer_hash + >; +} + +#endif diff --git a/library/include/Hooks.h b/library/include/Hooks.h index 6945de2ea6..5f49d8daba 100644 --- a/library/include/Hooks.h +++ b/library/include/Hooks.h @@ -26,6 +26,7 @@ distribution. union SDL_Event; +DFhackCExport void dfhooks_preinit(std::filesystem::path dllpath); DFhackCExport void dfhooks_init(); DFhackCExport void dfhooks_shutdown(); DFhackCExport void dfhooks_update(); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 53c17e1b21..f929c562f3 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -24,30 +24,29 @@ distribution. #pragma once +#include #include -#include -#include -#include #include +#include +#include +#include #include #include #include +#include #include "Core.h" #include "ColorText.h" #include "DataDefs.h" +#include "DataIdentity.h" +#include "df/coord.h" +#include "df/coord2d.h" #include "df/interface_key.h" -#include "df/interfacest.h" #include #include -/// Allocate a new user data object and push it on the stack -inline void *operator new (std::size_t size, lua_State *L) { - return lua_newuserdata(L, size); -} - namespace DFHack { class function_identity_base; struct MaterialInfo; @@ -60,12 +59,26 @@ namespace DFHack { }; } -namespace DFHack {namespace Lua { +namespace DFHack::Lua { /** * Create or initialize a lua interpreter with access to DFHack tools. */ DFHACK_EXPORT lua_State *Open(color_ostream &out, lua_State *state = NULL); + /** + * Allocate a lua userdata and construct a C++ object in that userdata's storage space + * The C++ object must be trivially destructible as lua GC will _not_ call the object's destructor + * be aware that the created userdata is left on the Lua stack as well as returned to the caller + */ + template + requires (std::is_trivially_destructible_v) + T* make_lua_userdata(lua_State* L, Args&&... args) + { + void* stg = lua_newuserdata(L, sizeof(T)); + T * obj = ::new (stg) T(std::forward(args)...); + return obj; + } + DFHACK_EXPORT void PushDFHack(lua_State *state); DFHACK_EXPORT void PushBaseGlobals(lua_State *state); @@ -130,20 +143,25 @@ namespace DFHack {namespace Lua { * Return behavior is of SafeCall below. */ DFHACK_EXPORT bool AssignDFObject(color_ostream &out, lua_State *state, - type_identity *type, void *target, int val_index, + const type_identity *type, void *target, int val_index, bool exact_type = false, bool perr = true); /** * Assign the value at val_index to the target of given identity using df.assign(). * Otherwise throws an error. */ - DFHACK_EXPORT void CheckDFAssign(lua_State *state, type_identity *type, + DFHACK_EXPORT void CheckDFAssign(lua_State *state, const type_identity *type, void *target, int val_index, bool exact_type = false); + template concept df_object = requires() + { + { df::identity_traits::get() } -> std::convertible_to; + }; + /** * Push the pointer onto the stack as a wrapped DF object of a specific type. */ - template + template void PushDFObject(lua_State *state, T *ptr) { PushDFObject(state, df::identity_traits::get(), ptr); } @@ -151,7 +169,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. */ - template + template T *GetDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)GetDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -159,7 +177,7 @@ namespace DFHack {namespace Lua { /** * Check that the value is a wrapped DF object of the correct type, and if so return the pointer. Otherwise throw an argument type error. */ - template + template T *CheckDFObject(lua_State *state, int val_index, bool exact_type = false) { return (T*)CheckDFObject(state, df::identity_traits::get(), val_index, exact_type); } @@ -167,7 +185,7 @@ namespace DFHack {namespace Lua { /** * Assign the value at val_index to the target using df.assign(). */ - template + template bool AssignDFObject(color_ostream &out, lua_State *state, T *target, int val_index, bool exact_type = false, bool perr = true) { return AssignDFObject(out, state, df::identity_traits::get(), @@ -178,7 +196,7 @@ namespace DFHack {namespace Lua { * Assign the value at val_index to the target using df.assign(). * Throws in case of an error. */ - template + template void CheckDFAssign(lua_State *state, T *target, int val_index, bool exact_type = false) { CheckDFAssign(state, df::identity_traits::get(), target, val_index, exact_type); } @@ -273,16 +291,16 @@ namespace DFHack {namespace Lua { * Uses RunCoreQueryLoop internally. */ DFHACK_EXPORT bool InterpreterLoop(color_ostream &out, lua_State *state, - const char *prompt = NULL, const char *hfile = NULL); + std::string_view prompt = {}, std::filesystem::path hfile = {}); /** * Run an interactive prompt loop. All access to the lua state * is done inside CoreSuspender, while waiting for input happens * without the suspend lock. */ - DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, - bool (*init)(color_ostream&, lua_State*, void*), - void *arg); + using init_fn = std::function; + + DFHACK_EXPORT bool RunCoreQueryLoop(color_ostream &out, lua_State *state, init_fn init); /** * Attempt to interrupt the currently-executing lua function by raising a lua error @@ -298,42 +316,33 @@ namespace DFHack {namespace Lua { /** * Push utility functions */ -#if 0 -#define NUMBER_PUSH(type) inline void Push(lua_State *state, type value) { lua_pushnumber(state, value); } - NUMBER_PUSH(char) - NUMBER_PUSH(int8_t) NUMBER_PUSH(uint8_t) - NUMBER_PUSH(int16_t) NUMBER_PUSH(uint16_t) - NUMBER_PUSH(int32_t) NUMBER_PUSH(uint32_t) - NUMBER_PUSH(int64_t) NUMBER_PUSH(uint64_t) - NUMBER_PUSH(float) NUMBER_PUSH(double) -#undef NUMBER_PUSH -#else - template - inline typename std::enable_if::value || std::is_enum::value>::type - Push(lua_State *state, T value) { + template concept lua_integral = (std::is_integral_v || std::is_enum_v); + + inline void Push(lua_State *state, lua_integral auto value) { lua_pushinteger(state, value); } - template - inline typename std::enable_if::value>::type - Push(lua_State *state, T value) { + inline void Push(lua_State* state, std::floating_point auto value) { lua_pushnumber(state, lua_Number(value)); } -#endif inline void Push(lua_State *state, bool value) { lua_pushboolean(state, value); } - inline void Push(lua_State *state, const char *str) { - lua_pushstring(state, str); - } - inline void Push(lua_State *state, const std::string &str) { - lua_pushlstring(state, str.data(), str.size()); + + template concept lua_string = (std::convertible_to); + + inline void Push(lua_State *state, lua_string auto str) { + std::string_view sv{ str }; + lua_pushlstring(state, sv.data(), sv.size()); } + DFHACK_EXPORT void Push(lua_State *state, const df::coord &obj); DFHACK_EXPORT void Push(lua_State *state, const df::coord2d &obj); void Push(lua_State *state, const Units::NoblePosition &pos); DFHACK_EXPORT void Push(lua_State *state, const MaterialInfo &info); DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info); - template inline void Push(lua_State *state, T *ptr) { + + template inline void Push(lua_State *state, T *ptr) + { PushDFObject(state, ptr); } @@ -472,10 +481,6 @@ namespace DFHack {namespace Lua { * All accesses must be done under CoreSuspender. */ namespace Core { -// DFHACK_EXPORT extern lua_State *State; - - // Not exported; for use by the Core class - lua_State* Init(color_ostream &out); DFHACK_EXPORT void Reset(color_ostream &out, const char *where); // Events signalled by the core @@ -513,9 +518,9 @@ namespace DFHack {namespace Lua { } } /** - * High-level wrappers for CallLuaModuleFunction that automatically suspends the - * core and pushes either an argument vector (i.e. single type variable number) or - * an argument tuple (i.e. fixed number of arguments of various types) + * High-level wrappers for CallLuaModuleFunction that pushes either an argument + * vector (i.e. single type variable number) or an argument tuple (i.e. fixed + * number of arguments of various types) */ template bool CallLuaModuleFunction( @@ -559,7 +564,7 @@ namespace DFHack {namespace Lua { Notification(function_identity_base *handler = NULL) : state(NULL), key(NULL), handler(handler), count(0) {} - int get_listener_count() { return count; } + int get_listener_count() const { return count; } lua_State *get_state() { return state; } function_identity_base *get_handler() { return handler; } @@ -572,7 +577,7 @@ namespace DFHack {namespace Lua { void bind(lua_State *state, const char *name); void bind(lua_State *state, void *key); }; -}} +} #define DEFINE_LUA_EVENT_0(name, handler) \ static DFHack::Lua::Notification name##_event(df::wrap_function(handler, true)); \ diff --git a/library/include/LuaWrapper.h b/library/include/LuaWrapper.h index 347945e896..9c9c3d6ee7 100644 --- a/library/include/LuaWrapper.h +++ b/library/include/LuaWrapper.h @@ -24,16 +24,10 @@ distribution. #pragma once -#include -#include -#include -#include +#include #include "DataDefs.h" -#include -#include - /** * Internal header file of the lua wrapper. */ @@ -41,6 +35,9 @@ distribution. namespace DFHack { struct FunctionReg; + + class function_identity_base; + namespace LuaWrapper { struct LuaToken; @@ -70,51 +67,51 @@ namespace LuaWrapper { extern LuaToken DFHACK_PTR_IDTABLE_TOKEN; // Function registry names -#define DFHACK_CHANGEERROR_NAME "DFHack::ChangeError" -#define DFHACK_COMPARE_NAME "DFHack::ComparePtrs" -#define DFHACK_TYPE_TOSTRING_NAME "DFHack::TypeToString" -#define DFHACK_SIZEOF_NAME "DFHack::Sizeof" -#define DFHACK_DISPLACE_NAME "DFHack::Displace" -#define DFHACK_NEW_NAME "DFHack::New" -#define DFHACK_ASSIGN_NAME "DFHack::Assign" -#define DFHACK_IS_INSTANCE_NAME "DFHack::IsInstance" -#define DFHACK_DELETE_NAME "DFHack::Delete" -#define DFHACK_CAST_NAME "DFHack::Cast" + constexpr auto DFHACK_CHANGEERROR_NAME = "DFHack::ChangeError"; + constexpr auto DFHACK_COMPARE_NAME = "DFHack::ComparePtrs"; + constexpr auto DFHACK_TYPE_TOSTRING_NAME = "DFHack::TypeToString"; + constexpr auto DFHACK_SIZEOF_NAME = "DFHack::Sizeof"; + constexpr auto DFHACK_DISPLACE_NAME = "DFHack::Displace"; + constexpr auto DFHACK_NEW_NAME = "DFHack::New"; + constexpr auto DFHACK_ASSIGN_NAME = "DFHack::Assign"; + constexpr auto DFHACK_IS_INSTANCE_NAME = "DFHack::IsInstance"; + constexpr auto DFHACK_DELETE_NAME = "DFHack::Delete"; + constexpr auto DFHACK_CAST_NAME = "DFHack::Cast"; extern LuaToken DFHACK_EMPTY_TABLE_TOKEN; /* * Upvalue: contents of DFHACK_TYPETABLE_NAME */ -#define UPVAL_TYPETABLE lua_upvalueindex(1) + constexpr auto UPVAL_TYPETABLE = lua_upvalueindex(1); /* * Expected metatable of the current object. */ -#define UPVAL_METATABLE lua_upvalueindex(2) + constexpr auto UPVAL_METATABLE = lua_upvalueindex(2); /* * Table mapping field names to indices or data structure pointers. * Enum index table is linked into here via getmetatable($).__index. * Fields that are actually in UPVAL_METATABLE are marked with NULL light udata. */ -#define UPVAL_FIELDTABLE lua_upvalueindex(3) -#define UPVAL_METHOD_NAME lua_upvalueindex(3) + constexpr auto UPVAL_FIELDTABLE = lua_upvalueindex(3); + constexpr auto UPVAL_METHOD_NAME = lua_upvalueindex(3); /* * Only for containers: light udata with container identity. */ -#define UPVAL_CONTAINER_ID lua_upvalueindex(4) + constexpr auto UPVAL_CONTAINER_ID = lua_upvalueindex(4); /* * Only for containers: light udata with item identity. */ -#define UPVAL_ITEM_ID lua_upvalueindex(5) + constexpr auto UPVAL_ITEM_ID = lua_upvalueindex(5); /* * Only for containers: if not nil, overrides the item count. */ -#define UPVAL_ITEM_COUNT lua_upvalueindex(6) + constexpr auto UPVAL_ITEM_COUNT = lua_upvalueindex(6); inline void lua_dup(lua_State *state) { lua_pushvalue(state, -1); } inline void lua_swap(lua_State *state) { lua_insert(state, -2); } @@ -142,13 +139,13 @@ namespace LuaWrapper { /** * Report an error while accessing a field (index = field name). */ - void field_error(lua_State *state, int index, const char *err, const char *mode); + [[noreturn]] DFHACK_EXPORT void field_error(lua_State *state, int index, const char *err, const char *mode); /* * If is_method is true, these use UPVAL_TYPETABLE to save a hash lookup. */ void push_object_internal(lua_State *state, const type_identity *type, void *ptr, bool in_method = true); - void *get_object_internal(lua_State *state, const type_identity *type, int val_index, bool exact_type, bool in_method = true); + DFHACK_EXPORT void *get_object_internal(lua_State *state, const type_identity *type, int val_index, bool exact_type, bool in_method = true); void push_adhoc_pointer(lua_State *state, void *ptr, const type_identity *target); @@ -166,7 +163,7 @@ namespace LuaWrapper { const char *ctx, bool allow_type = false, bool keep_metatable = false); - void LookupInTable(lua_State *state, void *id, LuaToken *tname); + void LookupInTable(lua_State *state, const void *id, LuaToken *tname); void SaveInTable(lua_State *state, void *node, LuaToken *tname); void SaveTypeInfo(lua_State *state, void *node); @@ -231,12 +228,15 @@ namespace LuaWrapper { /** * Wrap functions and add them to the table on the top of the stack. */ - typedef DFHack::FunctionReg FunctionReg; + using DFHack::FunctionReg; + void SetFunctionWrappers(lua_State *state, const FunctionReg *reg); int method_wrapper_core(lua_State *state, function_identity_base *id); void IndexStatics(lua_State *state, int meta_idx, int ftable_idx, struct_identity *pstruct); - void AttachDFGlobals(lua_State *state); + DFHACK_EXPORT void AttachDFGlobals(lua_State *state); + + DFHACK_EXPORT void notify_deprecated(lua_State* state, std::string_view name, std::string_view message); }} diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 083d94434c..5115348ba2 100644 --- a/library/include/MemAccess.h +++ b/library/include/MemAccess.h @@ -29,10 +29,10 @@ distribution. #define PROCESS_H_INCLUDED #include "Export.h" -#include #include #include #include +#include #include "VersionInfo.h" @@ -50,9 +50,9 @@ namespace DFHack */ struct DFHACK_EXPORT t_memrange { - void * base; - void * start; - void * end; + void* base; + void* start; + void* end; // memory range name (if any) char name[1024]; // permission to read @@ -63,7 +63,7 @@ namespace DFHack bool execute : 1; // is a shared region bool shared : 1; - inline bool isInRange( void * address) + inline bool isInRange(void* address) { if (address >= start && address < end) return true; return false; @@ -77,234 +77,242 @@ namespace DFHack */ class DFHACK_EXPORT Process { - public: - /// this is the single most important destructor ever. ~px - Process(const VersionInfoFactory& known_versions); - ~Process(); - /// read a 8-byte integer - uint64_t readQuad(const void * address) - { - return *(uint64_t *)address; - } - /// read a 8-byte integer - void readQuad(const void * address, uint64_t & value) - { - value = *(uint64_t *)address; - }; - /// write a 8-byte integer - void writeQuad(const void * address, const uint64_t value) - { - (*(uint64_t *)address) = value; - }; - - /// read a 4-byte integer - uint32_t readDWord(const void * address) - { - return *(uint32_t *)address; - } - /// read a 4-byte integer - void readDWord(const void * address, uint32_t & value) - { - value = *(uint32_t *)address; - }; - /// write a 4-byte integer - void writeDWord(const void * address, const uint32_t value) - { - (*(uint32_t *)address) = value; - }; - - /// read a pointer - char * readPtr(const void * address) - { - return *(char **)address; - } - /// read a pointer - void readPtr(const void * address, char * & value) - { - value = *(char **)address; - }; - - /// read a float - float readFloat(const void * address) - { - return *(float*)address; - } - /// write a float - void readFloat(const void * address, float & value) - { - value = *(float*)address; - }; - - /// read a 2-byte integer - uint16_t readWord(const void * address) - { - return *(uint16_t *)address; - } - /// read a 2-byte integer - void readWord(const void * address, uint16_t & value) - { - value = *(uint16_t *)address; - }; - /// write a 2-byte integer - void writeWord(const void * address, const uint16_t value) - { - (*(uint16_t *)address) = value; - }; - - /// read a byte - uint8_t readByte(const void * address) - { - return *(uint8_t *)address; - } - /// read a byte - void readByte(const void * address, uint8_t & value) - { - value = *(uint8_t *)address; - }; - /// write a byte - void writeByte(const void * address, const uint8_t value) - { - (*(uint8_t *)address) = value; - }; - - /// read an arbitrary amount of bytes - void read(void * address, uint32_t length, uint8_t* buffer) - { - memcpy(buffer, (void *) address, length); - }; - /// write an arbitrary amount of bytes - void write(void * address, uint32_t length, uint8_t* buffer) - { - memcpy((void *) address, buffer, length); - }; - - /// read an STL string - const std::string readSTLString (void * offset) - { - std::string * str = (std::string *) offset; - return *str; - }; - /// read an STL string - size_t readSTLString (void * offset, char * buffer, size_t bufcapacity) - { - if(!bufcapacity || bufcapacity == 1) - return 0; - std::string * str = (std::string *) offset; - size_t copied = str->copy(buffer,bufcapacity-1); - buffer[copied] = 0; - return copied; - }; - /** - * write an STL string - * @return length written - */ - size_t writeSTLString(const void * address, const std::string writeString) - { - std::string * str = (std::string *) address; - str->assign(writeString); - return writeString.size(); - }; - /** - * attempt to copy a string from source address to target address. may truncate or leak, depending on platform - * @return length copied - */ - size_t copySTLString(const void * address, const uintptr_t target) - { - std::string * strsrc = (std::string *) address; - std::string * str = (std::string *) target; - str->assign(*strsrc); - return str->size(); - } - - /// get class name of an object with rtti/type info - std::string doReadClassName(void * vptr); - - std::string readClassName(void * vptr) - { - std::map::iterator it = classNameCache.find(vptr); - if (it != classNameCache.end()) - return it->second; - return classNameCache[vptr] = doReadClassName(vptr); - } - - /// read a null-terminated C string - const std::string readCString (void * offset) - { - return std::string((char *) offset); - }; - - /// @return true if the process is suspended - bool isSuspended() - { - return true; - }; - /// @return true if the process is identified -- has a symbol table extension - bool isIdentified() - { - return identified; - }; - - /// get virtual memory ranges of the process (what is mapped where) - static void getMemRanges(std::vector & ranges); - - /// get the symbol table extension of this process - std::shared_ptr getDescriptor() - { - return my_descriptor; - }; - - void ValidateDescriptionOS() { - if (my_descriptor) - my_descriptor->ValidateOS(); - }; - - uintptr_t getBase(); - /// get the DF Process ID - int getPID(); - /// get the DF Process FilePath - std::string getPath(); - /// Adjust between in-memory and in-file image offset - int adjustOffset(int offset, bool to_file = false); - - /// millisecond tick count, exactly as DF uses - uint32_t getTickCount(); - - /// modify permisions of memory range - bool setPermisions(const t_memrange & range,const t_memrange &trgrange); - - /// write a possibly read-only memory area - bool patchMemory(void *target, const void* src, size_t count); - - /// allocate new memory pages for code or stuff - /// returns -1 on error (0 is a valid address) - void* memAlloc(const int length); - - /// free memory pages from memAlloc - /// should have length = alloced length for portability - /// returns 0 on success - int memDealloc(void *ptr, const int length); - - /// change memory page permissions - /// prot is a bitwise OR of the MemProt enum - /// returns 0 on success - int memProtect(void *ptr, const int length, const int prot); - - enum MemProt { - READ = 1, - WRITE = 2, - EXEC = 4 - }; - - uint32_t getPE() { return my_pe; } - std::string getMD5() { return my_md5; } + public: + /// this is the single most important destructor ever. ~px + Process(const VersionInfoFactory& known_versions); + ~Process(); + /// read a 8-byte integer + uint64_t readQuad(const void* address) + { + return *(uint64_t*)address; + } + /// read a 8-byte integer + void readQuad(const void* address, uint64_t& value) + { + value = *(uint64_t*)address; + }; + /// write a 8-byte integer + void writeQuad(const void* address, const uint64_t value) + { + (*(uint64_t*)address) = value; + }; + + /// read a 4-byte integer + uint32_t readDWord(const void* address) + { + return *(uint32_t*)address; + } + /// read a 4-byte integer + void readDWord(const void* address, uint32_t& value) + { + value = *(uint32_t*)address; + }; + /// write a 4-byte integer + void writeDWord(const void* address, const uint32_t value) + { + (*(uint32_t*)address) = value; + }; + + /// read a pointer + char* readPtr(const void* address) + { + return *(char**)address; + } + /// read a pointer + void readPtr(const void* address, char*& value) + { + value = *(char**)address; + }; + + /// read a float + float readFloat(const void* address) + { + return *(float*)address; + } + /// write a float + void readFloat(const void* address, float& value) + { + value = *(float*)address; + }; + + /// read a 2-byte integer + uint16_t readWord(const void* address) + { + return *(uint16_t*)address; + } + /// read a 2-byte integer + void readWord(const void* address, uint16_t& value) + { + value = *(uint16_t*)address; + }; + /// write a 2-byte integer + void writeWord(const void* address, const uint16_t value) + { + (*(uint16_t*)address) = value; + }; + + /// read a byte + uint8_t readByte(const void* address) + { + return *(uint8_t*)address; + } + /// read a byte + void readByte(const void* address, uint8_t& value) + { + value = *(uint8_t*)address; + }; + /// write a byte + void writeByte(const void* address, const uint8_t value) + { + (*(uint8_t*)address) = value; + }; + + /// read an arbitrary amount of bytes + void read(void* address, uint32_t length, uint8_t* buffer) + { + memcpy(buffer, (void*)address, length); + }; + /// write an arbitrary amount of bytes + void write(void* address, uint32_t length, uint8_t* buffer) + { + memcpy((void*)address, buffer, length); + }; + + /// read an STL string + const std::string readSTLString(void* offset) + { + std::string* str = (std::string*)offset; + return *str; + }; + /// read an STL string + size_t readSTLString(void* offset, char* buffer, size_t bufcapacity) + { + if (!bufcapacity || bufcapacity == 1) + return 0; + std::string* str = (std::string*)offset; + size_t copied = str->copy(buffer, bufcapacity - 1); + buffer[copied] = 0; + return copied; + }; + /** + * write an STL string + * @return length written + */ + size_t writeSTLString(const void* address, const std::string writeString) + { + std::string* str = (std::string*)address; + str->assign(writeString); + return writeString.size(); + }; + /** + * attempt to copy a string from source address to target address. may truncate or leak, depending on platform + * @return length copied + */ + size_t copySTLString(const void* address, const uintptr_t target) + { + std::string* strsrc = (std::string*)address; + std::string* str = (std::string*)target; + str->assign(*strsrc); + return str->size(); + } + + /// get class name of an object with rtti/type info + std::string doReadClassName(void* vptr); + + std::string readClassName(void* vptr) + { + auto it = classNameCache.find(vptr); + if (it != classNameCache.end()) + return it->second; + return classNameCache[vptr] = doReadClassName(vptr); + } + + /// read a null-terminated C string + const std::string readCString(void* offset) + { + return std::string((char*)offset); + }; + + /// @return true if the process is suspended + bool isSuspended() + { + return true; + }; + /// @return true if the process is identified -- has a symbol table extension + bool isIdentified() + { + return identified; + }; + + /// get virtual memory ranges of the process (what is mapped where) + static void getMemRanges(std::vector& ranges); + + /// check if an address has a mapping + bool checkValidAddress(void* ptr); + + /// get the symbol table extension of this process + std::shared_ptr getDescriptor() + { + return my_descriptor; + }; + + void ValidateDescriptionOS() + { + if (my_descriptor) + my_descriptor->ValidateOS(); + }; + + uintptr_t getBase(); + /// get the DF Process ID + int getPID(); + /// get the DF Process FilePath + std::filesystem::path getPath(); + /// Adjust between in-memory and in-file image offset + int adjustOffset(int offset, bool to_file = false); + + /// millisecond tick count, exactly as DF uses + uint32_t getTickCount(); + + /// modify permisions of memory range + bool setPermissions(const t_memrange& range, const t_memrange& trgrange); + + /// write a possibly read-only memory area + bool patchMemory(void* target, const void* src, size_t count); + + /// flush cache + bool flushCache(const void* target, size_t count); + + /// allocate new memory pages for code or stuff + /// returns -1 on error (0 is a valid address) + void* memAlloc(const int length); + + /// free memory pages from memAlloc + /// should have length = alloced length for portability + /// returns 0 on success + int memDealloc(void* ptr, const int length); + + /// change memory page permissions + /// prot is a bitwise OR of the MemProt enum + /// returns 0 on success + int memProtect(void* ptr, const int length, const int prot); + + enum MemProt + { + READ = 1, + WRITE = 2, + EXEC = 4 + }; + + uint32_t getPE() { return my_pe; } + std::string getMD5() { return my_md5; } private: std::shared_ptr my_descriptor; - PlatformSpecific *d; + PlatformSpecific* d; bool identified; uint32_t my_pid; uint32_t base; - std::map classNameCache; + std::map classNameCache; uint32_t my_pe; std::string my_md5; }; @@ -312,36 +320,21 @@ namespace DFHack class DFHACK_EXPORT ClassNameCheck { std::string name; - mutable void * vptr; + mutable void* vptr; public: ClassNameCheck() : vptr(0) {} ClassNameCheck(std::string _name); - ClassNameCheck &operator= (const ClassNameCheck &b); + ClassNameCheck& operator= (const ClassNameCheck& b); // Is the class name of the given virtual table pointer the same as the // name for thei ClassNameCheck object? - bool operator() (Process *p, void * ptr) const; + bool operator() (Process* p, void* ptr) const; // Get list of names given to ClassNameCheck constructors. - static void getKnownClassNames(std::vector &names); + static void getKnownClassNames(std::vector& names); }; - class DFHACK_EXPORT MemoryPatcher - { - Process *p; - std::vector ranges, save; - public: - MemoryPatcher(Process *p = NULL); - ~MemoryPatcher(); - - bool verifyAccess(void *target, size_t size, bool write = false); - bool makeWritable(void *target, size_t size) { - return verifyAccess(target, size, true); - } - bool write(void *target, const void *src, size_t size); - - void close(); - }; } + #endif diff --git a/library/include/MemoryPatcher.h b/library/include/MemoryPatcher.h new file mode 100644 index 0000000000..8577a762ef --- /dev/null +++ b/library/include/MemoryPatcher.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "MemAccess.h" + +namespace DFHack { + + class Process; + + class MemoryPatcher { + public: + explicit MemoryPatcher(Process *p_ = nullptr); + ~MemoryPatcher(); + + // Ensure the target memory region is accessible and optionally writable. + // If `write` is true this will attempt to make the pages writable. + bool verifyAccess(void *target, size_t count, bool write); + + // Write `size` bytes from `src` to `target`. Returns true on success. + bool write(void *target, const void *src, size_t size); + + // Restore any modified permissions and clear internal state. + void close(); + + bool makeWritable(void* target, size_t size) + { + return verifyAccess(target, size, true); + } + private: + Process* p; + std::vector ranges, save; + }; + +} diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 8568a55c0a..2676bb3d49 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -33,7 +33,6 @@ distribution. #include #include #include -#include #include #include #include @@ -309,24 +308,43 @@ Link *linked_list_insert_after(Link *pos, Link *link) } /** - * Returns true if the item with id idToRemove was found, deleted, and removed - * from the list. Otherwise returns false. + * Returns true if an item that matches the given function was found, deleted, + * and removed from the list. Otherwise returns false. Only removes the first + * match. + * + * Example usage: + * + * linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + * if (proj->getType() != df::enums::projectile_type::Unit) + * return false; + * if (auto unit_proj = virtual_cast(proj)) + * return unit_proj->unit == unit; + * return false; + * }); */ -template -bool linked_list_remove(Link *head, int32_t idToRemove) -{ - for (Link *link = head; link; link = link->next) - { - if (!link->item || link->item->id != idToRemove) - continue; - - link->prev->next = link->next; - if (link->next) - link->next->prev = link->prev; - delete(link); - return true; - } - return false; +template F> +bool linked_list_remove(L *list, F matches) { + auto matches_wrapper = [&](L::iterator::value_type item) { + return item && matches(item); + }; + typename L::const_iterator it = std::find_if(list->cbegin(), list->cend(), matches_wrapper); + if (it == list->cend()) + return false; + auto item = *it; + list->erase(it); + delete item; + return true; +} + +/** + * Returns true if the item with id idToRemove was found, deleted, and + * removed from the list. Otherwise returns false. + */ +template +bool linked_list_remove(L *list, int32_t idToRemove) { + return linked_list_remove(list, [&](L::iterator::value_type item) { + return item->id == idToRemove; + }); } template @@ -381,7 +399,7 @@ typename T::mapped_type findPrefixInMap( #endif template -inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT::mapped_type value) { +inline bool static_add_to_map(CT *pmap, const typename CT::key_type key, const typename CT::mapped_type value) { (*pmap)[key] = value; return true; } @@ -395,6 +413,42 @@ inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT:: /* * MISC */ +template< + std::ranges::input_range Range, + std::invocable> Callable> +void print_range( + std::ostream &out, + const Range &elements, + Callable&& print_element, + const std::string_view prefix = "[", + const std::string_view separator = ", ", + const std::string_view suffix = "]" +){ + out << prefix; + auto it = std::ranges::begin(elements); + auto end = std::ranges::end(elements); + + if (it != end) { + print_element(out, *it); + for (++it; it != end; ++it) { + out << separator; + print_element(out, *it); + } + } + out << suffix; +} + +template +void print_range( + std::ostream &out, + const Range& elements, + const std::string_view prefix = "[", + const std::string_view separator = ", ", + const std::string_view suffix = "]" +){ + auto print_element = [](std::ostream &out, auto& e) { out << e; }; + print_range(out, elements, print_element, prefix, separator, suffix); +} DFHACK_EXPORT bool split_string(std::vector *out, const std::string &str, const std::string &separator, @@ -456,6 +510,19 @@ static inline std::string &trim(std::string &s) { return ltrim(rtrim(s)); } +static inline bool has_backslashes(const std::string_view str) +{ + return (str.find('\\') != std::string::npos); +} + +static inline void replace_backslashes_with_forwardslashes(std::string& str) +{ + for (auto& c : str) { + if (c == '\\') + c = '/'; + } +} + enum struct NumberFormatType : int32_t { DEFAULT = 0, ENGLISH, diff --git a/library/include/Module.h b/library/include/Module.h deleted file mode 100644 index 7770e13970..0000000000 --- a/library/include/Module.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_H_INCLUDED -#define MODULE_H_INCLUDED - -#include "Export.h" -namespace DFHack -{ - /** - * The parent class for all DFHack modules - * \ingroup grp_modules - */ - class DFHACK_EXPORT Module - { - public: - virtual ~Module(){}; - virtual bool Start(){return true;};// default start... - virtual bool Finish() = 0;// everything should have a Finish() - /* - // should Context call Finish when Resume is called? - virtual bool OnResume() - { - Finish(); - return true; - }; - // Finish when map change is detected? - // TODO: implement - virtual bool OnMapChange() - { - return false; - }; - */ - }; -} -#endif //MODULE_H_INCLUDED diff --git a/library/include/ModuleFactory.h b/library/include/ModuleFactory.h deleted file mode 100644 index c99e7b3289..0000000000 --- a/library/include/ModuleFactory.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once - -#ifndef MODULE_FACTORY_H_INCLUDED -#define MODULE_FACTORY_H_INCLUDED - -#include - -namespace DFHack -{ - class Module; - std::unique_ptr createMaterials(); - std::unique_ptr createGraphic(); -} -#endif diff --git a/library/include/PluginLua.h b/library/include/PluginLua.h index 16d447b7bf..f04262c4d7 100644 --- a/library/include/PluginLua.h +++ b/library/include/PluginLua.h @@ -14,6 +14,6 @@ DFhackCExport const DFHack::EventReg plugin_lua_events[] = #define DFHACK_LUA_COMMAND(name) { #name, name } -#define DFHACK_LUA_FUNCTION(name) { #name, df::wrap_function(name,true) } +#define DFHACK_LUA_FUNCTION(name) { #name, df::wrap_function(name,#name,true) } #define DFHACK_LUA_EVENT(name) { #name, &name##_event } #define DFHACK_LUA_END { NULL, NULL } diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 3749ff9548..1eccc7a79a 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -25,7 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Hooks.h" #include "ColorText.h" #include "MiscUtils.h" #include @@ -64,7 +63,7 @@ namespace DFHack // DFLibrary* that can be used to resolve global names extern DFLibrary* GLOBAL_NAMES; // Open a plugin library - DFHACK_EXPORT DFLibrary * OpenPlugin (const char * filename); + DFHACK_EXPORT DFLibrary * OpenPlugin (std::filesystem::path filename); // find a symbol inside plugin DFHACK_EXPORT void * LookupPlugin (DFLibrary * plugin ,const char * function); // Close a plugin library. returns true on success, false on failure @@ -143,7 +142,7 @@ namespace DFHack struct RefAutoinc; friend class PluginManager; friend class RPCService; - Plugin(DFHack::Core* core, const std::string& filepath, + Plugin(DFHack::Core* core, const std::filesystem::path& filepath, const std::string &plug_name, PluginManager * pm); ~Plugin(); command_result on_update(color_ostream &out); @@ -202,7 +201,7 @@ namespace DFHack RefLock * access; std::vector commands; std::vector services; - std::string path; + std::filesystem::path path; std::string name; DFLibrary * plugin_lib; PluginManager * parent; diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index 8a5bee86f7..bade25b521 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -211,6 +211,7 @@ namespace DFHack functions.push_back(new VoidServerMethod(this, name, flags, fptr)); } + public: void dumpMethods(std::ostream & out) const; }; diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h index 38c234c5fb..8731ad3474 100644 --- a/library/include/RemoteTools.h +++ b/library/include/RemoteTools.h @@ -74,7 +74,7 @@ namespace DFHack */ template void flagarray_to_ints(RepeatedField *pf, const BitArray &val) { - for (size_t i = 0; i < val.size*8; i++) + for (size_t i = 0; i < size_t(val.size()*8); i++) if (val.is_set(T(i))) pf->Add(i); } diff --git a/library/include/Types.h b/library/include/Types.h index 53b6aee18a..17114faf6d 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -26,14 +26,16 @@ distribution. #pragma once #include +#include +#include #include "Export.h" -#include "DataDefs.h" - +#include "df/coord.h" +#include "df/coord2d.h" #include "df/general_ref_type.h" +#include "df/global_objects.h" #include "df/specific_ref_type.h" -#include "df/language_name_type.h" namespace df { struct building; @@ -47,16 +49,16 @@ namespace DFHack { int16_t type; int32_t index; - bool operator<(const t_matglossPair &b) const + bool operator<(const t_matglossPair& b) const { if (type != b.type) return (type < b.type); return (index < b.index); } - bool operator==(const t_matglossPair &b) const + bool operator==(const t_matglossPair& b) const { return (type == b.type) && (index == b.index); } - bool operator!=(const t_matglossPair &b) const + bool operator!=(const t_matglossPair& b) const { return (type != b.type) || (index != b.index); } @@ -95,10 +97,14 @@ namespace DFHack std::string name; uint32_t xpNxtLvl; }; +} - typedef std::pair rect2d; +namespace DFHack +{ + using rect2d = std::pair; - inline rect2d intersect(rect2d a, rect2d b) { + inline rect2d intersect(rect2d a, rect2d b) + { df::coord2d g1 = a.first, g2 = a.second; df::coord2d c1 = b.first, c2 = b.second; df::coord2d rc1 = df::coord2d(std::max(g1.x, c1.x), std::max(g1.y, c1.y)); @@ -106,28 +112,70 @@ namespace DFHack return rect2d(rc1, rc2); } - inline rect2d mkrect_xy(int x1, int y1, int x2, int y2) { + inline rect2d mkrect_xy(int x1, int y1, int x2, int y2) + { return rect2d(df::coord2d(x1, y1), df::coord2d(x2, y2)); } - inline rect2d mkrect_wh(int x, int y, int w, int h) { - return rect2d(df::coord2d(x, y), df::coord2d(x+w-1, y+h-1)); + inline rect2d mkrect_wh(int x, int y, int w, int h) + { + return rect2d(df::coord2d(x, y), df::coord2d(x + w - 1, y + h - 1)); } - inline df::coord2d rect_size(const rect2d &rect) { - return rect.second - rect.first + df::coord2d(1,1); + inline df::coord2d rect_size(const rect2d& rect) + { + return rect.second - rect.first + df::coord2d(1, 1); } - DFHACK_EXPORT int getdir(std::string dir, std::vector &files); - DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending); + DFHACK_EXPORT int getdir(std::filesystem::path dir, std::vector& files); + DFHACK_EXPORT bool hasEnding(std::string const& fullString, std::string const& ending); + + DFHACK_EXPORT df::general_ref* findRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT bool removeRef(std::vector& vec, df::general_ref_type type, int id); + + DFHACK_EXPORT df::item* findItemRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT df::building* findBuildingRef(std::vector& vec, df::general_ref_type type); + DFHACK_EXPORT df::unit* findUnitRef(std::vector& vec, df::general_ref_type type); + + DFHACK_EXPORT df::specific_ref* findRef(std::vector& vec, df::specific_ref_type type); + DFHACK_EXPORT bool removeRef(std::vector& vec, df::specific_ref_type type, void* ptr); + +} + +// A couple of headers that have to be included at once +#include "df/coord2d.h" +#include "df/coord.h" - DFHACK_EXPORT df::general_ref *findRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT bool removeRef(std::vector &vec, df::general_ref_type type, int id); +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const df::coord& c) const + { + return c(); + } + }; +} - DFHACK_EXPORT df::item *findItemRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT df::building *findBuildingRef(std::vector &vec, df::general_ref_type type); - DFHACK_EXPORT df::unit *findUnitRef(std::vector &vec, df::general_ref_type type); +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", c.x, c.y, c.z), ctx); + } +}; - DFHACK_EXPORT df::specific_ref *findRef(std::vector &vec, df::specific_ref_type type); - DFHACK_EXPORT bool removeRef(std::vector &vec, df::specific_ref_type type, void *ptr); -}// namespace DFHack +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const df::coord2d& c, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {})", c.x, c.y), ctx); + } +}; diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h index 7575ee18c2..9dff74d7e5 100644 --- a/library/include/VTableInterpose.h +++ b/library/include/VTableInterpose.h @@ -25,7 +25,8 @@ distribution. #pragma once #include "DataDefs.h" -#include "DataIdentity.h" + +#include namespace DFHack { diff --git a/library/include/VersionInfoFactory.h b/library/include/VersionInfoFactory.h index 060d622ecd..92a5f94e10 100644 --- a/library/include/VersionInfoFactory.h +++ b/library/include/VersionInfoFactory.h @@ -26,6 +26,7 @@ distribution. #pragma once #include +#include #include "Export.h" @@ -38,7 +39,7 @@ namespace DFHack public: VersionInfoFactory(); ~VersionInfoFactory(); - bool loadFile( std::string path_to_xml); + bool loadFile( std::filesystem::path path_to_xml); bool isInErrorState() const {return error;}; std::shared_ptr getVersionInfoByMD5(std::string md5string) const; std::shared_ptr getVersionInfoByPETimestamp(uintptr_t timestamp) const; diff --git a/library/include/df/custom/.gitignore b/library/include/df/custom/.gitignore new file mode 100644 index 0000000000..96e79c167d --- /dev/null +++ b/library/include/df/custom/.gitignore @@ -0,0 +1 @@ +!*.h diff --git a/library/include/df/custom/coord.methods.inc b/library/include/df/custom/coord.methods.inc index 77dbb766e3..9f1eede717 100644 --- a/library/include/df/custom/coord.methods.inc +++ b/library/include/df/custom/coord.methods.inc @@ -1,5 +1,5 @@ -coord(const coord2d &c, uint16_t _z) : x(c.x), y(c.y), z(_z) {} -coord(uint16_t _x, uint16_t _y, uint16_t _z) : x(_x), y(_y), z(_z) {} +coord(const coord2d &c, int16_t _z) : x(c.x), y(c.y), z(_z) {} +coord(int16_t _x, int16_t _y, int16_t _z) : x(_x), y(_y), z(_z) {} operator coord2d() const { return coord2d(x,y); } diff --git a/library/include/df/custom/coord2d.methods.inc b/library/include/df/custom/coord2d.methods.inc index 512149ce36..c828b0c47e 100644 --- a/library/include/df/custom/coord2d.methods.inc +++ b/library/include/df/custom/coord2d.methods.inc @@ -1,4 +1,4 @@ -coord2d(uint16_t _x, uint16_t _y) : x(_x), y(_y) {} +coord2d(int16_t _x, int16_t _y) : x(_x), y(_y) {} bool isValid() const { return x >= 0; } void clear() { x = y = -30000; } diff --git a/library/include/df/custom/creature_handler.methods.inc b/library/include/df/custom/creature_handler.methods.inc index 811128d058..14639bd889 100644 --- a/library/include/df/custom/creature_handler.methods.inc +++ b/library/include/df/custom/creature_handler.methods.inc @@ -1 +1 @@ -friend struct world_raws; +friend struct world; diff --git a/library/include/df/custom/hash/labor_kitchen_interface_food_key.h b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h new file mode 100644 index 0000000000..5f2d0d2b33 --- /dev/null +++ b/library/include/df/custom/hash/labor_kitchen_interface_food_key.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include "df/labor_kitchen_interface_food_key.h" + +namespace std +{ + template<> + struct hash + { + auto operator()(const df::labor_kitchen_interface_food_key& a) const -> size_t + { + struct thing + { + int16_t t; + int16_t st; + int32_t x; + } thing{ + .t = a.type, + .st = a.subtype, + .x = static_cast(a.mat) ^ (static_cast(a.matg)) + }; + + return hash()(std::bit_cast(thing)); + } + }; +} diff --git a/library/include/df/custom/hash/texture_fullid.h b/library/include/df/custom/hash/texture_fullid.h new file mode 100644 index 0000000000..a1e4606e33 --- /dev/null +++ b/library/include/df/custom/hash/texture_fullid.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include "df/texture_fullid.h" + +template<> +struct std::hash +{ + size_t operator()(df::texture_fullid const &t) const noexcept + { +// for some reason, bay12 used different hash methods on windows vs linux +#ifdef WIN32 + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(std::bit_cast(std::make_pair(t.r, t.g))); + h^=u_hash(std::bit_cast(std::make_pair(t.b, t.br)))<<1; + h^=u_hash(std::bit_cast(std::make_pair(t.bg, t.bb)))<<2; + h^=std::hash{}(t.flag.whole); + return h; +#else + size_t h=std::hash{}(t.texpos); + auto u_hash=std::hash{}; + h^=u_hash(t.r); + h^=u_hash(t.g)<<1; + h^=u_hash(t.b)<<2; + h^=u_hash(t.br)<<3; + h^=u_hash(t.bg)<<4; + h^=u_hash(t.bb)<<5; + h^=std::hash{}(t.flag.whole)<<6; + return h; +#endif + } +}; diff --git a/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc new file mode 100644 index 0000000000..a1de2fec13 --- /dev/null +++ b/library/include/df/custom/labor_kitchen_interface_food_key.methods.inc @@ -0,0 +1,11 @@ + bool operator<(const df::labor_kitchen_interface_food_key &b) const { + if (typetexpos == other.texpos && + this->r == other.r && + this->g == other.g && + this->b == other.b && + this->br == other.br && + this->bg == other.bg && + this->bb == other.bb && + this->flag.whole == other.flag.whole + ); +} + +auto operator< (const texture_fullid &other) const { + if (this->texpos < other.texpos) return true; + if (this->r < other.r) return true; + if (this->g < other.g) return true; + if (this->b < other.b) return true; + if (this->br < other.br) return true; + if (this->bg < other.bg) return true; + if (this->bb < other.bb) return true; + if (this->flag.whole < other.flag.whole) return true; + return false; +} diff --git a/library/include/df/custom/tile_bitmask.methods.inc b/library/include/df/custom/tile_bitmask.methods.inc index b991819b94..ade00e7218 100644 --- a/library/include/df/custom/tile_bitmask.methods.inc +++ b/library/include/df/custom/tile_bitmask.methods.inc @@ -6,11 +6,11 @@ inline uint16_t &operator[] (int y) } void clear() { - memset(bits,0,sizeof(bits)); + bits.fill(0); } void set_all() { - memset(bits,0xFF,sizeof(bits)); + bits.fill(-1); } inline bool getassignment( const df::coord2d &xy ) { diff --git a/library/include/git-describe.h.in b/library/include/git-describe.h.in index 761fe4ca5f..60dd514887 100644 --- a/library/include/git-describe.h.in +++ b/library/include/git-describe.h.in @@ -1,7 +1,7 @@ -#cmakedefine DFHACK_GIT_DESCRIPTION "@DFHACK_GIT_DESCRIPTION@" -#cmakedefine DFHACK_GIT_COMMIT "@DFHACK_GIT_COMMIT@" -#cmakedefine DFHACK_GIT_XML_EXPECTED_COMMIT "@DFHACK_GIT_XML_EXPECTED_COMMIT@" -#cmakedefine DFHACK_GIT_XML_COMMIT "@DFHACK_GIT_XML_COMMIT@" +#define DFHACK_GIT_DESCRIPTION "@DFHACK_GIT_DESCRIPTION@" +#define DFHACK_GIT_COMMIT "@DFHACK_GIT_COMMIT@" +#define DFHACK_GIT_XML_EXPECTED_COMMIT "@DFHACK_GIT_XML_EXPECTED_COMMIT@" +#define DFHACK_GIT_XML_COMMIT "@DFHACK_GIT_XML_COMMIT@" #define DFHACK_BUILD_ID "@DFHACK_BUILD_ID@" #cmakedefine DFHACK_GIT_TAGGED #cmakedefine DFHACK_GIT_XML_MATCH diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index ee7fcdf698..1a37cb8304 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -44,7 +44,6 @@ namespace df { struct building; struct building_cagest; struct building_civzonest; - struct building_extents; struct building_stockpilest; struct item; struct job_item; @@ -74,6 +73,11 @@ DFHACK_EXPORT bool ReadCustomWorkshopTypes(std::map & bt DFHACK_EXPORT df::general_ref *getGeneralRef(df::building *building, df::general_ref_type type); DFHACK_EXPORT df::specific_ref *getSpecificRef(df::building *building, df::specific_ref_type type); +/** + * Gets the owner unit for the zone. Uses the cached index in the civzone if valid, updates if not + */ +DFHACK_EXPORT df::unit* getOwner(df::building_civzonest* building); + /** * Sets the owner unit for the zone. */ @@ -112,7 +116,7 @@ DFHACK_EXPORT bool getCorrectSize(df::coord2d &size, df::coord2d ¢er, * Checks if the tiles are free to be built upon. */ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext = NULL, + df::building *bld = nullptr, bool create_ext = false, bool allow_occupied = false, bool allow_wall = false, @@ -121,7 +125,7 @@ DFHACK_EXPORT bool checkFreeTiles(df::coord pos, df::coord2d size, /** * Returns the number of tiles included by the extent, or defval. */ -DFHACK_EXPORT int countExtentTiles(df::building_extents *ext, int defval = -1); +DFHACK_EXPORT int countExtentTiles(df::building *bld, int defval = -1); /** * Checks if the building contains the specified tile. If the building has diff --git a/library/include/modules/Burrows.h b/library/include/modules/Burrows.h index b2e3e56b4e..2662826aad 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -23,11 +23,15 @@ distribution. */ #pragma once + +#include + #include "Export.h" #include "DataDefs.h" #include "modules/Maps.h" -#include +#include "df/coord.h" +#include "df/coord2d.h" /** * \defgroup grp_burrows Burrows module and its types @@ -45,6 +49,8 @@ namespace DFHack { namespace Burrows { + DFHACK_EXPORT std::string getName(df::burrow* burrow); + DFHACK_EXPORT df::burrow *findByName(std::string name, bool ignore_final_plus = false); // Units diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 0fcd17b322..95837b6889 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,26 +1,21 @@ #pragma once -#include "Error.h" -#include "Export.h" #include "ColorText.h" +#include "Export.h" +#include "PluginManager.h" +#include +#include +#include #include struct SDL_Surface; struct SDL_Rect; +struct SDL_Renderer; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; - -namespace DFHack -{ - struct DFTileSurface - { - bool paintOver; // draw over original tile? - SDL_Surface* surface; // from where it should be drawn - SDL_Rect* rect; // from which coords (NULL to draw whole surface) - SDL_Rect* dstResize; // if not NULL dst rect will be resized (x/y/w/h will be added to original dst) - }; +using SDL_Keycode = int32_t; /** * The DFSDL module - provides access to SDL functions without actually @@ -28,47 +23,68 @@ namespace DFHack * \ingroup grp_modules * \ingroup grp_dfsdl */ -namespace DFSDL +namespace DFHack::DFSDL { + /** + * Call this on DFHack init so we can load the SDL functions. Returns false on + * failure. + */ + bool init(DFHack::color_ostream& out); -/** - * Call this on DFHack init so we can load the SDL functions. Returns false on - * failure. - */ -bool init(DFHack::color_ostream &out); + /** + * Call this when DFHack is being unloaded. + */ + void cleanup(); -/** - * Call this when DFHack is being unloaded. - */ -void cleanup(); + /** + * Obtain DFHack's handle to the SDL or IMG libraries, in case a plugin needs + * to map a SDL API not mapped here + */ + DFHACK_EXPORT DFLibrary* obtain_library_handle(); + DFHACK_EXPORT DFLibrary* obtain_image_library_handle(); + + DFHACK_EXPORT SDL_Surface* DFIMG_Load(const char* file); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceFrom(void* pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); + DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface* src, const SDL_Rect* srcrect, SDL_Surface* dst, SDL_Rect* dstrect); + DFHACK_EXPORT SDL_Surface* DFSDL_ConvertSurface(SDL_Surface* src, const SDL_PixelFormat* fmt, uint32_t flags); + DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface* surface); + // DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); + // DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); + DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event* event); + DFHACK_EXPORT void DFSDL_free(void* ptr); + DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); + DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); + DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); + + DFHACK_EXPORT uint32_t DFSDL_GetMouseState(int* x, int* y); + DFHACK_EXPORT void DFSDL_RenderWindowToLogical(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); + DFHACK_EXPORT void DFSDL_RenderLogicalToWindow(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); -DFHACK_EXPORT SDL_Surface * DFIMG_Load(const char *file); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT SDL_Surface * DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFHACK_EXPORT int DFSDL_UpperBlit(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect); -DFHACK_EXPORT SDL_Surface * DFSDL_ConvertSurface(SDL_Surface *src, const SDL_PixelFormat *fmt, uint32_t flags); -DFHACK_EXPORT void DFSDL_FreeSurface(SDL_Surface *surface); -// DFHACK_EXPORT int DFSDL_SemWait(SDL_sem *sem); -// DFHACK_EXPORT int DFSDL_SemPost(SDL_sem *sem); -DFHACK_EXPORT int DFSDL_PushEvent(SDL_Event *event); -DFHACK_EXPORT void DFSDL_free(void *ptr); -DFHACK_EXPORT SDL_PixelFormat* DFSDL_AllocFormat(uint32_t pixel_format); -DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); -DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window); + // submitted and returned text is UTF-8 + // see wrapper functions below for cp-437 variants + DFHACK_EXPORT char* DFSDL_GetClipboardText(); + DFHACK_EXPORT int DFSDL_SetClipboardText(const char* text); -// submitted and returned text is UTF-8 -// see wrapper functions below for cp-437 variants -DFHACK_EXPORT char * DFSDL_GetClipboardText(); -DFHACK_EXPORT int DFSDL_SetClipboardText(const char *text); + DFHACK_EXPORT char* DFSDL_GetPrefPath(const char* org, const char* app); + DFHACK_EXPORT char* DFSDL_GetBasePath(); + DFHACK_EXPORT SDL_Keycode DFSDL_GetKeyFromName(const char* name); + DFHACK_EXPORT const char* DFSDL_GetKeyName(SDL_Keycode key); } -// System clipboard -- submitted and returned text must be in CP437 -DFHACK_EXPORT std::string getClipboardTextCp437(); -DFHACK_EXPORT bool setClipboardTextCp437(std::string text); +namespace DFHack +{ + + // System clipboard -- submitted and returned text must be in CP437 + DFHACK_EXPORT std::string getClipboardTextCp437(); + DFHACK_EXPORT bool setClipboardTextCp437(std::string text); -// interprets 0xa as newline instead of usual CP437 char -DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); -DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // interprets 0xa as newline instead of usual CP437 char + DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); + DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // Queue a cb to be run on the render thread + DFHACK_EXPORT void runOnRenderThread(std::function cb); + DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/include/modules/DFSteam.h b/library/include/modules/DFSteam.h index e604294f8a..7080a94e2e 100644 --- a/library/include/modules/DFSteam.h +++ b/library/include/modules/DFSteam.h @@ -24,7 +24,7 @@ bool init(DFHack::color_ostream& out); /** * Call this when DFHack is being unloaded. */ -void cleanup(DFHack::color_ostream& out); +void cleanup(); DFHACK_EXPORT void launchSteamDFHackIfNecessary(DFHack::color_ostream& out); diff --git a/library/include/modules/Designations.h b/library/include/modules/Designations.h index 914605e58a..5eb0e7cb86 100644 --- a/library/include/modules/Designations.h +++ b/library/include/modules/Designations.h @@ -1,5 +1,7 @@ #pragma once +#include "df/coord.h" + namespace df { struct plant; } diff --git a/library/include/modules/EventManager.h b/library/include/modules/EventManager.h index e0a74bed92..a2c546a755 100644 --- a/library/include/modules/EventManager.h +++ b/library/include/modules/EventManager.h @@ -1,10 +1,8 @@ #pragma once -#include "Core.h" #include "Export.h" #include "ColorText.h" #include "PluginManager.h" -#include "Console.h" #include "DataDefs.h" #include "df/unit_inventory_item.h" diff --git a/library/include/modules/Filesystem.h b/library/include/modules/Filesystem.h index 8e7bab9b20..68da9ec7f6 100644 --- a/library/include/modules/Filesystem.h +++ b/library/include/modules/Filesystem.h @@ -50,123 +50,43 @@ SOFTWARE. #include #include -#ifndef _WIN32 - #ifndef _AIX - #define _FILE_OFFSET_BITS 64 /* Linux, Solaris and HP-UX */ - #else - #define _LARGE_FILES 1 /* AIX */ - #endif -#endif +#include -#ifndef _LARGEFILE64_SOURCE - #define _LARGEFILE64_SOURCE -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 - #include - #define NOMINMAX - #include - #include - #include - #ifdef __BORLANDC__ - #include - #else - #include - #endif - #include - #include "wdirent.h" -#else - #include - #include - #include - #include - #include -#endif - -#ifdef _WIN32 - #ifdef __BORLANDC__ - #define lfs_setmode(L,file,m) ((void)L, setmode(_fileno(file), m)) - #define STAT_STRUCT struct stati64 - #else - #define lfs_setmode(L,file,m) ((void)L, _setmode(_fileno(file), m)) - #define STAT_STRUCT struct _stati64 - #endif - #define STAT_FUNC _stati64 - #define LSTAT_FUNC STAT_FUNC -#else - #define _O_TEXT 0 - #define _O_BINARY 0 - #define lfs_setmode(L,file,m) ((void)L, (void)file, (void)m, 0) - #define STAT_STRUCT struct stat - #define STAT_FUNC stat - #define LSTAT_FUNC lstat -#endif - -#ifdef _WIN32 - #ifndef S_ISDIR - #define S_ISDIR(mode) (mode&_S_IFDIR) - #endif - #ifndef S_ISREG - #define S_ISREG(mode) (mode&_S_IFREG) - #endif - #ifndef S_ISLNK - #define S_ISLNK(mode) (0) - #endif - #ifndef S_ISSOCK - #define S_ISSOCK(mode) (0) - #endif - #ifndef S_ISCHR - #define S_ISCHR(mode) (mode&_S_IFCHR) - #endif - #ifndef S_ISBLK - #define S_ISBLK(mode) (0) - #endif -#endif - -enum _filetype { - FILETYPE_NONE = -2, - FILETYPE_UNKNOWN = -1, - FILETYPE_FILE = 1, - FILETYPE_DIRECTORY, - FILETYPE_LINK, - FILETYPE_SOCKET, - FILETYPE_NAMEDPIPE, - FILETYPE_CHAR_DEVICE, - FILETYPE_BLOCK_DEVICE -}; namespace DFHack { namespace Filesystem { - DFHACK_EXPORT void init (); - DFHACK_EXPORT bool chdir (std::string path); - DFHACK_EXPORT std::string getcwd (); - DFHACK_EXPORT bool restore_cwd (); - DFHACK_EXPORT std::string get_initial_cwd (); - DFHACK_EXPORT bool mkdir (std::string path); + DFHACK_EXPORT void init(); + DFHACK_EXPORT bool chdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::filesystem::path getcwd(); + DFHACK_EXPORT bool restore_cwd(); + DFHACK_EXPORT std::filesystem::path get_initial_cwd(); + DFHACK_EXPORT bool mkdir(std::filesystem::path path) noexcept; // returns true on success or if directory already exists - DFHACK_EXPORT bool mkdir_recursive (std::string path); - DFHACK_EXPORT bool rmdir (std::string path); - DFHACK_EXPORT bool stat (std::string path, STAT_STRUCT &info); - DFHACK_EXPORT bool exists (std::string path); - DFHACK_EXPORT _filetype filetype (std::string path); - DFHACK_EXPORT bool isfile (std::string path); - DFHACK_EXPORT bool isdir (std::string path); - DFHACK_EXPORT int64_t atime (std::string path); - DFHACK_EXPORT int64_t ctime (std::string path); - DFHACK_EXPORT int64_t mtime (std::string path); - DFHACK_EXPORT int listdir (std::string dir, std::vector &files); + DFHACK_EXPORT bool mkdir_recursive(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool rmdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool stat(std::filesystem::path path, std::filesystem::file_status& info) noexcept; + DFHACK_EXPORT bool exists(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isfile(std::filesystem::path path) noexcept; + DFHACK_EXPORT bool isdir(std::filesystem::path path) noexcept; + DFHACK_EXPORT std::time_t mtime(std::filesystem::path path) noexcept; + DFHACK_EXPORT int listdir(std::filesystem::path dir, std::vector& files) noexcept; // set include_prefix to false to prevent dir from being prepended to // paths returned in files - DFHACK_EXPORT int listdir_recursive (std::string dir, std::map &files, - int depth = 10, bool include_prefix = true); + DFHACK_EXPORT int listdir_recursive(std::filesystem::path dir, std::map& files, + int depth = 10, bool include_prefix = true) noexcept; + DFHACK_EXPORT std::filesystem::path canonicalize(std::filesystem::path p) noexcept; + inline std::string as_string(const std::filesystem::path path) noexcept + { + // this just mashes the utf-8 into a std::string without any conversion + // this is largely because we use utf-8 everywhere internally as much as we can + // but we should ultimately convert to using u8strings for strings that are utf-8 + // and use a different string type for strings encoded in cp437 or in the locale codepage + std::u8string pstr = path.generic_u8string(); + return std::string((char*)pstr.c_str()); + + } + DFHACK_EXPORT std::filesystem::path getInstallDir() noexcept; + DFHACK_EXPORT std::filesystem::path getBaseDir() noexcept; + } } diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h deleted file mode 100644 index 9fa498a7cf..0000000000 --- a/library/include/modules/Graphic.h +++ /dev/null @@ -1,59 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -/******************************************************************************* - GRAPHIC - Changing tile cache -*******************************************************************************/ -#pragma once -#ifndef CL_MOD_GRAPHIC -#define CL_MOD_GRAPHIC - -#include -#include "Export.h" -#include "Module.h" -#include - -namespace DFHack -{ - // forward declaration used here instead of including DFSDL.h to reduce inclusion loading - struct DFTileSurface; - - class DFHACK_EXPORT Graphic : public Module - { - public: - bool Finish() - { - return true; - } - bool Register(DFTileSurface* (*func)(int,int)); - bool Unregister(DFTileSurface* (*func)(int,int)); - DFTileSurface* Call(int x, int y); - - private: - std::vector funcs; - }; -} - -#endif diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 222779a8ee..761e948bb0 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -25,8 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "ColorText.h" #include "Types.h" #include "DataDefs.h" @@ -34,7 +32,6 @@ distribution. #include "modules/GuiHooks.h" #include "df/announcement_type.h" -#include "df/report_zoom_type.h" #include "df/unit_report_type.h" namespace df { @@ -211,7 +208,7 @@ namespace DFHack /// Get the current top-level view-screen DFHACK_EXPORT df::viewscreen *getCurViewscreen(bool skip_dismissed = false); - DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(virtual_identity &id, int n = 1); + DFHACK_EXPORT df::viewscreen *getViewscreenByIdentity(const virtual_identity &id, int n = 1); /// Get the top-most underlying DF viewscreen (not owned by DFHack) DFHACK_EXPORT df::viewscreen *getDFViewscreen(bool skip_dismissed = false, df::viewscreen *top = NULL); diff --git a/library/include/modules/Hotkey.h b/library/include/modules/Hotkey.h new file mode 100644 index 0000000000..25129b2ff5 --- /dev/null +++ b/library/include/modules/Hotkey.h @@ -0,0 +1,77 @@ +#pragma once + +#include "Export.h" +#include "ColorText.h" + +#include +#include +#include +#include +#include + +namespace DFHack { + namespace Hotkey { + class DFHACK_EXPORT KeySpec { + public: + int modifiers = 0; + // Negative numbers denote mouse buttons + int sym = 0; + std::vector focus; + + static std::optional parse(std::string spec, std::string* err = nullptr); + std::string toString(bool include_focus=true) const; + + // Determines if a keybind could be disruptive to normal gameplay, + // including typing and navigating the UI. + bool isDisruptive() const; + }; + + struct KeyBinding { + KeySpec spec; + std::string command; + std::string cmdline; + }; + } + class DFHACK_EXPORT HotkeyManager { + public: + HotkeyManager(); + ~HotkeyManager(); + + + bool addKeybind(std::string keyspec, std::string_view cmd); + bool addKeybind(Hotkey::KeySpec spec, std::string_view cmd); + // Clear a keybind with the given keyspec, optionally for any focus, or with a specific command + bool removeKeybind(std::string keyspec, bool match_focus=true, std::string_view cmdline=""); + bool removeKeybind(const Hotkey::KeySpec& spec, bool match_focus=true, std::string_view cmdline=""); + + std::vector listKeybinds(std::string keyspec); + std::vector listKeybinds(const Hotkey::KeySpec& spec); + + std::vector listActiveKeybinds(); + std::vector listAllKeybinds(); + + bool handleKeybind(int sym, int modifiers); + void setHotkeyCommand(std::string cmd); + + // Used to request the next hotkey-compatible input is saved. + // This is to allow for graphical keybinding menus. + void requestKeybindingInput(bool cancel=false); + // Returns the latest requested keybind input + std::string getKeybindingInput(); + + private: + std::thread hotkey_thread; + std::mutex lock; + std::condition_variable cond; + + bool keybind_save_requested = false; + std::string requested_keybind; + + uint8_t hotkey_sig = 0; + std::string queued_command; + + std::map> bindings; + + void hotkey_thread_fn(); + }; +} diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index 79aed92c36..52af19e680 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -28,8 +28,6 @@ distribution. */ #include "DataDefs.h" #include "Export.h" -#include "MemAccess.h" -#include "Module.h" #include "Types.h" #include "modules/Materials.h" @@ -157,7 +155,7 @@ DFHACK_EXPORT bool moveToContainer(df::item *item, df::item *container); DFHACK_EXPORT bool moveToBuilding(df::item *item, df::building_actual *building, df::building_item_role_type use_mode = df::building_item_role_type::TEMP, bool force_in_building = false); DFHACK_EXPORT bool moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Hauled, int body_part = -1); + df::inv_item_role_type mode = df::inv_item_role_type::Hauled, int body_part = -1); /// Remove item from jobs and inventories, hide and forbid. /// Unless no_uncat, item is marked for garbage collection. @@ -172,8 +170,11 @@ DFHACK_EXPORT int getItemBaseValue(int16_t item_type, int16_t item_subtype, int1 // Gets the value of a specific item, taking into account civ values and trade agreements if a caravan is given. DFHACK_EXPORT int getValue(df::item *item, df::caravan_state *caravan = NULL); +// Automatically choose a growth print variant for the specified plant growth subtype+material +DFHACK_EXPORT int32_t pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg); + DFHACK_EXPORT bool createItem(std::vector &out_items, df::unit *creator, df::item_type type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false); + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor = false, int32_t count = 1); // Returns true if the item is free from mandates, or false if mandates prevent trading the item. DFHACK_EXPORT bool checkMandates(df::item *item); @@ -188,6 +189,9 @@ DFHACK_EXPORT bool markForTrade(df::item *item, df::building_tradedepotst *depot // Returns true if an active caravan will pay extra for the given item. DFHACK_EXPORT bool isRequestedTradeGood(df::item *item, df::caravan_state *caravan = NULL); +// DF standard_material_itemtype - returns true if item has material/matgloss, false if race+caste +DFHACK_EXPORT bool usesStandardMaterial(df::item_type item_type); + // Returns true if the item can currently be melted. If game_ui, then able to be marked is enough. DFHACK_EXPORT bool canMelt(df::item *item, bool game_ui = false); // Marks the item for melting. diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index f0d3e89b97..acb37169c3 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -27,15 +27,13 @@ distribution. #define CL_MOD_JOB #include "Export.h" -#include "Module.h" #include "Types.h" #include "DataDefs.h" +#include "df/building_workshopst.h" #include "df/item_type.h" #include "df/job_item_ref.h" -#include - namespace df { struct job; @@ -43,6 +41,7 @@ namespace df struct job_item_filter; struct building; struct unit; + struct manager_order; } namespace DFHack @@ -94,7 +93,13 @@ namespace DFHack DFHACK_EXPORT void checkBuildingsNow(); DFHACK_EXPORT void checkDesignationsNow(); + // link the job into the global job list, passing ownership to DF DFHACK_EXPORT bool linkIntoWorld(df::job *job, bool new_id = true); + // create a job and immediately link it into the global job list + DFHACK_EXPORT df::job* createLinked(); + + // assign job to workshop, returns false if workshop already has the maximum of ten jobs + DFHACK_EXPORT bool assignToWorkshop(df::job *job, df::building_workshopst *workshop); // Flag this job's posting as "dead" and set its posting_index to -1 // If remove_all is true, flag all postings pointing to this job @@ -105,7 +110,7 @@ namespace DFHack DFHACK_EXPORT bool listNewlyCreated(std::vector *pvec, int *id_var); DFHACK_EXPORT bool attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx = -1, int insert_idx = -1); DFHACK_EXPORT bool isSuitableItem(const df::job_item *item, df::item_type itype, int isubtype); @@ -113,6 +118,7 @@ namespace DFHack int mat_index, df::item_type itype); DFHACK_EXPORT std::string getName(df::job *job); + DFHACK_EXPORT std::string getManagerOrderName(df::manager_order *order); struct JobDeleter { void operator()(df::job *ptr) const { diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h index 04fddd38bd..d6f44b12d4 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -68,8 +68,8 @@ struct BiomeInfo { int16_t layer_stone[MAX_LAYERS]; }; -typedef uint8_t t_veintype[16][16]; -typedef df::tiletype t_tilearr[16][16]; +using t_veintype = arr40d; +using t_tilearr = arr40d; class BlockInfo { @@ -84,7 +84,7 @@ class BlockInfo }; static GroundType getGroundType(int material); - typedef df::block_square_event_mineralst::T_flags DFVeinFlags; + typedef df::mineral_event_flag DFVeinFlags; t_veintype veintype; t_blockmaterials veinmats; @@ -424,7 +424,7 @@ class DFHACK_EXPORT MapCache Block *BlockAt(DFCoord blockcoord); /// get the map block at a tile coord. Block *BlockAtTile(DFCoord coord) { - return BlockAt(df::coord(coord.x>>4,coord.y>>4,coord.z)); + return BlockAt(Maps::getTileBlockCoord(coord)); } bool ensureBlockAt(df::coord coord) @@ -433,6 +433,11 @@ class DFHACK_EXPORT MapCache return b ? b->Allocate() : false; } + /// Propagate the light, outside, and subterranean flags of the tile at + /// pos to the tiles below it. Call after the tile at pos changed + /// between an open and a solid shape. + void propagateVerticalFlags(DFCoord pos); + /// delete the block from memory void discardBlock(Block *block); @@ -616,6 +621,7 @@ class DFHACK_EXPORT MapCache uint32_t y_tmax; uint32_t z_max; std::vector biomes; + std::map biome_index; std::map region_details; std::map blocks; }; diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c..6d85b35648 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -31,17 +31,26 @@ distribution. #define CL_MOD_MAPS #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "modules/Materials.h" +#include +#include +#include + #include "df/biome_type.h" #include "df/block_flags.h" +#include "df/coord.h" +#include "df/coord2d.h" #include "df/feature_type.h" +#include "df/flow_info.h" #include "df/flow_type.h" +#include "df/item_type.h" +#include "df/matter_state.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" +#include "df/weather_type.h" +#include "df/world_site.h" namespace df { struct block_square_event; @@ -127,28 +136,32 @@ enum BiomeOffset { */ typedef df::block_flags t_blockflags; +template +using arr40d = std::array, 16>; + /** * 16x16 array of tile types * \ingroup grp_maps */ -typedef df::tiletype tiletypes40d [16][16]; +using tiletypes40d = arr40d; /** * 16x16 array used for squashed block materials * \ingroup grp_maps */ -typedef int16_t t_blockmaterials [16][16]; +using t_blockmaterials = arr40d; /** * 16x16 array of designation flags * \ingroup grp_maps */ typedef df::tile_designation t_designation; -typedef t_designation designations40d [16][16]; +using designations40d = arr40d; + /** * 16x16 array of occupancy flags * \ingroup grp_maps */ typedef df::tile_occupancy t_occupancy; -typedef t_occupancy occupancies40d [16][16]; +using occupancies40d = arr40d; /** * array of 16 biome indexes valid for the block * \ingroup grp_maps @@ -158,7 +171,7 @@ typedef uint8_t biome_indices40d [9]; * 16x16 array of temperatures * \ingroup grp_maps */ -typedef uint16_t t_temperatures [16][16]; +using t_temperatures = arr40d; /** * Index a tile array by a 2D coordinate, clipping it to mod 16. @@ -229,9 +242,11 @@ class cuboid { DFHACK_EXPORT bool containsPos(int16_t x, int16_t y, int16_t z) const; DFHACK_EXPORT bool containsPos(const df::coord &pos) const { return containsPos(pos.x, pos.y, pos.z); } - /// Iterate over every point in the cuboid from top-down, N-S, then W-E. Doesn't guarantee valid map tile! + /// Iterate over every point in the cuboid. Doesn't guarantee valid map tile! /// "fn" should return true to keep iterating. Won't iterate if cuboid invalid. - DFHACK_EXPORT void forCoord(std::function fn) const; + /// If row_major is false, iterates from top-down (z), N-S (y), then W-E (x). + /// If row_major is true, iterates from top-down (z), W-E (x), then N-S (y). + DFHACK_EXPORT void forCoord(std::function fn, bool row_major = false) const; /// Iterate over every non-NULL map block intersecting the tile cuboid from top-down, N-S, then W-E. /// Will also supply the intersection of this cuboid and block to your "fn" for use with cuboid::forCoord. @@ -250,12 +265,13 @@ namespace Maps extern DFHACK_EXPORT bool IsValid(); /// Iterate over points in a cuboid from z1:z2, y1:y2, then x1:x2. +/// If row_major is true, iterates from z1:z2, x1:x2, then y1:y2. /// Doesn't guarantee valid map tile! Can be used to iterate over blocks, etc. /// "fn" should return true to keep iterating. DFHACK_EXPORT void forCoord(std::function fn, - int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); -inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2) { - forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); + int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2, bool row_major = false); +inline void forCoord(std::function fn, const df::coord &p1, const df::coord &p2, bool row_major = false) { + forCoord(fn, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, row_major); } /** @@ -337,6 +353,18 @@ inline df::map_block *getBlock (df::coord pos) { return getBlock(pos.x, pos.y, p inline df::map_block *getTileBlock (df::coord pos) { return getTileBlock(pos.x, pos.y, pos.z); } inline df::map_block *ensureTileBlock (df::coord pos) { return ensureTileBlock(pos.x, pos.y, pos.z); } +// Get the coordinate of the map block containing the given tile position. +// The z coordinate is a level and is preserved, not scaled. +inline df::coord getTileBlockCoord(df::coord pos) { return df::coord(pos.x >> 4, pos.y >> 4, pos.z); } + +// Get the tile position of a map block's origin (north-west corner), given the +// block's coordinate. The z coordinate is a level and is preserved, not scaled. +inline df::coord getBlockOrigin(df::coord pos) { return df::coord(pos.x << 4, pos.y << 4, pos.z); } + +// Get a tile position's offset within its containing map block. The returned +// x and y are in the range 0-15; z is preserved (blocks span a single level). +inline df::coord getTileBlockOffset(df::coord pos) { return df::coord(pos.x & 15, pos.y & 15, pos.z); } + extern DFHACK_EXPORT df::tiletype *getTileType(int32_t x, int32_t y, int32_t z); extern DFHACK_EXPORT df::tile_designation *getTileDesignation(int32_t x, int32_t y, int32_t z); extern DFHACK_EXPORT df::tile_occupancy *getTileOccupancy(int32_t x, int32_t y, int32_t z); @@ -345,14 +373,23 @@ inline df::tiletype *getTileType(df::coord pos) { return getTileType(pos.x, pos. inline df::tile_designation *getTileDesignation(df::coord pos) { return getTileDesignation(pos.x, pos.y, pos.z); } inline df::tile_occupancy *getTileOccupancy(df::coord pos) { return getTileOccupancy(pos.x, pos.y, pos.z); } +// shift world region coordinate by region_details biome reference +DFHACK_EXPORT df::coord2d addRegionBiomeOffset(df::coord2d world_pos, int8_t offset_dir); // Returns biome info about the specified world region. DFHACK_EXPORT df::region_map_entry *getRegionBiome(df::coord2d rgn_pos); +// Returns world region coordinates of the BiomeOffset neighbor of a world +// region cell, clipped to the world boundaries. Idx must be in 0-8. +DFHACK_EXPORT df::coord2d getBiomeRgnPos(df::coord2d rgn_pos, int idx); + // Returns biome world region coordinates for the given tile within given block. DFHACK_EXPORT df::coord2d getBlockTileBiomeRgn(df::map_block *block, df::coord2d pos); inline df::coord2d getTileBiomeRgn(df::coord pos) { return getBlockTileBiomeRgn(getTileBlock(pos), pos); } +DFHACK_EXPORT df::weather_type getCurrentWeather(df::coord pos); +DFHACK_EXPORT df::weather_type getCurrentWeather(); + // Enables per-frame updates for liquid flow and/or temperature. DFHACK_EXPORT void enableBlockUpdates(df::map_block *blk, bool flow = false, bool temperature = false); @@ -371,6 +408,10 @@ extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block, std::vector *priorities = 0 ); +// Add spatters at the specified location, returning the amount that couldn't be placed (e.g. due to overflow) +extern DFHACK_EXPORT int32_t addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount); +extern DFHACK_EXPORT int32_t addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount); + // Remove a block event from the block by address. extern DFHACK_EXPORT bool RemoveBlockEvent(int32_t x, int32_t y, int32_t z, df::block_square_event *which ); extern DFHACK_EXPORT bool RemoveBlockEvent(uint32_t x, uint32_t y, uint32_t z, df::block_square_event *which ); // TODO: deprecate me @@ -399,6 +440,125 @@ DFHACK_EXPORT bool removeTileAquifer(int32_t x, int32_t y, int32_t z); inline bool removeTileAquifer(df::coord pos) { return removeTileAquifer(pos.x, pos.y, pos.z); } DFHACK_EXPORT int removeAreaAquifer(df::coord pos1, df::coord pos2, std::function filter = [](df::coord pos, df::map_block *block) { return true; }); + +DFHACK_EXPORT void addBlockColumns(int32_t new_height); + +// Get surroundings classification from savagery and evilness +DFHACK_EXPORT const char* describeSurroundings(int savagery, int evilness); + +/** + * A single function does not merit a "Sites" module, hence we collect site functions here in the meantime. + */ + +// Get the classification string (e.g. "town", "hillocs", "tower", etc.) for a site +DFHACK_EXPORT const char* getSiteTypeName(df::world_site *site); + +/** + * Filter used by Maps::forEachTile to decide which tiles are acted on. + * A tile matches iff it satisfies every criterion that is enabled (non-empty + * set, non-zero mask, or a custom predicate that accepts it). A filter with + * no enabled criteria matches every tile. + * \ingroup grp_maps + */ +struct DFHACK_EXPORT TileFilter { + /// Allowed tiletypes; empty means any tiletype matches. + std::unordered_set tiletypes; + /// Allowed df::tiletype_material attribute values; empty means any. + std::unordered_set materials; + /// Allowed df::tiletype_shape attribute values; empty means any. + std::unordered_set shapes; + /// Allowed df::tiletype_shape_basic attribute values; empty means any. + std::unordered_set shapes_basic; + /// Allowed df::tiletype_special attribute values; empty means any. + std::unordered_set specials; + /// Allowed df::tiletype_variant attribute values; empty means any. + std::unordered_set variants; + /// Designation requirement: (designation.whole & mask) == bits. + /// A zero mask matches any designation. + uint32_t designation_mask = 0; + uint32_t designation_bits = 0; + /// Occupancy requirement: (occupancy.whole & mask) == bits. + /// A zero mask matches any occupancy. + uint32_t occupancy_mask = 0; + uint32_t occupancy_bits = 0; + /// Optional extra predicate, evaluated last for each candidate tile. + /// Arguments are the map block, the tile-local coordinates, and the + /// global coordinates. + std::function extra; + + /// Check whether the given tile matches this filter. + bool matches(df::map_block *block, df::coord2d local, df::coord pos) const; +}; + +/** + * Actions applied by Maps::forEachTile to every tile that passes the filter. + * All enabled actions run for each matching tile, in the order the fields + * are declared below; the callback runs last. + * \ingroup grp_maps + */ +struct DFHACK_EXPORT TileActions { + /// If set, matching tiles are changed to this tiletype, unless their + /// current tiletype is listed in tiletype_map (which takes precedence). + std::optional set_tiletype; + /// Per-tiletype replacements: a matching tile whose current tiletype is + /// a key in this map is changed to the mapped value instead of + /// set_tiletype. Tiles with unmapped tiletypes fall back to + /// set_tiletype if it is set, and are otherwise left unchanged. + std::unordered_map tiletype_map; + /// Designation update: whole = (whole & ~clear) | set. + uint32_t designation_set = 0; + uint32_t designation_clear = 0; + /// Occupancy update: whole = (whole & ~clear) | set. + uint32_t occupancy_set = 0; + uint32_t occupancy_clear = 0; + /// If true, a df::construction record is spawned at each matching tile + /// that does not already have one, using the fields below as a template. + bool construct = false; + /// Template fields for spawned constructions. + df::item_type construct_item_type = df::item_type::NONE; + int16_t construct_item_subtype = -1; + int16_t construct_mat_type = -1; + int32_t construct_mat_index = -1; + /// Raw value copied to df::construction::flags.whole. + uint8_t construct_flags = 0; + /// If set, the map tile's tiletype is rewritten to this when its + /// construction is spawned. The construction's original_tile records + /// the tiletype present immediately before this write. + std::optional construct_tiletype; + /// Called for each matching tile after the other actions are applied. + /// Return false to abort the scan early. + std::function callback; +}; + +/** + * Statistics produced by Maps::forEachTile. + * \ingroup grp_maps + */ +struct DFHACK_EXPORT TileScanResult { + /// Tiles examined inside allocated map blocks. + int64_t tiles_scanned = 0; + /// Tiles that matched the filter. + int64_t tiles_matched = 0; + /// Tiles whose tiletype was rewritten. + int64_t tiletypes_changed = 0; + /// Construction records created. + int64_t constructions_added = 0; + /// True if the scan stopped early because the callback returned false. + bool aborted = false; +}; + +/** + * Iterate every tile inside a cuboid of map tiles, test each one against a + * filter, and apply the enabled actions to the matching tiles. + * + * The cuboid is clamped to the loaded map; tiles in unallocated blocks are + * skipped. Iteration is block-major for speed. Constructions are buffered + * during the scan and merged into world.event.constructions in a single + * sorted pass at the end. TileScanResult::tiles_matched doubles as a + * count of matching tiles when no other action is enabled. + */ +DFHACK_EXPORT TileScanResult forEachTile( + const cuboid &bounds, const TileFilter &filter, const TileActions &actions); } } #endif diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 90907b8a41..652ed5fc2a 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -30,14 +30,13 @@ distribution. * @ingroup grp_modules */ #include "Export.h" -#include "Module.h" -#include "Types.h" -#include "BitArray.h" #include "DataDefs.h" #include "df/craft_material_class.h" +#include "df/item_type.h" -namespace df { +namespace df +{ struct item; struct plant_raw; struct creature_raw; @@ -56,28 +55,25 @@ namespace df { namespace DFHack { - struct t_matpair { + struct t_matpair + { int16_t mat_type; int32_t mat_index; t_matpair(int16_t type = -1, int32_t index = -1) - : mat_type(type), mat_index(index) {} + : mat_type(type), mat_index(index) + {} }; - struct DFHACK_EXPORT MaterialInfo { - static const int NUM_BUILTIN = 19; - static const int GROUP_SIZE = 200; - static const int CREATURE_BASE = NUM_BUILTIN; - static const int FIGURE_BASE = NUM_BUILTIN + GROUP_SIZE; - static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE*2; - static const int END_BASE = NUM_BUILTIN + GROUP_SIZE*3; - + struct DFHACK_EXPORT MaterialInfo + { int16_t type; int32_t index; - df::material *material; + df::material* material; - enum Mode { + enum Mode + { None, Builtin, Inorganic, @@ -87,16 +83,16 @@ namespace DFHack Mode mode; int16_t subtype; - df::inorganic_raw *inorganic; - df::creature_raw *creature; - df::plant_raw *plant; + df::inorganic_raw* inorganic; + df::creature_raw* creature; + df::plant_raw* plant; - df::historical_figure *figure; + df::historical_figure* figure; public: MaterialInfo(int16_t type = -1, int32_t index = -1) { decode(type, index); } - MaterialInfo(const t_matpair &mp) { decode(mp.mat_type, mp.mat_index); } - template MaterialInfo(T *ptr) { decode(ptr); } + MaterialInfo(const t_matpair& mp) { decode(mp.mat_type, mp.mat_index); } + template MaterialInfo(T* ptr) { decode(ptr); } bool isValid() const { return material != NULL; } @@ -110,25 +106,27 @@ namespace DFHack bool isInorganicWildcard() const { return isAnyInorganic() && isBuiltin(); } bool decode(int16_t type, int32_t index = -1); - bool decode(df::item *item); - bool decode(const df::material_vec_ref &vr, int idx); - bool decode(const t_matpair &mp) { return decode(mp.mat_type, mp.mat_index); } + bool decode(df::item* item); + bool decode(const df::material_vec_ref& vr, int idx); + bool decode(const t_matpair& mp) { return decode(mp.mat_type, mp.mat_index); } - template bool decode(T *ptr) { + template bool decode(T* ptr) + { // Assume and exploit a certain naming convention return ptr ? decode(ptr->mat_type, ptr->mat_index) : decode(-1); } - bool find(const std::string &token); - bool find(const std::vector &tokens); + bool find(const std::string& token); + bool find(const std::vector& tokens); - bool findBuiltin(const std::string &token); - bool findInorganic(const std::string &token); - bool findPlant(const std::string &token, const std::string &subtoken); - bool findCreature(const std::string &token, const std::string &subtoken); + bool findBuiltin(const std::string& token); + bool findInorganic(const std::string& token); + bool findPlant(const std::string& token, const std::string& subtoken); + bool findCreature(const std::string& token, const std::string& subtoken); - bool findProduct(df::material *material, const std::string &name); - bool findProduct(const MaterialInfo &info, const std::string &name) { + bool findProduct(df::material* material, const std::string& name); + bool findProduct(const MaterialInfo& info, const std::string& name) + { return findProduct(info.material, name); } @@ -137,35 +135,38 @@ namespace DFHack bool isAnyCloth() const; - void getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const; - void getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const; - void getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const; + void getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const; + void getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const; + void getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const; df::craft_material_class getCraftClass(); - bool matches(const MaterialInfo &mat) const + bool matches(const MaterialInfo& mat) const { if (!mat.isValid()) return true; return (type == mat.type) && - (mat.index == -1 || index == mat.index); + (mat.index == -1 || index == mat.index); } - bool matches(const df::job_material_category &cat) const; - bool matches(const df::dfhack_material_category &cat) const; - bool matches(const df::job_item &jitem, - df::item_type itype = df::item_type::NONE) const; + bool matches(const df::job_material_category& cat) const; + bool matches(const df::dfhack_material_category& cat) const; + bool matches(const df::job_item& jitem, + df::item_type itype = df::item_type::NONE) const; }; - DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category *cat, const std::string &token); - DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category* cat, const std::string& token); + DFHACK_EXPORT bool parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token); - inline bool operator== (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator== (const MaterialInfo& a, const MaterialInfo& b) + { return a.type == b.type && a.index == b.index; } - inline bool operator!= (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator!= (const MaterialInfo& a, const MaterialInfo& b) + { return a.type != b.type || a.index != b.index; } - inline bool operator< (const MaterialInfo &a, const MaterialInfo &b) { + inline bool operator< (const MaterialInfo& a, const MaterialInfo& b) + { return a.type < b.type || (a.type == b.type && a.index < b.index); } @@ -347,38 +348,18 @@ namespace DFHack t_materialIndex mat_index; uint32_t flags; }; - /** - * The Materials module - * \ingroup grp_modules - * \ingroup grp_materials - */ - class DFHACK_EXPORT Materials : public Module + + + namespace Materials { - public: - Materials(); - ~Materials(); - bool Finish(); - - std::vector race; - std::vector raceEx; - std::vector color; - std::vector other; - std::vector alldesc; - - bool CopyInorganicMaterials (std::vector & inorganic); - bool CopyOrganicMaterials (std::vector & organic); - bool CopyWoodMaterials (std::vector & tree); - bool CopyPlantMaterials (std::vector & plant); - - bool ReadCreatureTypes (void); - bool ReadCreatureTypesEx (void); - bool ReadDescriptorColors(void); - bool ReadOthers (void); - - bool ReadAllMaterials(void); - - std::string getType(const t_material & mat); - std::string getDescription(const t_material & mat); - }; + /** + * The Materials module + * \ingroup grp_modules + * \ingroup grp_materials + */ + + std::string getType(const t_material& mat); + std::string getDescription(const t_material& mat); + } } #endif diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h index 013f804415..3b51a921e2 100644 --- a/library/include/modules/Military.h +++ b/library/include/modules/Military.h @@ -17,6 +17,8 @@ namespace Military DFHACK_EXPORT std::string getSquadName(int32_t squad_id); DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); +DFHACK_EXPORT bool removeFromSquad(int32_t unit_id); +DFHACK_EXPORT bool addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos = -1); } } diff --git a/library/include/modules/Random.h b/library/include/modules/Random.h index 38fbdd0f89..b8e5de26bf 100644 --- a/library/include/modules/Random.h +++ b/library/include/modules/Random.h @@ -31,9 +31,6 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" - #include "DataDefs.h" namespace DFHack @@ -106,6 +103,37 @@ namespace Random extern template void DFHACK_IMPORT MersenneRNG::unitvector(double *p, int size); #endif + // Standard Splitmix64 RNG, as used by Dwarf Fortress's "hash_rngst" class + class SplitmixRNG + { + uint64_t state; + + public: + SplitmixRNG(uint64_t seed) { + init(seed); + } + + void init(uint64_t seed) { + state = seed; + } + + uint64_t next() { + state += 0x9e3779b97f4a7c15; + uint64_t z = state; + z ^= z >> 30; + z *= 0xbf58476d1ce4e5b9; + z ^= z >> 27; + z *= 0x94d049bb133111eb; + z ^= z >> 31; + return z; + } + + int32_t df_trandom(uint32_t max) { + uint32_t val = next() >> 32; + return (int32_t)(val % max); + } + }; + /* * Classical Perlin noise function in template form. * http://mrl.nyu.edu/~perlin/doc/oscar.html#noise diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h index 3989460bb6..173cda6aa8 100644 --- a/library/include/modules/Screen.h +++ b/library/include/modules/Screen.h @@ -25,9 +25,6 @@ distribution. #pragma once #include "Export.h" -#include "Module.h" -#include "BitArray.h" -#include "ColorText.h" #include "Types.h" #include "DataDefs.h" @@ -35,6 +32,7 @@ distribution. #include "df/viewscreen.h" #include "df/graphic_viewportst.h" +#include "df/graphic_map_portst.h" #include #include @@ -203,6 +201,12 @@ namespace DFHack /// Retrieves one screen tile from the buffer DFHACK_EXPORT Pen readTile(int x, int y, bool map = false, int32_t * df::graphic_viewportst::*texpos_field = NULL); + /// Paint one world map tile with the given pen + DFHACK_EXPORT bool paintMapPortTile(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field = NULL); + + /// Retrieves one world map tile from the buffer + DFHACK_EXPORT Pen readMapPortTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field); + /// Paint a string onto the screen. Ignores ch and tile of pen. DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text, bool map = false); diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index b820c3332d..c2038c20b3 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -32,7 +32,7 @@ DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = fal * Load tileset from image file. * Return vector of handles to obtain valid texposes. */ -DFHACK_EXPORT std::vector loadTileset(const std::string& file, +DFHACK_EXPORT std::vector loadTileset(const std::filesystem::path file, int tile_px_w = TILE_WIDTH_PX, int tile_px_h = TILE_HEIGHT_PX, bool reserved = false); diff --git a/library/include/modules/Translation.h b/library/include/modules/Translation.h index 2709dbf6fa..2860c3db87 100644 --- a/library/include/modules/Translation.h +++ b/library/include/modules/Translation.h @@ -31,9 +31,9 @@ distribution. */ #include "Export.h" -#include "Module.h" -#include "Types.h" #include "DataDefs.h" +#include "Types.h" +#include "df/language_name_type.h" namespace df { struct language_name; diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 2e0f188b5c..569471d9f0 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -39,12 +39,14 @@ distribution. #include "df/job_skill.h" #include "df/mental_attribute_type.h" #include "df/misc_trait_type.h" +#include "df/need_type.h" #include "df/physical_attribute_type.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" #include "df/unit_path_goal.h" #include +#include namespace df { struct activity_entry; @@ -76,7 +78,7 @@ DFHACK_EXPORT bool isActive(df::unit *unit); DFHACK_EXPORT bool isVisible(df::unit *unit); // Unit is a non-dead (optionally sane) citizen of fort. DFHACK_EXPORT bool isCitizen(df::unit *unit, bool include_insane = false); -// Long-term resident, not the hostile type. +// Long-term (non-dead) resident, not the hostile type. DFHACK_EXPORT bool isResident(df::unit *unit, bool include_insane = false); // Similar to isCitizen, but includes tame animals. Will reveal ambushers for the fort, etc. DFHACK_EXPORT bool isFortControlled(df::unit *unit); @@ -152,6 +154,7 @@ DFHACK_EXPORT bool isPet(df::unit *unit); DFHACK_EXPORT bool hasExtravision(df::unit *unit); DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); DFHACK_EXPORT bool isBloodsucker(df::unit *unit); +DFHACK_EXPORT bool breathes(df::unit* unit); // Same race as fort (even if active werebeast). DFHACK_EXPORT bool isDwarf(df::unit *unit); @@ -197,10 +200,10 @@ DFHACK_EXPORT df::unit *getUnitByNobleRole(std::string noble); inline auto citizensRange(std::vector &vec, bool exclude_residents = false, bool include_insane = false) { return vec | std::views::filter([=](df::unit *unit) { - if (isDead(unit) || !isActive(unit)) - return false; - return isCitizen(unit, include_insane) || - (!exclude_residents && isResident(unit, include_insane)); + // isCitizen and isResident both reject dead units + return isActive(unit) && + (isCitizen(unit, include_insane) || + (!exclude_residents && isResident(unit, include_insane))); }); } @@ -281,9 +284,9 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit *unit, bool plural = false); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id, bool plural = false); DFHACK_EXPORT std::string getRaceChildName(df::unit *unit, bool plural = false); // Full readable name including profession. HF profession might be different from unit profession. -DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf); +DFHACK_EXPORT std::string getReadableName(df::historical_figure *hf, bool skip_english = false); // Full readable name including profession, curse name, and tame description. -DFHACK_EXPORT std::string getReadableName(df::unit *unit); +DFHACK_EXPORT std::string getReadableName(df::unit *unit, bool skip_english = false); // Unit's age (in non-integer years). Ignore false identities if true_age. DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); @@ -339,6 +342,17 @@ DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); +// get largest (i.e. most negative) focus penalty for a set of needs +using need_type_set = std::bitset; +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, need_type_set need_types); +// get focused penalty for a single need +DFHACK_EXPORT int32_t getFocusPenalty(df::unit* unit, df::need_type need_type); + +// unit has an unbailable social activity (e.g. "Socialize!") +DFHACK_EXPORT bool hasUnbailableSocialActivity(df::unit *unit); +// unit can be assigned a job +DFHACK_EXPORT bool isJobAvailable(df::unit *unit, bool preserve_social); + // Stress categories. 0 is highest stress, 6 is lowest. DFHACK_EXPORT extern const std::vector stress_cutoffs; DFHACK_EXPORT int getStressCategory(df::unit *unit); @@ -350,5 +364,10 @@ DFHACK_EXPORT void multiplyActionTimers(color_ostream &out, df::unit *unit, floa DFHACK_EXPORT void multiplyGroupActionTimers(color_ostream &out, df::unit *unit, float amount, df::unit_action_type_group affectedActionTypeGroup); DFHACK_EXPORT void setActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type affectedActionType); DFHACK_EXPORT void setGroupActionTimers(color_ostream &out, df::unit *unit, int32_t amount, df::unit_action_type_group affectedActionTypeGroup); + +DFHACK_EXPORT df::unit* get_cached_unit_by_global_id(int32_t id, int32_t& index); + +enum class breathing_state { CANT = 0, TROUBLE, FINE }; +DFHACK_EXPORT breathing_state getBreathingState(df::unit* unit); } } diff --git a/library/include/modules/World.h b/library/include/modules/World.h index b03170b5fa..6d84df6f0c 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -31,12 +31,16 @@ distribution. * @ingroup grp_modules */ +#include "DataDefs.h" #include "Export.h" -#include "Module.h" + #include "modules/Persistence.h" -#include -#include "DataDefs.h" +#include "df/game_mode.h" +#include "df/game_type.h" +#include "df/unit.h" +#include "df/weather_type.h" + namespace df { @@ -77,7 +81,7 @@ namespace DFHack DFHACK_EXPORT uint32_t ReadCurrentYear(); DFHACK_EXPORT uint32_t ReadCurrentMonth(); DFHACK_EXPORT uint32_t ReadCurrentDay(); - DFHACK_EXPORT uint8_t ReadCurrentWeather(); + DFHACK_EXPORT df::weather_type ReadCurrentWeather(); // deprecated DFHACK_EXPORT void SetCurrentWeather(uint8_t weather); DFHACK_EXPORT bool ReadGameMode(t_gamemodes& rd); DFHACK_EXPORT bool WriteGameMode(const t_gamemodes & wr); // this is very dangerous diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index d9ee57d139..e5337afa11 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -237,13 +237,16 @@ function coords(arg, arg_name, skip_validation) return pos end -local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true, - ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false} +local toBool={["true"]=true,["yes"]=true,["y"]=true,["on"]=true,["1"]=true,["enable"]=true,["enabled"]=true, + ["false"]=false,["no"]=false,["n"]=false,["off"]=false,["0"]=false,["disable"]=false,["disabled"]=false} ---@nodiscard ---@param arg string ---@param arg_name? string ---@return boolean function boolean(arg, arg_name) + if arg == nil then + arg_error(arg_name, 'missing value; expected "true", "yes", "false", or "no"') + end local arg_lower = string.lower(arg) if toBool[arg_lower] == nil then arg_error(arg_name, diff --git a/library/lua/custom-raw-tokens.lua b/library/lua/custom-raw-tokens.lua index 6c19deb72a..7975ba5e39 100644 --- a/library/lua/custom-raw-tokens.lua +++ b/library/lua/custom-raw-tokens.lua @@ -300,7 +300,7 @@ local function getTokenArg1Else(userdata, token) elseif df.is_instance(df.building_workshopst, userdata) or df.is_instance(df.building_furnacest, userdata) then rawStruct = df.building_def.find(userdata.custom_type) elseif df.is_instance(df.interaction_instance, userdata) then - rawStruct = df.global.world.raws.interactions[userdata.interaction_id] + rawStruct = df.global.world.raws.interactions.all[userdata.interaction_id] else -- Assume raw struct *is* argument 1 rawStruct = userdata diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ef08fb7f4b..aaf58e214e 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -367,6 +367,26 @@ local function print_fields(value, seen, indent, prefix) return 0 end +-- Recursing into the members of an untagged union is unsafe: only one member +-- is live at a time and there is no tag to identify which one, so accessing +-- any of the others is undefined behavior and can crash the game. Tagged +-- unions are safe to iterate since their iterators only produce the live +-- member. +local function is_untagged_union(value) + if df.isvalid(value) ~= 'ref' then + return false + end + local vtype = value._type + if type(vtype) ~= 'table' or not vtype._union then + return false + end + -- a tagged union's iterator produces at most the one live member; an + -- untagged union's iterator produces all of them + local count = 0 + safe_iterate(value, pairs, function() count = count + 1 end) + return count > 1 +end + -- This should be same as print_array but userdata doesn't compare equal even if -- they hold same pointer. local function print_userdata(printfn, value, seen, indent) @@ -375,10 +395,15 @@ local function print_userdata(printfn, value, seen, indent) dfhack.println(strvalue) if seen[strvalue] then dfhack.print(prefix) - dfhack.println('\n') + dfhack.println('') return 0 end seen[strvalue] = true + if is_untagged_union(value) then + dfhack.print(prefix) + dfhack.println('') + return 0 + end return print_fields(value, seen, indent, prefix) end @@ -387,7 +412,7 @@ local function print_array(printfn, value, seen, indent) dfhack.println(tostring(value)) if seen[value] then dfhack.print(prefix) - dfhack.println('\n') + dfhack.println('') return 0 end seen[value] = true @@ -845,16 +870,26 @@ function dfhack.interpreter(prompt,hfile,env) return nil, 'not interactive' end - print("Type quit to exit interactive lua interpreter.") + local function print_keyword(pre, keyword, post) + dfhack.color(COLOR_RESET) + if pre ~= nil then dfhack.print(pre) end + dfhack.color(COLOR_YELLOW) + dfhack.print(keyword) + dfhack.color(COLOR_RESET) + if post ~= nil then print(post) end + end + print_keyword("Type ", "quit", " to exit interactive lua interpreter.") if print_banner then - print("Shortcuts:\n".. - " '= foo' => '_1,_2,... = foo'\n".. - " '! foo' => 'print(foo)'\n".. - " '~ foo' => 'printall(foo)'\n".. - " '^ foo' => 'printall_recurse(foo)'\n".. - " '@ foo' => 'printall_ipairs(foo)'\n".. - "All of these save the first result as '_'.") + print("Shortcuts:") + print_keyword(" '", "= foo", "' => '_1,_2,... = foo'") + print_keyword(" '", "! foo", "' => 'print(foo)'") + print_keyword(" '", "~ foo", "' => 'printall(foo)'") + print_keyword(" '", "^ foo", "' => 'printall_recurse(foo)'") + print_keyword(" '", "@ foo", "' => 'printall_ipairs(foo)'") + print_keyword("All of these save the first result as '", "_", "'.") + print("These keywords refer to the currently-selected object in the game:") + print_keyword(" ", "unit item plant building bld job workshop_job wsjob screen scr", "") print_banner = false end @@ -994,13 +1029,13 @@ local valid_script_flags = { scripts = {required = false}, } -local warned_scripts = {} +local checked_scripts = {} function dfhack.run_script(name,...) - if not warned_scripts[name] then + if not checked_scripts[name] then + checked_scripts[name] = true local helpdb = require('helpdb') if helpdb.has_tag(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.') qerror('Please run the command again to ignore this warning and proceed.') diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua index 7fff00dcf2..ae4d624360 100644 --- a/library/lua/dfhack/buildings.lua +++ b/library/lua/dfhack/buildings.lua @@ -130,7 +130,6 @@ local building_inputs = { vector_id=df.job_item_vector_id.PIPE_SECTION } }, - [df.building_type.Construction] = { { flags2={ building_material=true, non_economic=true } } }, [df.building_type.Hatch] = { { item_type=df.item_type.HATCH_COVER, @@ -353,6 +352,27 @@ local siegeengine_input = { quantity=3 } }, + [df.siegeengine_type.BoltThrower] = { + { + item_type=df.item_type.BOLT_THROWER_PARTS, + vector_id=df.job_item_vector_id.BOLT_THROWER_PARTS, + }, + { + flags1={ empty=true }, + item_type=df.item_type.BIN, + vector_id=df.job_item_vector_id.BIN, + }, + { + name='mechanism', + item_type=df.item_type.TRAPPARTS, + vector_id=df.job_item_vector_id.TRAPPARTS, + }, + { + name='chain', + item_type=df.item_type.CHAIN, + vector_id=df.job_item_vector_id.CHAIN + }, + }, } --[[ Functions for lookup in tables. ]] @@ -380,6 +400,11 @@ local function get_inputs_by_type(type,subtype,custom) return trap_inputs[subtype] elseif type == df.building_type.SiegeEngine then return siegeengine_input[subtype] + elseif type == df.building_type.Construction then + if subtype == df.construction_type.ReinforcedWall then + return { { flags2={ building_material=true, non_economic=true }, quantity=2 }, { flags3={ metal=true }, item_type=df.item_type.BAR, vector_id=df.job_item_vector_id.BAR } } + end + return { { flags2={ building_material=true, non_economic=true } } } else return building_inputs[type] end diff --git a/library/lua/dfhack/workshops.lua b/library/lua/dfhack/workshops.lua index a09849a57d..f43429642f 100644 --- a/library/lua/dfhack/workshops.lua +++ b/library/lua/dfhack/workshops.lua @@ -509,7 +509,7 @@ local function addReactionJobs(ret,bid,wid,cid) end local function scanRawsOres() local ret={} - for idx,ore in ipairs(df.global.world.raws.inorganics) do + for idx,ore in ipairs(df.global.world.raws.inorganics.all) do if #ore.metal_ore.mat_index~=0 then ret[idx]=ore end diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 793acbd520..f856e6281b 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -7,6 +7,7 @@ local utils = require('utils') local dscreen = dfhack.screen +local a_look = df.global.game.main_interface.adventure.look local g_cursor = df.global.cursor local g_sel_rect = df.global.selection_rect local world_map = df.global.world.map @@ -38,17 +39,39 @@ end ---@return df.coord|nil function getCursorPos() - if g_cursor.x >= 0 then + if df.global.gamemode == df.game_mode.ADVENTURE then + if a_look.open then + return copyall(a_look.cursor) + end + elseif g_cursor.x >= 0 then return copyall(g_cursor) end end function setCursorPos(cursor) - df.global.cursor = copyall(cursor) + if df.global.gamemode == df.game_mode.ADVENTURE then + if not a_look.open then + return + end + a_look.cursor = copyall(cursor) + else + df.global.cursor = copyall(cursor) + end end function clearCursorPos() - df.global.cursor = xyz2pos(nil) + if df.global.gamemode == df.game_mode.ADVENTURE then + if not a_look.open then + return + end + local u = dfhack.world.getAdventurer() + local pos = u and dfhack.units.getPosition(u) + if pos then -- recenter cursor on adventurer + a_look.cursor = copyall(pos) + end + else + df.global.cursor = xyz2pos(nil) + end end function getSelection() @@ -249,7 +272,7 @@ end function get_hotkey_target(key) local hk = HOTKEY_KEYS[key] - if hk and hk.cmd == df.ui_hotkey.T_cmd.Zoom then + if hk and hk.cmd == df.hotkey_type.Zoom then return xyz2pos(hk.x, hk.y, hk.z) end end diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index d3a9408a51..99e4e6449b 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -8,8 +8,9 @@ local dlg = require('gui.dialogs') ARROW = string.char(26) -CREATURE_BASE = 19 -PLANT_BASE = 419 +-- For backwards compatibility with older scripts +CREATURE_BASE = df.builtin_mats.CREATURE_1 +PLANT_BASE = df.builtin_mats.PLANT_1 MaterialDialog = defclass(MaterialDialog, gui.FramedScreen) @@ -116,7 +117,7 @@ end function MaterialDialog:initInorganicMode() local choices = {} - for i,mat in ipairs(df.global.world.raws.inorganics) do + for i,mat in ipairs(df.global.world.raws.inorganics.all) do self:addMaterial(choices, mat.material, 0, i, false, mat) end @@ -127,7 +128,7 @@ function MaterialDialog:initCreatureMode() local choices = {} for i,v in ipairs(df.global.world.raws.creatures.all) do - self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i) + self:addObjectChoice(choices, v, v.name[0], df.builtin_mats.CREATURE_1, i) end self:pushContext('Creature materials', choices) @@ -137,7 +138,7 @@ function MaterialDialog:initPlantMode() local choices = {} for i,v in ipairs(df.global.world.raws.plants.all) do - self:addObjectChoice(choices, v, v.name, PLANT_BASE, i) + self:addObjectChoice(choices, v, v.name, df.builtin_mats.PLANT_1, i) end self:pushContext('Plant materials', choices) @@ -378,14 +379,14 @@ function ItemTraitsDialog(args) end -------------------------------------- local set_ore_ix = {} - for i, raw in ipairs(df.global.world.raws.inorganics) do + for i, raw in ipairs(df.global.world.raws.inorganics.all) do for _, ix in ipairs(raw.metal_ore.mat_index) do set_ore_ix[ix] = true end end local ores = {} for ix in pairs(set_ore_ix) do - local raw = df.global.world.raws.inorganics[ix] + local raw = df.global.world.raws.inorganics.all[ix] ores[#ores+1] = {mat_index = ix, name = raw.material.state_name.Solid} end table.sort(ores, function(a,b) return a.name < b.name end) diff --git a/library/lua/gui/textures.lua b/library/lua/gui/textures.lua index 44d4f0de30..b97a10e439 100644 --- a/library/lua/gui/textures.lua +++ b/library/lua/gui/textures.lua @@ -8,16 +8,16 @@ local _ENV = mkmodule('gui.textures') -- Use these handles if you need to get dfhack standard textures. ---@type table local texpos_handles = { - green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), - red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12, true), - icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12, true), - on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true), - control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12, true), - border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12, true), - border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12, true), - border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12, true), - border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12, true), - border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12, true), + green_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/green-pin.png', 8, 12, true), + red_pin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/red-pin.png', 8, 12, true), + icons = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/icons.png', 8, 12, true), + on_off = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/on-off.png', 8, 12, true), + control_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/control-panel.png', 8, 12, true), + border_thin = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-thin.png', 8, 12, true), + border_medium = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-medium.png', 8, 12, true), + border_bold = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-bold.png', 8, 12, true), + border_panel = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-panel.png', 8, 12, true), + border_window = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/border-window.png', 8, 12, true), } -- Get valid texpos for preloaded texture in tileset diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 8a5daab7b2..726b52003a 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -16,8 +16,9 @@ Label = require('gui.widgets.labels.label') Scrollbar = require('gui.widgets.scrollbar') WrappedLabel = require('gui.widgets.labels.wrapped_label') TooltipLabel = require('gui.widgets.labels.tooltip_label') -HelpButton = require('gui.widgets.buttons.help_button') ConfigureButton = require('gui.widgets.buttons.configure_button') +HelpButton = require('gui.widgets.buttons.help_button') +RadioButton = require('gui.widgets.buttons.radio_button') BannerPanel = require('gui.widgets.containers.banner_panel') TextButton = require('gui.widgets.buttons.text_button') CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') @@ -27,6 +28,7 @@ List = require('gui.widgets.list') FilteredList = require('gui.widgets.filtered_list') TabBar = require('gui.widgets.tab_bar') RangeSlider = require('gui.widgets.range_slider') +Slider = require('gui.widgets.slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') TextArea = require('gui.widgets.text_area') diff --git a/library/lua/gui/widgets/buttons/configure_button.lua b/library/lua/gui/widgets/buttons/configure_button.lua index b93b1e18b2..7d865b27ad 100644 --- a/library/lua/gui/widgets/buttons/configure_button.lua +++ b/library/lua/gui/widgets/buttons/configure_button.lua @@ -1,3 +1,5 @@ +-- A 3x1 tile button with a gear symbol on it. Clicking on it will run a callback + local textures = require('gui.textures') local Panel = require('gui.widgets.containers.panel') local Label = require('gui.widgets.labels.label') @@ -6,10 +8,10 @@ local to_pen = dfhack.pen.parse local button_pen_left = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_center = to_pen{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol local button_pen_right = to_pen{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} -local configure_pen_center = to_pen{ - tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol --------------------- -- ConfigureButton -- @@ -17,6 +19,9 @@ local configure_pen_center = to_pen{ ---@class widgets.ConfigureButton.attrs: widgets.Panel.attrs ---@field on_click? function +---@field pen_left dfhack.pen|fun(): dfhack.pen +---@field pen_center dfhack.pen|fun(): dfhack.pen +---@field pen_right dfhack.pen|fun(): dfhack.pen ---@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs @@ -27,27 +32,40 @@ local configure_pen_center = to_pen{ ConfigureButton = defclass(ConfigureButton, Panel) ConfigureButton.ATTRS{ + frame={t=0, l=0, w=3, h=1}, on_click=DEFAULT_NIL, + pen_left=button_pen_left, + pen_center=button_pen_center, + pen_right=button_pen_right, } -function ConfigureButton:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 - init_table.frame.w = init_table.frame.w or 3 -end - function ConfigureButton:init() + self.frame.h = self.frame.h or 1 + self.frame.w = self.frame.w or 3 + self:addviews{ Label{ + view_id='label', frame={t=0, l=0, w=3, h=1}, text={ - {tile=button_pen_left}, - {tile=configure_pen_center}, - {tile=button_pen_right}, + {tile=self.pen_left}, + {tile=self.pen_center}, + {tile=self.pen_right}, }, on_click=self.on_click, }, } end +function ConfigureButton:postinit() + local l = self.subviews.label + + l.on_click = self.on_click + l.pen_left = self.pen_left + l.pen_center = self.pen_center + l.pen_right = self.pen_right + + l:setText({{tile=self.pen_left}, {tile=self.pen_center}, {tile=self.pen_right}}) +end + return ConfigureButton diff --git a/library/lua/gui/widgets/buttons/help_button.lua b/library/lua/gui/widgets/buttons/help_button.lua index 9f9b7dc989..9fa45f7a70 100644 --- a/library/lua/gui/widgets/buttons/help_button.lua +++ b/library/lua/gui/widgets/buttons/help_button.lua @@ -1,53 +1,35 @@ +-- A 3x1 tile button with a question mark on it. Clicking on it will show help text for a command + local textures = require('gui.textures') -local Panel = require('gui.widgets.containers.panel') -local Label = require('gui.widgets.labels.label') +local ConfigureButton = require('gui.widgets.buttons.configure_button') -local to_pen = dfhack.pen.parse +local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} ---------------- -- HelpButton -- ---------------- ----@class widgets.HelpButton.attrs: widgets.Panel.attrs +---@class widgets.HelpButton.attrs: widgets.ConfigureButton.attrs ---@field command? string ---@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs ----@class widgets.HelpButton: widgets.Panel, widgets.HelpButton.attrs ----@field super widgets.Panel +---@class widgets.HelpButton: widgets.ConfigureButton, widgets.HelpButton.attrs +---@field super widgets.ConfigureButton ---@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial) ---@overload fun(init_table: widgets.HelpButton.attrs.partial): self -HelpButton = defclass(HelpButton, Panel) +HelpButton = defclass(HelpButton, ConfigureButton) HelpButton.ATTRS{ frame={t=0, r=1, w=3, h=1}, command=DEFAULT_NIL, + pen_center=help_pen_center, } -local button_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} -local button_pen_right = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} -local help_pen_center = to_pen{ - tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} - function HelpButton:init() - self.frame.w = self.frame.w or 3 - self.frame.h = self.frame.h or 1 - local command = self.command .. ' ' - - self:addviews{ - Label{ - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=button_pen_left}, - {tile=help_pen_center}, - {tile=button_pen_right}, - }, - on_click=function() dfhack.run_command('gui/launcher', command) end, - }, - } + self.on_click = function() dfhack.run_command('gui/launcher', command) end end return HelpButton diff --git a/library/lua/gui/widgets/buttons/radio_button.lua b/library/lua/gui/widgets/buttons/radio_button.lua new file mode 100644 index 0000000000..d27d54edc3 --- /dev/null +++ b/library/lua/gui/widgets/buttons/radio_button.lua @@ -0,0 +1,59 @@ +-- A 3x1 tile button that toggles state when clicked + +local textures = require('gui.textures') +local ConfigureButton = require('gui.widgets.buttons.configure_button') + +local to_pen = dfhack.pen.parse + +local enabled_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1) or nil, ch=string.byte('[')} +local enabled_pen_center = to_pen{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check mark +local enabled_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} +local disabled_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} +local disabled_pen_center = to_pen{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} +local disabled_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + +----------------- +-- RadioButton -- +----------------- + +---@class widgets.RadioButton.attrs: widgets.ConfigureButton.attrs +---@field initial_state boolean +---@field on_change? fun(val: boolean) + +---@class widgets.RadioButton.attrs.partial: widgets.RadioButton.attrs + +---@class widgets.RadioButton: widgets.ConfigureButton, widgets.RadioButton.attrs +---@field super widgets.ConfigureButton +---@field ATTRS widgets.RadioButton.attrs|fun(attributes: widgets.RadioButton.attrs.partial) +---@overload fun(init_table: widgets.RadioButton.attrs.partial): self +RadioButton = defclass(RadioButton, ConfigureButton) + +RadioButton.ATTRS{ + initial_state=true, + on_change=DEFAULT_NIL, +} + +function RadioButton:setState(val) + self.toggle_state = not not val + + if self.on_change then + self.on_change(self.toggle_state) + end +end + +function RadioButton:init() + self.on_click = function() self:setState(not self.toggle_state) end + self.pen_left = function() return self.toggle_state and enabled_pen_left or disabled_pen_left end + self.pen_center = function() return self.toggle_state and enabled_pen_center or disabled_pen_center end + self.pen_right = function() return self.toggle_state and enabled_pen_right or disabled_pen_right end + + self:setState(self.initial_state) +end + +return RadioButton diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index bd1f9cb3de..0f53906c8c 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -1,49 +1,38 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse +local _SliderCore = require('gui.widgets.slide_core') -------------------------------- -- RangeSlider -------------------------------- ---@class widgets.RangeSlider.attrs: widgets.Widget.attrs ----@field num_stops integer ---@field get_left_idx_fn? function ---@field get_right_idx_fn? function ---@field on_left_change? fun(index: integer) ---@field on_right_change? fun(index: integer) +---@field is_single boolean ---@class widgets.RangeSlider.attrs.partial: widgets.RangeSlider.attrs ---@class widgets.RangeSlider.initTable: widgets.RangeSlider.attrs ---@field num_stops integer ----@class widgets.RangeSlider: widgets.Widget, widgets.RangeSlider.attrs ----@field super widgets.Widget +---@class widgets.RangeSlider: widgets._SliderCore, widgets.RangeSlider.attrs +---@field super widgets._SliderCore ---@field ATTRS widgets.RangeSlider.attrs|fun(attributes: widgets.RangeSlider.attrs.partial) ---@overload fun(init_table: widgets.RangeSlider.initTable): self -RangeSlider = defclass(RangeSlider, Widget) +RangeSlider = defclass(RangeSlider, _SliderCore) 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, + num_stops=DEFAULT_NIL, + is_single=false, } -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)) +function RangeSlider:get_width_per_idx() + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) end function RangeSlider:onInput(keys) @@ -51,11 +40,11 @@ function RangeSlider:onInput(keys) 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 width_per_idx = self:get_width_per_idx() 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) + self.on_left_change(self:clamp_idx(self.get_left_idx_fn() - 1)) elseif x < left_pos+3 then self.is_dragging_target = 'left' self.is_dragging_idx = x - left_pos @@ -66,99 +55,9 @@ function RangeSlider:onInput(keys) self.is_dragging_target = 'right' self.is_dragging_idx = x - right_pos else - self.on_right_change(self.get_right_idx_fn() + 1) + self.on_right_change(self:clamp_idx(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 - if not new_right_idx and new_left_idx > self.get_right_idx_fn() then - self.on_right_change(new_left_idx) - end - self.on_left_change(new_left_idx) - end - if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then - if new_right_idx < self.get_left_idx_fn() then - self.on_left_change(new_right_idx) - end - 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_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - return RangeSlider diff --git a/library/lua/gui/widgets/slide_core.lua b/library/lua/gui/widgets/slide_core.lua new file mode 100644 index 0000000000..84ef12427a --- /dev/null +++ b/library/lua/gui/widgets/slide_core.lua @@ -0,0 +1,168 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- _SliderCore +-------------------------------- + +---@class widgets._SliderCore.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field is_single boolean +---@field w integer + +---@class widgets._SliderCore.attrs.partial: widgets._SliderCore.attrs + +---@class widgets._SliderCore.initTable: widgets._SliderCore.attrs +---@field num_stops integer + +---@class widgets._SliderCore: widgets.Widget, widgets._SliderCore.attrs +---@field super widgets.Widget +---@field ATTRS widgets._SliderCore.attrs|fun(attributes: widgets._SliderCore.attrs.partial) +---@overload fun(init_table: widgets._SliderCore.initTable): self +_SliderCore = defclass(_SliderCore, Widget) +_SliderCore.ATTRS{ + num_stops=DEFAULT_NIL, + is_single=DEFAULT_NIL, + w=DEFAULT_NIL +} + +function _SliderCore:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function _SliderCore:init() + local min_stops = self:get_min_stops() + if self.num_stops < min_stops then error(('too few stops, expected at least %s'):format(min_stops)) end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +function _SliderCore:get_min_stops() + return self.is_single and 1 or 2 +end + +function _SliderCore:clamp_idx(idx) + return math.max(1, math.min(self.num_stops, idx)) +end + +local function 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 + new_idx = self:clamp_idx(new_idx) + 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 + if self.get_left_idx_fn ~= nil then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + else + new_right_idx = new_left_idx + end + if new_right_idx > self.num_stops then + return + end + end + end + if self.get_idx_fn == nil then + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + if not new_right_idx and new_left_idx > self.get_right_idx_fn() then + self.on_right_change(new_left_idx) + end + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + if new_right_idx < self.get_left_idx_fn() then + self.on_left_change(new_right_idx) + end + self.on_right_change(new_right_idx) + end + else + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end + 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 _SliderCore:onRenderBody(dc, rect) + local left_idx, right_idx + if self.get_idx_fn ~= nil then + left_idx = self.get_idx_fn() + right_idx = left_idx + else + left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + end + local width_per_idx = self:get_width_per_idx() + -- 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 tab(s) + if self.is_single then + -- Single slider: Draw one centered tab + dc:seek(width_per_idx * (left_idx-1) + 2) -- Center the tab + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + else + -- Dual slider: Draw left and right tabs separately + 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) + end + + -- Manage dragging + if self.is_dragging_target then + do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return _SliderCore diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua new file mode 100644 index 0000000000..480f4787f8 --- /dev/null +++ b/library/lua/gui/widgets/slider.lua @@ -0,0 +1,60 @@ +local _SliderCore = require('gui.widgets.slide_core') + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field get_idx_fn? function +---@field on_change? fun(index: integer) +---@field is_single boolean + + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets._SliderCore, widgets.Slider.attrs +---@field super widgets._SliderCore +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, _SliderCore) +Slider.ATTRS{ + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, + num_stops=DEFAULT_NIL, + is_single=true, +} + +function Slider:get_width_per_idx() + local min_value = (self.is_single) and 3 or 5 -- Single slider = 3, else 5 + return math.max(min_value, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_idx_fn(), self.get_idx_fn() + local width_per_idx = self:get_width_per_idx() + 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_change(self:clamp_idx(self.get_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_change(self:clamp_idx(self.get_idx_fn() + 1)) + end + return true +end + +return Slider diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua index cfa790a355..cd35f751ee 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -37,7 +37,7 @@ function TextAreaContent:init() self.cursor = nil self.main_pen = dfhack.pen.parse({ - bg=COLOR_RESET, + bg=COLOR_BLACK, bold=true }, self.text_pen) @@ -98,7 +98,12 @@ function TextAreaContent:setCursor(cursor_offset) end function TextAreaContent:setSelection(from_offset, to_offset) + if #self.text == 0 then + return + end + -- text selection is always start on self.cursor and end on self.sel_end + self:setCursor(from_offset) self.sel_end = to_offset @@ -240,12 +245,10 @@ function TextAreaContent:onRenderBody(dc) dc:newline() end - local show_focus = not self.enable_cursor_blink - or ( - not self:hasSelection() - and self.parent_view:hasFocus() - and gui.blink_visible(530) - ) + local show_focus = not self:hasSelection() and ( + not self.enable_cursor_blink + or (self.parent_view:hasFocus() and gui.blink_visible(530)) + ) if show_focus then local x, y = self.wrapped_text:indexToCoords(self.cursor) diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 0bd64d29cc..5af55f5697 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -5,8 +5,8 @@ local _ENV = mkmodule('helpdb') local argparse = require('argparse') -- paths -local RENDERED_PATH = 'hack/docs/docs/tools/' -local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt' +local RENDERED_PATH = dfhack.getHackPath() .. '/docs/docs/tools/' +local TAG_DEFINITIONS = dfhack.getHackPath() .. '/docs/docs/Tags.txt' -- used when reading help text embedded in script sources local SCRIPT_DOC_BEGIN = '[====[' diff --git a/library/lua/quickfix.lua b/library/lua/quickfix.lua index f46d03ef66..59880862b2 100644 --- a/library/lua/quickfix.lua +++ b/library/lua/quickfix.lua @@ -27,4 +27,21 @@ function set_entity_race_references() print(('quickfix: fixed %d unset entity race reference(s)'):format(count)) end +-- on reclaimed fortresses DF fails to assign plotinfo.site_id until the +-- first save, breaking anything that needs the site id; fortress_site is +-- set at embark, so restore the invariant from it +-- called from onMapLoad.default.init +function repair_site_id() + local plotinfo = df.global.plotinfo + if plotinfo.site_id ~= -1 then + return + end + local site = plotinfo.main.fortress_site + if not site then + return + end + plotinfo.site_id = site.id + print(('quickfix: repaired unassigned site_id (now %d)'):format(site.id)) +end + return _ENV diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d..cc4a02c6cd 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -80,33 +80,60 @@ end -- 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/' +local MODS_PATH = dfhack.filesystem.getBaseDir() .. 'mods/' +local INSTALLED_MODS_PATH = dfhack.filesystem.getBaseDir() .. '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' +-- returns the values of the given list of tags from the info.txt file in the given mod directory +-- if a requested tag includes the string 'NUMERIC_', it will return the numeric value for that tag +-- (e.g. NUMERIC_VERSION will return the numeric version of the mod as a number instead of a string). +function get_mod_info_metadata(mod_path, tags) + local idfile = mod_path .. '/info.txt' local ok, lines = pcall(io.lines, idfile) if not ok then return end - local id, version + + if type(tags) ~= 'table' then + tags = {tags} + end + + local tag_regexes = {} + for _,tag in ipairs(tags) do + local tag_regex = ('^%%[%s:'):format(tag) + if tag:find('NUMERIC_') then + -- note this doesn't go all the way to the closing brace since some people put + -- non-number characters in here, and DF only reads the leading digits for + -- numeric fields + tag_regex = tag_regex .. '(%d+)' + else + tag_regex = tag_regex .. '([^%]]+)' + end + tag_regexes[tag] = tag_regex + end + + local values = {} for line in lines do - if not id then - _,_,id = line:find('^%[ID:([^%]]+)%]') + local _,_,info_tag = line:find('^%[([^:]+):') + if not info_tag or not tag_regexes[info_tag] then + goto continue 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 leading digits - -- as the numeric version - _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') + local _,_,value = line:find(tag_regexes[info_tag]) + if value then + values[info_tag] = value end - -- note that we do *not* want to break out of this loop early since - -- lines has to hit EOF to close the file + ::continue:: end - return id, version + + return values +end + +local function get_mod_id_and_version(path) + local values = get_mod_info_metadata(path, {'ID', 'NUMERIC_VERSION'}) + if not values then return end + return values.ID, values.NUMERIC_VERSION end local function add_mod_paths(mod_paths, id, base_path, subdir) @@ -125,7 +152,7 @@ function get_mod_paths(installed_subdir, active_subdir) -- 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) + path = dfhack.filesystem.getBaseDir() .. path -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end local id = get_mod_id_and_version(path) @@ -172,6 +199,35 @@ function get_mod_paths(installed_subdir, active_subdir) return mod_paths end +-- returns a list of tables in load order with the following fields: +-- id: mod id +-- name: mod display name +-- version: mod display version +-- numeric_version: numeric mod version +-- path: path to the mod directory +-- vanilla: true if this is a vanilla mod +function get_active_mods() + if not dfhack.isWorldLoaded() then return {} end + + local mods = {} + + local ol = df.global.world.object_loader + + for idx=0,#ol.object_load_order_id-1 do + local path = ol.object_load_order_src_dir[idx] + table.insert(mods, { + id=ol.object_load_order_id[idx].value, + name=ol.object_load_order_name[idx].value, + version=ol.object_load_order_displayed_version[idx].value, + numeric_version=ol.object_load_order_numeric_version[idx], + path=path, + vanilla=path:startswith('data/vanilla/'), -- windows also uses forward slashes + }) + end + + return mods +end + function get_mod_script_paths() local paths = {} for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do @@ -190,7 +246,7 @@ function getModSourcePath(mod_id) end function getModStatePath(mod_id) - local path = ('dfhack-config/mods/%s/'):format(mod_id) + local path = (dfhack.getConfigPath() .. ('/mods/%s/')):format(mod_id) if not dfhack.filesystem.mkdir_recursive(path) then error(('failed to create mod state directory: "%s"'):format(path)) end diff --git a/library/lua/syndrome-util.lua b/library/lua/syndrome-util.lua index efbaa5deaa..c0af9f8a72 100644 --- a/library/lua/syndrome-util.lua +++ b/library/lua/syndrome-util.lua @@ -98,7 +98,7 @@ function infectWithSyndrome(target,syndrome,resetPolicy) unitSyndrome.ticks = 0 unitSyndrome.wound_id = -1 for k,v in ipairs(syndrome.ce) do - local symptom = df.unit_syndrome.T_symptoms:new() + local symptom = df.active_creature_interaction_effectst:new() symptom.quantity = 0 symptom.delay = 0 symptom.ticks = 0 diff --git a/library/lua/tile-material.lua b/library/lua/tile-material.lua index c0fe2e7cb6..5c847c463d 100644 --- a/library/lua/tile-material.lua +++ b/library/lua/tile-material.lua @@ -194,7 +194,7 @@ function GetTreeMat(x, y, z) for _, tree in ipairs(df.global.world.plants.all) do if tree.tree_info ~= nil then if coordInTree(pos, tree) then - return dfhack.matinfo.decode(419, tree.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, tree.material) end end end @@ -209,7 +209,7 @@ function GetShrubMat(x, y, z) for _, shrub in ipairs(df.global.world.plants.all) do if shrub.tree_info == nil then if shrub.pos.x == pos.x and shrub.pos.y == pos.y and shrub.pos.z == pos.z then - return dfhack.matinfo.decode(419, shrub.material) + return dfhack.matinfo.decode(df.builtin_mats.PLANT_1, shrub.material) end end end diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index a560a6fc99..ddedbbc1b6 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -29,7 +29,6 @@ distribution. #include "MemAccess.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "TileTypes.h" #include "MiscUtils.h" @@ -38,6 +37,7 @@ distribution. #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Job.h" +#include "modules/Units.h" #include "df/building_axle_horizontalst.h" #include "df/building_bars_floorst.h" @@ -48,18 +48,21 @@ distribution. #include "df/building_coffinst.h" #include "df/building_def.h" #include "df/building_design.h" +#include "df/building_extents_type.h" #include "df/building_floodgatest.h" #include "df/building_furnacest.h" #include "df/building_grate_floorst.h" #include "df/building_grate_wallst.h" #include "df/building_rollersst.h" #include "df/building_screw_pumpst.h" +#include "df/building_squad_infost.h" #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/buildingitemst.h" #include "df/buildings_other_id.h" #include "df/d_init.h" #include "df/dfhack_room_quality_level.h" @@ -77,6 +80,7 @@ distribution. #include "df/plotinfost.h" #include "df/punishment.h" #include "df/squad.h" +#include "df/squad_barracks_infost.h" #include "df/unit.h" #include "df/unit_relationship_type.h" #include "df/world.h" @@ -112,15 +116,15 @@ struct CoordHash { static unordered_map locationToBuilding; -static df::building_extents_type *getExtentTile(df::building_extents &extent, df::coord2d tile) +static df::building_extents_type *getExtentTile(const df::building::T_room &room, df::coord2d tile) { - if (!extent.extents) + if (!room.extents) return NULL; - int dx = tile.x - extent.x; - int dy = tile.y - extent.y; - if (dx < 0 || dy < 0 || dx >= extent.width || dy >= extent.height) + int dx = tile.x - room.x; + int dy = tile.y - room.y; + if (dx < 0 || dy < 0 || dx >= room.width || dy >= room.height) return NULL; - return &extent.extents[dx + dy*extent.width]; + return &room.extents[dx + dy*room.width]; } /* @@ -162,14 +166,14 @@ void buildings_onUpdate(color_ostream &out) for (size_t i = 0; i < job->items.size(); i++) { df::job_item_ref *iref = job->items[i]; - if (iref->role != df::job_item_ref::Reagent) + if (iref->role != df::job_role_type::Reagent) continue; df::job_item *item = vector_get(job->job_items.elements, iref->job_item_idx); if (!item) continue; // Convert Reagent to Hauled, while decrementing quantity item->quantity = std::max(0, item->quantity-1); - iref->role = df::job_item_ref::Hauled; + iref->role = df::job_role_type::Hauled; iref->job_item_idx = -1; } } @@ -219,7 +223,7 @@ static void add_building_to_all_zones(df::building* bld) static void add_zone_to_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -259,7 +263,7 @@ static void remove_building_from_zone(df::building* bld, df::building_civzonest* static void remove_zone_from_all_buildings(df::building* zone_as_building) { - if (zone_as_building->getType() != building_type::Civzone) + if (zone_as_building->getType() != df::building_type::Civzone) return; auto zone = strict_virtual_cast(zone_as_building); @@ -313,30 +317,39 @@ std::string Buildings::getName(df::building* building) return tmp; } +df::unit* Buildings::getOwner(df::building_civzonest* bld) +{ + return Units::get_cached_unit_by_global_id(bld->assigned_unit_id, bld->owner_unit_cached_index); +} + bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) { CHECK_NULL_POINTER(bld); - if (bld->assigned_unit == unit) + auto unit_id = unit ? unit->id : -1; + + if (bld->assigned_unit_id == unit_id) return true; - if (bld->assigned_unit) + if (bld->assigned_unit_id != -1) { - auto &blist = bld->assigned_unit->owned_buildings; - vector_erase_at(blist, linear_index(blist, bld)); - - if (auto spouse = df::unit::find(bld->assigned_unit->relationship_ids[df::unit_relationship_type::Spouse])) + if (auto old_unit = df::unit::find(bld->assigned_unit_id)) { - auto &blist = spouse->owned_buildings; + auto& blist = old_unit->owned_buildings; vector_erase_at(blist, linear_index(blist, bld)); + + if (auto spouse = df::unit::find(old_unit->relationship_ids[df::unit_relationship_type::Spouse])) + { + auto& blist = spouse->owned_buildings; + vector_erase_at(blist, linear_index(blist, bld)); + } } } - bld->assigned_unit = unit; + bld->assigned_unit_id = unit_id; if (unit) { - bld->assigned_unit_id = unit->id; unit->owned_buildings.push_back(bld); if (auto spouse = df::unit::find(unit->relationship_ids[df::unit_relationship_type::Spouse])) @@ -346,10 +359,8 @@ bool Buildings::setOwner(df::building_civzonest *bld, df::unit *unit) blist.push_back(bld); } } - else - { - bld->assigned_unit_id = -1; - } + + Units::get_cached_unit_by_global_id(unit_id, bld->owner_unit_cached_index); return true; } @@ -478,19 +489,20 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in // Type specific init switch (type) { - case building_type::Well: + using namespace df::enums::building_type; + case Well: { if (VIRTUAL_CAST_VAR(obj, df::building_wellst, bld)) obj->bucket_z = bld->z; break; } - case building_type::Workshop: + case Workshop: { if (VIRTUAL_CAST_VAR(obj, df::building_workshopst, bld)) obj->profile.max_general_orders = 5; break; } - case building_type::Furnace: + case Furnace: { if (VIRTUAL_CAST_VAR(obj, df::building_furnacest, bld)) { @@ -507,7 +519,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in break; } */ - case building_type::Trap: + case Trap: { if (VIRTUAL_CAST_VAR(obj, df::building_trapst, bld)) { @@ -516,46 +528,46 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in } break; } - case building_type::Floodgate: + case Floodgate: { if (VIRTUAL_CAST_VAR(obj, df::building_floodgatest, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateWall: + case GrateWall: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_wallst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::GrateFloor: + case GrateFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_grate_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsVertical: + case BarsVertical: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_verticalst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::BarsFloor: + case BarsFloor: { if (VIRTUAL_CAST_VAR(obj, df::building_bars_floorst, bld)) obj->gate_flags.bits.closed = true; break; } - case building_type::Bridge: + case Bridge: { if (VIRTUAL_CAST_VAR(obj, df::building_bridgest, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.raised = false; break; } - case building_type::Weapon: + case Weapon: { if (VIRTUAL_CAST_VAR(obj, df::building_weaponst, bld)) - obj->gate_flags.bits.closed = false; + obj->gate_flags.bits.retracted = false; break; } default: @@ -602,6 +614,23 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, return false; case SiegeEngine: + { + using namespace df::enums::siegeengine_type; + + switch((df::siegeengine_type)subtype) + { + case df::siegeengine_type::BoltThrower: + size = df::coord2d(1, 1); + center = df::coord2d(0, 0); + break; + default: + size = df::coord2d(3,3); + center = df::coord2d(1,1); + break; + } + return false; + } + case Windmill: case Wagon: size = df::coord2d(3,3); @@ -711,20 +740,20 @@ bool Buildings::getCorrectSize(df::coord2d &size, df::coord2d ¢er, } } -static void init_extents(df::building_extents *ext, const df::coord &pos, +static void init_extents(df::building::T_room &room, const df::coord &pos, const df::coord2d &size) { - ext->extents = new df::building_extents_type[size.x*size.y]; - ext->x = pos.x; - ext->y = pos.y; - ext->width = size.x; - ext->height = size.y; + room.extents = new df::building_extents_type[size.x*size.y]; + room.x = pos.x; + room.y = pos.y; + room.width = size.x; + room.height = size.y; - memset(ext->extents, 1, size.x*size.y); + memset(room.extents, 1, size.x*size.y); } bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, - df::building_extents *ext, + df::building *bld, bool create_ext, bool allow_occupied, bool allow_wall, @@ -740,9 +769,9 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, df::building_extents_type *etile = NULL; // Exclude using extents - if (ext && ext->extents) + if (bld && bld->room.extents) { - etile = getExtentTile(*ext, tile); + etile = getExtentTile(bld->room, tile); if (!etile || !*etile) continue; } @@ -776,13 +805,13 @@ bool Buildings::checkFreeTiles(df::coord pos, df::coord2d size, found_any = true; else { - if (!ext || !create_ext) + if (!bld || !create_ext) return false; - if (!ext->extents) + if (!bld->room.extents) { - init_extents(ext, pos, size); - etile = getExtentTile(*ext, tile); + init_extents(bld->room, pos, size); + etile = getExtentTile(bld->room, tile); } if (!etile) @@ -813,10 +842,10 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, if (force_extents && !bld->room.extents) { // populate the room structure if it's not set already - init_extents(&bld->room, psize.first, psize.second); + init_extents(bld->room, psize.first, psize.second); } - return Buildings::checkFreeTiles(psize.first, psize.second, &bld->room, + return Buildings::checkFreeTiles(psize.first, psize.second, bld, can_change && bld->isExtentShaped(), !bld->isSettingOccupancy(), bld->getType() == @@ -824,14 +853,18 @@ static bool checkBuildingTiles(df::building *bld, bool can_change, !bld->isActual()); } -int Buildings::countExtentTiles(df::building_extents *ext, int defval) +int Buildings::countExtentTiles(df::building *bld, int defval) { - if (!ext || !ext->extents) + CHECK_NULL_POINTER(bld); + + auto & room = bld->room; + + if (!room.extents) return defval; int cnt = 0; - for (int i = 0; i < ext->width * ext->height; i++) - if (ext->extents[i]) + for (int i = 0; i < room.width * room.height; i++) + if (room.extents[i]) cnt++; return cnt; } @@ -881,7 +914,7 @@ static int computeMaterialAmount(df::building *bld) int cnt = size.x * size.y; if (bld->room.extents && bld->isExtentShaped()) - cnt = Buildings::countExtentTiles(&bld->room, cnt); + cnt = Buildings::countExtentTiles(bld, cnt); return cnt/4 + 1; } @@ -911,10 +944,9 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) auto type = bld->getType(); - using namespace df::enums::building_type; - switch (type) { + using namespace df::enums::building_type; case WaterWheel: { auto obj = (df::building_water_wheelst*)bld; @@ -953,7 +985,7 @@ bool Buildings::setSize(df::building *bld, df::coord2d size, int direction) bool ok = checkBuildingTiles(bld, true); - if (type != Construction) + if (type != df::building_type::Construction) bld->setMaterialAmount(computeMaterialAmount(bld)); return ok; @@ -1158,7 +1190,7 @@ bool Buildings::constructWithItems(df::building *bld, std::vector ite for (size_t i = 0; i < items.size(); i++) { - Job::attachJobItem(job, items[i], df::job_item_ref::Hauled); + Job::attachJobItem(job, items[i], df::job_role_type::Hauled); if (items[i]->getType() == item_type::BOULDER) rough = true; @@ -1224,15 +1256,9 @@ bool Buildings::constructWithFilters(df::building *bld, std::vectorsquad_room_info) + for (auto room_info : zone->squad_room_info) { - int32_t squad_id = room_info->squad_id; - - df::squad* squad = df::squad::find(squad_id); - - //if this is null, something has gone just *terribly* wrong - if (squad) - { + if (auto squad = df::squad::find(room_info->squad_id)) { for (int i=(int)squad->rooms.size() - 1; i >= 0; i--) { if (squad->rooms[i]->building_id == zone->id) @@ -1406,7 +1432,7 @@ bool Buildings::deconstruct(df::building *bld) for (int i = ui_look_list->size()-1; i >= 0; --i) { auto item = (*ui_look_list)[i]; - if (item->type == df::lookinfost::Building && + if (item->type == df::look_info_type::Building && item->data.building.bld_id == id) { vector_erase_at(*ui_look_list, i); @@ -1652,7 +1678,7 @@ StockpileIterator& StockpileIterator::operator++() { // If the current item isn't properly stored, move on to the next. item = df::item::find(block->items[current]); - if (!item->flags.bits.on_ground) { + if (!item || !item->flags.bits.on_ground) { continue; } diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index 3c641e79b7..548ef98d87 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -52,6 +52,13 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; +std::string Burrows::getName(df::burrow* burrow) +{ + CHECK_NULL_POINTER(burrow); + return burrow->name.empty() ? fmt::format("Burrow {}", burrow->id + 1) : burrow->name; +} + + df::burrow *Burrows::findByName(std::string name, bool ignore_final_plus) { auto &vec = df::burrow::get_vector(); @@ -99,8 +106,6 @@ bool Burrows::isAssignedUnit(df::burrow *burrow, df::unit *unit) void Burrows::setAssignedUnit(df::burrow *burrow, df::unit *unit, bool enable) { - using df::global::plotinfo; - CHECK_NULL_POINTER(unit); CHECK_NULL_POINTER(burrow); @@ -199,7 +204,9 @@ df::block_burrow *Burrows::getBlockMask(df::burrow *burrow, df::map_block *block prev->next = link; df::coord base(world->map.region_x*3,world->map.region_y*3,world->map.region_z); - df::coord pos = base + block->map_pos/16; + // map_pos x/y are tile coordinates while z is already a z-level + df::coord pos(base.x + block->map_pos.x/16, + base.y + block->map_pos.y/16, base.z + block->map_pos.z); burrow->block_x.push_back(pos.x); burrow->block_y.push_back(pos.y); @@ -220,7 +227,9 @@ bool Burrows::deleteBlockMask(df::burrow *burrow, df::map_block *block, df::bloc return false; df::coord base(world->map.region_x*3,world->map.region_y*3,world->map.region_z); - df::coord pos = base + block->map_pos/16; + // map_pos x/y are tile coordinates while z is already a z-level + df::coord pos(base.x + block->map_pos.x/16, + base.y + block->map_pos.y/16, base.z + block->map_pos.z); destroyBurrowMask(mask); diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 832ffeb0be..01b89720cf 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -1,3 +1,4 @@ +#include "Error.h" #include "Internal.h" #include "modules/DFSDL.h" @@ -6,6 +7,9 @@ #include "PluginManager.h" #include +#include + +#include #ifdef WIN32 # include @@ -21,6 +25,17 @@ using std::vector; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; + +DFLibrary* DFHack::DFSDL::obtain_library_handle() +{ + return g_sdl_handle; +} + +DFLibrary* DFHack::DFSDL::obtain_image_library_handle() +{ + return g_sdl_image_handle; +} + static const vector SDL_LIBS { #ifdef WIN32 "SDL2.dll" @@ -58,6 +73,14 @@ void (*g_SDL_free)(void *); SDL_PixelFormat* (*g_SDL_AllocFormat)(uint32_t pixel_format) = nullptr; SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int height, int depth, uint32_t format) = nullptr; int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; +char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; +char* (*g_SDL_GetBasePath)() = nullptr; +uint32_t (*g_SDL_GetMouseState)(int* x, int* y) = nullptr; +void (*g_SDL_RenderWindowToLogical)(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); +void (*g_SDL_RenderLogicalToWindow)(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); + +SDL_Keycode (*g_SDL_GetKeyFromName)(const char* name) = nullptr; +const char* (*g_SDL_GetKeyName)(SDL_Keycode key) = nullptr; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -101,6 +124,13 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_AllocFormat); bind(g_sdl_handle, SDL_CreateRGBSurfaceWithFormat); bind(g_sdl_handle, SDL_ShowSimpleMessageBox); + bind(g_sdl_handle, SDL_GetPrefPath); + bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetKeyFromName); + bind(g_sdl_handle, SDL_GetKeyName); + bind(g_sdl_handle, SDL_GetMouseState); + bind(g_sdl_handle, SDL_RenderWindowToLogical); + bind(g_sdl_handle, SDL_RenderLogicalToWindow); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -175,12 +205,46 @@ SDL_Surface* DFSDL::DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, return g_SDL_CreateRGBSurfaceWithFormat(flags, width, height, depth, format); } +char* DFSDL::DFSDL_GetPrefPath(const char* org, const char* app) +{ + return g_SDL_GetPrefPath(org, app); +} + +char* DFSDL::DFSDL_GetBasePath() +{ + return g_SDL_GetBasePath(); +} + +uint32_t DFSDL::DFSDL_GetMouseState(int* x, int* y) { + return g_SDL_GetMouseState(x, y); +} + +void DFSDL::DFSDL_RenderWindowToLogical(SDL_Renderer *renderer, int windowX, int windowY, float *logicalX, float *logicalY) { + g_SDL_RenderWindowToLogical(renderer, windowX, windowY, logicalX, logicalY); +} + +void DFSDL::DFSDL_RenderLogicalToWindow(SDL_Renderer *renderer, float logicalX, float logicalY, int *windowX, int *windowY) { + g_SDL_RenderLogicalToWindow(renderer, logicalX, logicalY, windowX, windowY); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; return g_SDL_ShowSimpleMessageBox(flags, title, message, window); } +SDL_Keycode DFSDL::DFSDL_GetKeyFromName(const char* name) { + if (!g_SDL_GetKeyFromName) + return SDLK_UNKNOWN; + return g_SDL_GetKeyFromName(name); +} + +const char* DFSDL::DFSDL_GetKeyName(SDL_Keycode key) { + if (!g_SDL_GetKeyName) + return ""; + return g_SDL_GetKeyName(key); +} + // convert tabs to spaces so they don't get converted to '?' static char * tabs_to_spaces(char *str) { for (char *c = str; *c; ++c) { @@ -251,3 +315,25 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { } return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } + +// Queue to run callbacks on the render thread. +// Semantics loosely based on SDL3's SDL_RunOnMainThread +static std::recursive_mutex render_cb_lock; +static std::vector> render_cb_queue; + +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb) { + std::lock_guard l(render_cb_lock); + render_cb_queue.push_back(std::move(cb)); +} + +DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { + static decltype(render_cb_queue) local_queue; + { + std::lock_guard l(render_cb_lock); + std::swap(local_queue, render_cb_queue); + } + for (auto& cb : local_queue) { + cb(); + } + local_queue.clear(); +} diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index b5892bbb16..9c61b2a7fc 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -1,11 +1,30 @@ -#include "Internal.h" - #include "modules/DFSteam.h" #include "Debug.h" #include "PluginManager.h" +#include "ColorText.h" +#include "Core.h" + +#include +#include +#include +#include +#include + #include "df/gamest.h" +#include + +#ifdef WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#endif namespace DFHack { @@ -35,17 +54,20 @@ bool (*g_SteamAPI_RestartAppIfNecessary)(uint32_t unOwnAppID) = nullptr; void* (*g_SteamInternal_FindOrCreateUserInterface)(int, const char*) = nullptr; bool (*g_SteamAPI_ISteamApps_BIsAppInstalled)(void *iSteamApps, uint32_t appID) = nullptr; -static void bind_all(color_ostream& out, DFLibrary* handle) { -#define bind(name) \ - if (!handle) { \ - g_##name = nullptr; \ - } else { \ - g_##name = (decltype(g_##name))LookupPlugin(handle, #name); \ - if (!g_##name) { \ - WARN(dfsteam, out).print("steam library function not found: " #name "\n"); \ - } \ +template +static void bind_(color_ostream& out, DFLibrary* handle, const char* name, Ptr& func_ptr) { + if (!handle) { + func_ptr = nullptr; + } else { + func_ptr = (Ptr)LookupPlugin(handle, name); + if (!func_ptr) { + WARN(dfsteam, out).print("steam library function not found: {}\n", name); } + } +} +static void bind_all(color_ostream& out, DFLibrary* handle) { +#define bind(name) bind_(out, handle, #name, g_##name) bind(SteamAPI_Init); bind(SteamAPI_Shutdown); bind(SteamAPI_GetHSteamUser); @@ -56,6 +78,16 @@ static void bind_all(color_ostream& out, DFLibrary* handle) { #undef bind } +static void unbind_all() +{ + g_SteamAPI_Init = nullptr; + g_SteamAPI_Shutdown = nullptr; + g_SteamAPI_GetHSteamUser = nullptr; + g_SteamInternal_FindOrCreateUserInterface = nullptr; + g_SteamAPI_RestartAppIfNecessary = nullptr; + g_SteamAPI_ISteamApps_BIsAppInstalled = nullptr; +} + bool DFSteam::init(color_ostream& out) { char *steam_client_launch = getenv("SteamClientLaunch"); if (!steam_client_launch || strncmp(steam_client_launch, "1", 2) != 0) { @@ -84,7 +116,7 @@ bool DFSteam::init(color_ostream& out) { return true; } -void DFSteam::cleanup(color_ostream& out) { +void DFSteam::cleanup() { if (!g_steam_handle) return; @@ -94,15 +126,12 @@ void DFSteam::cleanup(color_ostream& out) { ClosePlugin(g_steam_handle); g_steam_handle = nullptr; - bind_all(out, nullptr); + unbind_all(); g_steam_initialized = false; } #ifdef WIN32 -#include -#include -#include static bool is_running_on_wine() { typedef const char* (CDECL wine_get_version)(void); @@ -157,13 +186,13 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - static LPCWSTR procname = L"hack/launchdf.exe"; + auto procpath = Core::getInstance().getHackPath() / "launchdf.exe"; static const char * env = "\0"; // note that the environment must be explicitly zeroed out and not NULL, // otherwise the launched process will inherit this process's environment, // and the Steam API in the launchdf process will think it is in DF's context. - BOOL res = CreateProcessW(procname, + BOOL res = CreateProcessW(procpath.wstring().c_str(), NULL, NULL, NULL, FALSE, 0, (LPVOID)env, NULL, &si, &pi); return !!res; @@ -179,8 +208,8 @@ static bool findProcess(color_ostream& out, std::string name, pid_t &pid) { command += name; FILE *cmd_pipe = popen(command.c_str(), "r"); if (!cmd_pipe) { - WARN(dfsteam, out).print("failed to exec '%s' (error: %d)\n", - command.c_str(), errno); + WARN(dfsteam, out).print("failed to exec '{}' (error: {})\n", + command, errno); return false; } @@ -204,13 +233,14 @@ static bool launchDFHack(color_ostream& out) { pid = fork(); if (pid == -1) { - WARN(dfsteam, out).print("failed to fork (error: %d)\n", errno); + WARN(dfsteam, out).print("failed to fork (error: {})\n", errno); return false; } else if (pid == 0) { // child process - static const char * command = "hack/launchdf"; + auto procpath = Core::getInstance().getHackPath() / "launchdf"; + auto command = procpath.string(); unsetenv("SteamAppId"); - execl(command, command, NULL); + execl(command.c_str(), command.c_str(), NULL); _exit(EXIT_FAILURE); } @@ -248,5 +278,5 @@ void DFSteam::launchSteamDFHackIfNecessary(color_ostream& out) { } bool ret = launchDFHack(out); - DEBUG(dfsteam, out).print("launching DFHack via Steam: %s\n", ret ? "successful" : "unsuccessful"); + DEBUG(dfsteam, out).print("launching DFHack via Steam: {}\n", ret ? "successful" : "unsuccessful"); } diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 5a24f1174a..2a5f678a0b 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -2,6 +2,7 @@ #include "Console.h" #include "Debug.h" #include "VTableInterpose.h" +#include "MemAccess.h" #include "modules/Buildings.h" #include "modules/Constructions.h" @@ -30,6 +31,7 @@ #include "df/report.h" #include "df/plotinfost.h" #include "df/unit.h" +#include "df/unit_action.h" #include "df/unit_flags1.h" #include "df/unit_inventory_item.h" #include "df/unit_report_type.h" @@ -70,7 +72,10 @@ static int32_t eventLastTick[EventType::EVENT_MAX]; static const int32_t ticksPerYear = 403200; void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler) { - DEBUG(log).print("registering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("registering handler {} from plugin {} for event {}\n", + reinterpret_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); handlers[e].insert(pair(handler.plugin, handler)); } @@ -86,7 +91,9 @@ int32_t DFHack::EventManager::registerTick(EventHandler handler, int32_t when, b } handler.freq = when; tickQueue.insert(pair(handler.freq, handler)); - DEBUG(log).print("registering handler %p from plugin %s for event TICK\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str()); + DEBUG(log).print("registering handler {} from plugin {} for event TICK\n", + reinterpret_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : ""); handlers[EventType::TICK].insert(pair(handler.plugin,handler)); return when; } @@ -112,7 +119,10 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl i++; continue; } - DEBUG(log).print("unregistering handler %p from plugin %s for event %d\n", handler.eventHandler, !handler.plugin ? "" : handler.plugin->getName().c_str(), e); + DEBUG(log).print("unregistering handler {} from plugin {} for event {}\n", + reinterpret_cast(handler.eventHandler), + handler.plugin ? handler.plugin->getName() : "", + static_cast(e)); i = handlers[e].erase(i); if ( e == EventType::TICK ) removeFromTickQueue(handler); @@ -120,7 +130,8 @@ void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handl } void DFHack::EventManager::unregisterAll(Plugin* plugin) { - DEBUG(log).print("unregistering all handlers for plugin %s\n", !plugin ? "" : plugin->getName().c_str()); + DEBUG(log).print("unregistering all handlers for plugin {}\n", + plugin ? plugin->getName() : ""); for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) { if ( (*i).first != plugin ) break; @@ -214,12 +225,28 @@ static std::vector startedJobs; struct JobCompleteData { int32_t id; int32_t completion_timer; - uint32_t flags_bits_repeat; + df::job_flags flags; }; +/* + * Some jobs are driven by the worker's Job unit action timer rather than by + * job.completion_timer, which stays -1 for the job's entire lifetime (e.g. + * HarvestPlants). DF deletes these jobs in the same tick the action expires, + * so they are never observed with completion_timer == 0. Snapshot the worker's + * action timer for claimed jobs that are in their work phase but whose + * completion_timer isn't counting down; when such a job disappears, a timer + * that was within reach of expiring during the elapsed ticks indicates a + * completion rather than a cancellation. + */ +struct ActionDrivenJobData { + int32_t unit_id; + int32_t unit_index; // cache for Units::get_cached_unit_by_global_id + int32_t action_timer; // -1 = worker's Job action not observed yet +}; static std::unordered_map seenJobs; static std::vector prevJobs; +static std::unordered_map actionDrivenJobs; //active units static unordered_set activeUnits; @@ -302,6 +329,7 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event startedJobs.clear(); seenJobs.clear(); prevJobs.clear(); + actionDrivenJobs.clear(); tickQueue.clear(); livingUnits.clear(); buildings.clear(); @@ -407,7 +435,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { CoreSuspender suspender; int32_t tick = df::global::world->frame_counter; - TRACE(log,out).print("processing events at tick %d\n", tick); + TRACE(log,out).print("processing events at tick {}\n", tick); auto &core = Core::getInstance(); auto &counters = core.perf_counters; @@ -550,10 +578,34 @@ static void manageJobCompletedEvent(color_ostream& out) { || (seenJob->general_refs.size() != job.general_refs.size())) { seenIt->second = Job::JobUniquePtr(Job::cloneJobStruct(&job, true)); } - } else if (job.completion_timer != -1) { + } else if (job.completion_timer != -1 || job.flags.bits.working) { // Restrict additions to seenJobs to jobs that we know have started. seenJobs.emplace(job.id, Job::JobUniquePtr(Job::cloneJobStruct(&job, true))); } + + if (job.completion_timer == -1 && job.flags.bits.working) { + auto ait = actionDrivenJobs.find(job.id); + if (ait == actionDrivenJobs.end()) { + if (df::unit* worker = Job::getWorker(&job)) + ait = actionDrivenJobs.emplace(job.id, + ActionDrivenJobData{worker->id, -1, -1}).first; + } + if (ait != actionDrivenJobs.end()) { + auto& data = ait->second; + df::unit* unit = Units::get_cached_unit_by_global_id(data.unit_id, data.unit_index); + if (!unit || unit->job.current_job != &job) { + // worker lost or swapped; re-register on a later poll + actionDrivenJobs.erase(ait); + } else { + for (auto action : unit->actions) { + if (action->type == unit_action_type::Job) { + data.action_timer = action->data.job.timer; + break; + } + } + } + } + } /* * We still need to push back all jobs to maintain the invariant of the * algorithm used with prevJobs and nowJobs. @@ -580,7 +632,7 @@ static void manageJobCompletedEvent(color_ostream& out) { * 1 in nowJobs is less than the smallest ID in prevJobs, * which breaks the algorithm. */ - nowJobs.emplace_back(job.id, job.completion_timer, (bool)job.flags.bits.repeat); + nowJobs.emplace_back(job.id, job.completion_timer, job.flags); } // Do we want this check? @@ -598,13 +650,13 @@ static void manageJobCompletedEvent(color_ostream& out) { df::job& job1 = *(*j).second; out.print("new job\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" "\n", job1.list_link->item, job1.id, job1.job_type, ENUM_ATTR(job_type, caption, job1.job_type), job1.flags.bits.working, job1.completion_timer, getWorkerID(&job1), tick0, tick1); } for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { @@ -612,13 +664,13 @@ static void manageJobCompletedEvent(color_ostream& out) { auto j = nowJobs.find((*i).first); if ( j == nowJobs.end() ) { out.print("job deallocated\n" - " location : 0x%X\n" - " id : %d\n" - " type : %d %s\n" - " working : %d\n" - " completion_timer : %d\n" - " workerID : %d\n" - " time : %d -> %d\n" + " location : {:#X}\n" + " id : {}\n" + " type : {} {}\n" + " working : {}\n" + " completion_timer : {}\n" + " workerID : {}\n" + " time : {} -> {}\n" ,job0.list_link == NULL ? 0 : job0.list_link->item, job0.id, job0.job_type, ENUM_ATTR(job_type, caption, job0.job_type), job0.flags.bits.working, job0.completion_timer, getWorkerID(&job0), tick0, tick1); continue; } @@ -630,14 +682,14 @@ static void manageJobCompletedEvent(color_ostream& out) { continue; out.print("job change\n" - " location : 0x%X -> 0x%X\n" - " id : %d -> %d\n" - " type : %d -> %d\n" - " type : %s -> %s\n" - " working : %d -> %d\n" - " completion timer : %d -> %d\n" - " workerID : %d -> %d\n" - " time : %d -> %d\n" + " location : {:#X} -> {:#X}\n" + " id : {} -> {}\n" + " type : {} -> {}\n" + " type : {} -> {}\n" + " working : {} -> {}\n" + " completion timer : {} -> {}\n" + " workerID : {} -> {}\n" + " time : {} -> {}\n" "\n", job0.list_link->item, job1.list_link->item, job0.id, job1.id, @@ -653,6 +705,7 @@ static void manageJobCompletedEvent(color_ostream& out) { auto prevIt = prevJobs.begin(); auto nowIt = nowJobs.begin(); + int32_t tick = df::global::world->frame_counter; /* * Iterate through two ordered sets, prevJobs and nowJobs, where job IDs in nowJobs are invariably * greater than or equal to job IDs in prevJobs. The algorithm maintains the invariant that for each @@ -664,7 +717,27 @@ static void manageJobCompletedEvent(color_ostream& out) { auto& prevJob = *prevIt; if (nowIt == nowJobs.end() || prevJob.id != nowIt->id) { // job ID is in prevJobs. ID does not exist in nowJobs. // recently finished or cancelled job - if (!prevJob.flags_bits_repeat && prevJob.completion_timer == 0) { + bool completed = prevJob.completion_timer == 0; + if (!completed) { + /* + * Jobs whose work is tracked by the worker's Job unit action + * are deleted the same tick they complete, before + * completion_timer ever reaches 0. If the last observed action + * timer was close enough to expiring that it could have hit 0 + * during the elapsed ticks, count it as a completion. + */ + auto ait = actionDrivenJobs.find(prevJob.id); + if (ait != actionDrivenJobs.end() + && prevJob.flags.bits.working + && !prevJob.flags.bits.suspend + && !prevJob.flags.bits.item_lost + && ait->second.action_timer >= 0 + && ait->second.action_timer <= tick - eventLastTick[EventType::JOB_COMPLETED]) { + completed = true; + } + } + actionDrivenJobs.erase(prevJob.id); + if (completed && !prevJob.flags.bits.repeat) { // It should be in seenJobs. auto seenIt = seenJobs.find(prevJob.id); if (seenIt != seenJobs.end()) { @@ -678,7 +751,7 @@ static void manageJobCompletedEvent(color_ostream& out) { } } else { // prevIt job ID and nowIt job ID are equal. // could have just finished if it's a repeat job - if (prevJob.flags_bits_repeat && prevJob.completion_timer == 0 + if (prevJob.flags.bits.repeat && prevJob.completion_timer == 0 && nowIt->completion_timer == -1) { // It should be in seenJobs. auto seenIt = seenJobs.find(prevJob.id); @@ -1126,6 +1199,11 @@ static void manageUnitAttackEvent(color_ostream& out) { multimap copy(handlers[EventType::UNIT_ATTACK].begin(), handlers[EventType::UNIT_ATTACK].end()); std::vector& reports = df::global::world->status.reports; size_t idx = df::report::binsearch_index(reports, lastReportUnitAttack, false); + if (idx >= reports.size()) + { + WARN(log, out).print("manageUnitAttackEvent: last reported unit attack lookup failed ({} -> {})\n", lastReportUnitAttack, idx); + return; + } // returns the index to the key equal to or greater than the key provided idx = reports[idx]->id == lastReportUnitAttack ? idx + 1 : idx; // we need the index after (where the new stuff is) @@ -1228,7 +1306,7 @@ static void manageUnitAttackEvent(color_ostream& out) { if ( reportStr.find("severed part") ) continue; if ( Once::doOnce("EventManager neither wound") ) { - out.print("%s, %d: neither wound: %s\n", __FILE__, __LINE__, reportStr.c_str()); + out.print("{}, {}: neither wound: {}\n", __FILE__, __LINE__, reportStr.c_str()); } } } @@ -1351,9 +1429,8 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, //if trying attack-defend pair and it fails to find attacker, try defend only InteractionData result = /*(InteractionData)*/ { std::string(), std::string(), -1, -1, -1, -1 }; if ( attackers.size() > 1 ) { -//out.print("%s,%d\n",__FILE__,__LINE__); if ( Once::doOnce("EventManager interaction ambiguous attacker") ) { - out.print("%s,%d: ambiguous attacker on report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{},{}: ambiguous attacker on report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } else if ( attackers.empty() ) { //out.print("%s,%d\n",__FILE__,__LINE__); @@ -1367,7 +1444,7 @@ static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, result.defender = defenders[0]->id; if ( defenders.size() > 1 ) { if ( Once::doOnce("EventManager interaction ambiguous defender") ) { - out.print("%s,%d: ambiguous defender: shouldn't happen. On report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + out.print("{}, {}: ambiguous defender: shouldn't happen. On report\n \'{}\'\n \'{}\'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text : "", defendEvent ? defendEvent->text : ""); } } result.attackVerb = attackVerb; @@ -1394,7 +1471,7 @@ static vector gatherRelevantUnits(color_ostream& out, df::report* r1, vector& units = reportToRelevantUnits[report->id]; if ( units.size() > 2 ) { if ( Once::doOnce("EventManager interaction too many relevant units") ) { - out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, report->text.c_str()); + out.print("{},{}: too many relevant units. On report\n \'{}\'\n", __FILE__, __LINE__, report->text); } } for (int & unit_id : units) diff --git a/library/modules/Filesystem.cpp b/library/modules/Filesystem.cpp index 2c275ceba8..226d7bc830 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -46,14 +46,22 @@ SOFTWARE. */ #include +#include +#include +#include #include +#include "modules/DFSDL.h" #include "modules/Filesystem.h" +#include "df/global_objects.h" +#include "df/init.h" + + using namespace DFHack; static bool initialized = false; -static std::string initial_cwd; +static std::filesystem::path initial_cwd; void Filesystem::init () { @@ -64,24 +72,23 @@ void Filesystem::init () } } -bool Filesystem::chdir (std::string path) +bool Filesystem::chdir (std::filesystem::path path) noexcept { Filesystem::init(); - return ::chdir(path.c_str()) == 0; + try + { + std::filesystem::current_path(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -std::string Filesystem::getcwd () +std::filesystem::path Filesystem::getcwd () { - char *path; - char buf[FILENAME_MAX]; - std::string result = ""; -#ifdef _WIN32 - if ((path = ::_getcwd(buf, FILENAME_MAX)) != NULL) -#else - if ((path = ::getcwd(buf, FILENAME_MAX)) != NULL) -#endif - result = buf; - return result; + return std::filesystem::current_path(); } bool Filesystem::restore_cwd () @@ -89,196 +96,169 @@ bool Filesystem::restore_cwd () return Filesystem::chdir(initial_cwd); } -std::string Filesystem::get_initial_cwd () +std::filesystem::path Filesystem::get_initial_cwd () { Filesystem::init(); return initial_cwd; } -bool Filesystem::mkdir (std::string path) +bool Filesystem::mkdir (std::filesystem::path path) noexcept { - int fail; -#ifdef _WIN32 - fail = ::_mkdir(path.c_str()); -#else - fail = ::mkdir(path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | - S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - return fail == 0; + try + { + std::filesystem::create_directory(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -static bool mkdir_recursive_impl (std::string path) +bool Filesystem::mkdir_recursive (std::filesystem::path path) noexcept { - size_t last_slash = path.find_last_of("/"); - if (last_slash != std::string::npos) + try { - std::string parent_path = path.substr(0, last_slash); - bool parent_exists = mkdir_recursive_impl(parent_path); - if (!parent_exists) - { - return false; - } + std::filesystem::create_directories(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; } - return (Filesystem::mkdir(path) || errno == EEXIST) && Filesystem::isdir(path); } -bool Filesystem::mkdir_recursive (std::string path) +bool Filesystem::rmdir (std::filesystem::path path) noexcept { -#ifdef _WIN32 - // normalize to forward slashes - std::replace(path.begin(), path.end(), '\\', '/'); -#endif - - if (path.size() > FILENAME_MAX) + try + { + std::filesystem::remove(path); + return true; + } + catch (std::filesystem::filesystem_error&) { - // path too long return false; } - - return mkdir_recursive_impl(path); } -bool Filesystem::rmdir (std::string path) +bool Filesystem::stat (std::filesystem::path path, std::filesystem::file_status &info) noexcept { - int fail; -#ifdef _WIN32 - fail = ::_rmdir(path.c_str()); -#else - fail = ::rmdir(path.c_str()); -#endif - return fail == 0; -} - -#ifdef _WIN32 -_filetype mode2type (unsigned short mode) { -#else -_filetype mode2type (mode_t mode) { -#endif - if (S_ISREG(mode)) - return FILETYPE_FILE; - else if (S_ISDIR(mode)) - return FILETYPE_DIRECTORY; - else if (S_ISLNK(mode)) - return FILETYPE_LINK; - else if (S_ISSOCK(mode)) - return FILETYPE_SOCKET; - else if (S_ISCHR(mode)) - return FILETYPE_CHAR_DEVICE; - else if (S_ISBLK(mode)) - return FILETYPE_BLOCK_DEVICE; - else - return FILETYPE_UNKNOWN; + try + { + info = std::filesystem::status(path); + return true; + } + catch (std::filesystem::filesystem_error&) + { + return false; + } } -bool Filesystem::stat (std::string path, STAT_STRUCT &info) +bool Filesystem::exists (std::filesystem::path path) noexcept { - return (STAT_FUNC(path.c_str(), &info)) == 0; + std::error_code ec; + auto r = std::filesystem::exists(path, ec); + if (ec) + return false; + return r; } -bool Filesystem::exists (std::string path) +bool Filesystem::isfile(std::filesystem::path path) noexcept { - STAT_STRUCT info; - return Filesystem::stat(path, info); + std::error_code ec; + // is_regular_file() also checks for existence. + auto r = std::filesystem::is_regular_file(path, ec); + if (ec) + return false; + return r; } -_filetype Filesystem::filetype (std::string path) +bool Filesystem::isdir (std::filesystem::path path) noexcept { - STAT_STRUCT info; - if (!Filesystem::stat(path, info)) - return FILETYPE_NONE; - return mode2type(info.st_mode); + std::error_code ec; + // is_directory() also checks for existence. + auto r = std::filesystem::is_directory(path, ec); + if (ec) + return false; + return r; } -bool Filesystem::isfile (std::string path) +std::time_t Filesystem::mtime (std::filesystem::path path) noexcept { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_FILE; + try + { + auto ftime = std::filesystem::last_write_time(path); + auto t = ftime.time_since_epoch().count(); + return t; + } + catch (std::filesystem::filesystem_error&) + { + return -1; + } } -bool Filesystem::isdir (std::string path) +int Filesystem::listdir (std::filesystem::path dir, std::vector &files) noexcept { - return Filesystem::exists(path) && Filesystem::filetype(path) == FILETYPE_DIRECTORY; + try { + for (auto const& dirent : std::filesystem::directory_iterator(dir)) + { + files.push_back(dirent.path().filename()); + } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; + } } -#define DEFINE_STAT_TIME_WRAPPER(attr) \ -int64_t Filesystem::attr (std::string path) \ -{ \ - STAT_STRUCT info; \ - if (!Filesystem::stat(path, info)) \ - return -1; \ - return (int64_t)info.st_##attr; \ +int Filesystem::listdir_recursive (std::filesystem::path dir, std::map &files, + int depth /* = 10 */, bool include_prefix /* = true */) noexcept +{ + try { + for (auto i = std::filesystem::recursive_directory_iterator(dir); + i != std::filesystem::recursive_directory_iterator(); + ++i) + { + if (i->is_directory() && i.depth() >= depth) + i.disable_recursion_pending(); + auto p = i->path(); + auto pp = include_prefix ? p : std::filesystem::relative(p, dir); + files.emplace(pp, std::filesystem::is_directory(p)); + } + return 0; + } + catch (std::filesystem::filesystem_error&) + { + return 1; + } } -DEFINE_STAT_TIME_WRAPPER(atime) -DEFINE_STAT_TIME_WRAPPER(ctime) -DEFINE_STAT_TIME_WRAPPER(mtime) - -#undef DEFINE_STAT_TIME_WRAPPER - -int Filesystem::listdir (std::string dir, std::vector &files) +std::filesystem::path Filesystem::canonicalize(std::filesystem::path p) noexcept { - DIR *dp; - struct dirent *dirp; - if((dp = opendir(dir.c_str())) == NULL) + try { - return errno; + return std::filesystem::weakly_canonical(p); } - while ((dirp = readdir(dp)) != NULL) { - files.push_back(std::string(dirp->d_name)); + catch (std::filesystem::filesystem_error&) + { + return p; } - closedir(dp); - return 0; } -// prefix is the top-level dir where we started recursing -// path is the relative path under the prefix; must be empty or end in a '/' -// files is the output list of files and directories (bool == true for dir) -// depth is the remaining dir depth to recurse into. function returns -1 if -// we haven't finished recursing when we run out of depth. -// include_prefix controls whether the directory where we started recursing is -// included in the filenames returned in files. -static int listdir_recursive_impl (std::string prefix, std::string path, - std::map &files, int depth, bool include_prefix) +std::filesystem::path Filesystem::getInstallDir() noexcept { - if (depth < 0) - return -1; - std::string prefixed_path = prefix + "/" + path; - std::vector curdir_files; - int err = Filesystem::listdir(prefixed_path, curdir_files); - if (err) - return err; - bool out_of_depth = false; - for (auto file = curdir_files.begin(); file != curdir_files.end(); ++file) - { - if (*file == "." || *file == "..") - continue; - std::string prefixed_file = prefixed_path + *file; - std::string path_file = path + *file; - if (Filesystem::isdir(prefixed_file)) - { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, true)); - - if (depth == 0) - { - out_of_depth = true; - continue; - } - - err = listdir_recursive_impl(prefix, path_file + "/", files, depth - 1, include_prefix); - if (err) - return err; - } - else - { - files.insert(std::pair(include_prefix ? prefixed_file : path_file, false)); - } - } - return out_of_depth ? -1 : 0; + return std::filesystem::path{ DFSDL::DFSDL_GetBasePath() }; } -int Filesystem::listdir_recursive (std::string dir, std::map &files, - int depth /* = 10 */, bool include_prefix /* = true */) +std::filesystem::path Filesystem::getBaseDir() noexcept { - if (dir.size() && dir[dir.size()-1] == '/') - dir.resize(dir.size()-1); - return listdir_recursive_impl(dir, "", files, depth, include_prefix); + auto getsavebase = []() { + // assume portable mode is _on_ if init is missing + if (!df::global::init || df::global::init->media.flag.is_set(df::enums::init_media_flags::PORTABLE_MODE)) + return DFSDL::DFSDL_GetBasePath(); + else + return DFSDL::DFSDL_GetPrefPath("Bay 12 Games", "Dwarf Fortress"); + }; + return std::filesystem::path{ getsavebase() }; } diff --git a/library/modules/Graphic.cpp b/library/modules/Graphic.cpp deleted file mode 100644 index b55ee83ed4..0000000000 --- a/library/modules/Graphic.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mr�zek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - - -#include "Internal.h" - -#include -#include -#include -#include -#include -using namespace std; - -#include "modules/Graphic.h" -#include "Error.h" -#include "VersionInfo.h" -#include "MemAccess.h" -#include "MiscUtils.h" -#include "ModuleFactory.h" -#include "Core.h" - -using namespace DFHack; - -std::unique_ptr DFHack::createGraphic() -{ - return std::make_unique(); -} - -bool Graphic::Register(DFTileSurface* (*func)(int,int)) -{ - funcs.push_back(func); - return true; -} - -bool Graphic::Unregister(DFTileSurface* (*func)(int,int)) -{ - if ( funcs.empty() ) return false; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - if ( *it == func ) - { - funcs.erase(it); - return true; - } - it++; - } - - return false; -} - -// This will return first DFTileSurface it can get (or NULL if theres none) -DFTileSurface* Graphic::Call(int x, int y) -{ - if ( funcs.empty() ) return NULL; - - DFTileSurface* temp = NULL; - - vector::iterator it = funcs.begin(); - while ( it != funcs.end() ) - { - temp = (*it)(x,y); - if ( temp != NULL ) - { - return temp; - } - it++; - } - - return NULL; -} diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e9dd0dda9..96a7ab5472 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -30,7 +30,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "Debug.h" #include "PluginManager.h" @@ -84,13 +83,13 @@ distribution. #include "df/route_stockpile_link.h" #include "df/soundst.h" #include "df/stop_depart_condition.h" -#include "df/ui_unit_view_mode.h" #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_dungeonmodest.h" #include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_legendsst.h" +#include "df/viewscreen_new_arenast.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_setupdwarfgamest.h" #include "df/viewscreen_titlest.h" @@ -100,15 +99,15 @@ distribution. #include #include #include +#include using std::string; using std::vector; using namespace DFHack; -const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size -const int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE -const int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS -const int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS +static constexpr int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE +static constexpr int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 2 sec at 50 GFPS +static constexpr int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 40 sec at 50 GFPS namespace DFHack { @@ -135,7 +134,7 @@ static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int id } */ -static std::string getNameChunk(virtual_identity *id, int start, int end) +static std::string getNameChunk(const virtual_identity *id, int start, int end) { if (!id) return "UNKNOWN"; @@ -152,9 +151,10 @@ static std::string getNameChunk(virtual_identity *id, int start, int end) */ typedef void (*getFocusStringsHandler)(std::string &str, std::vector &strList, df::viewscreen *screen); -static std::map getFocusStringsHandlers; +static std::map getFocusStringsHandlers; #define VIEWSCREEN(name) df::viewscreen_##name##st + #define DEFINE_GET_FOCUS_STRING_HANDLER(screen_type) \ static void getFocusStrings_##screen_type(const std::string &baseFocus, std::vector &focusStrings, VIEWSCREEN(screen_type) *screen);\ DFHACK_STATIC_ADD_TO_MAP(\ @@ -189,6 +189,18 @@ DEFINE_GET_FOCUS_STRING_HANDLER(new_region) focusStrings.push_back(baseFocus); } +DEFINE_GET_FOCUS_STRING_HANDLER(new_arena) +{ + if (screen->raw_load) + focusStrings.push_back(baseFocus + "/Loading"); + else if (screen->doing_mods) + focusStrings.push_back(baseFocus + "/Mods"); + + if (focusStrings.empty()) + focusStrings.push_back(baseFocus); + +} + DEFINE_GET_FOCUS_STRING_HANDLER(choose_start_site) { if (screen->doing_site_finder) @@ -763,11 +775,6 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.squad_supplies.open) { - newFocusString = baseFocus; - newFocusString += "/SquadSupplies"; - focusStrings.push_back(newFocusString); - } if (game->main_interface.squads.open) { newFocusString = baseFocus; newFocusString += "/Squads"; @@ -817,11 +824,12 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.settings.current_mode); - if (game->main_interface.settings.doing_custom_settings) - newFocusString += "/CustomSettings"; - else - newFocusString += "/Default"; - + if (game->main_interface.settings.current_mode == df::settings_tab_type::DIFFICULTY) { + if (game->main_interface.settings.doing_custom_settings) + newFocusString += "/CustomSettings"; + else + newFocusString += "/Default"; + } focusStrings.push_back(newFocusString); } if (game->main_interface.adventure.aim_projectile.open) { @@ -848,6 +856,9 @@ static void add_main_interface_focus_strings(const string &baseFocus, vectormain_interface.adventure.jump.open) { focusStrings.push_back(baseFocus + "/Jump"); } + if (game->main_interface.adventure.look.open) { + focusStrings.push_back(baseFocus + "/Look"); + } if (game->main_interface.adventure.movement_options.open) { focusStrings.push_back(baseFocus + "/MovementOptions"); } @@ -958,7 +969,7 @@ std::vector Gui::getFocusStrings(df::viewscreen* top) } } - if (virtual_identity *id = virtual_identity::get(top)) + if (const virtual_identity *id = virtual_identity::get(top)) { std::string name = getNameChunk(id, 11, 2); @@ -1055,6 +1066,33 @@ bool Gui::any_job_hotkey(df::viewscreen *top) || workshop_job_hotkey(top); } +// Resolves the selection reported by a DFHack viewscreen, if it can be +// queried safely on this thread. Returns the selection (possibly null) when +// the question is answered, or std::nullopt when the caller should continue +// to the native UI checks. +// +// getSelected*() on a lua-backed screen enters Lua through CoreSuspender, +// which asserts or deadlocks on the render thread, where hotkey guards run. +// A focused lua screen owns the selection, so we must report none; a +// defocused lua screen is transparent to the UI below, so the native checks +// still apply. +template +static std::optional dfscreen_selection(df::viewscreen *top, + R (dfhack_viewscreen::*method)()) +{ + auto dfscreen = dfhack_viewscreen::try_cast(top); + if (!dfscreen) + return std::nullopt; + + if (!dfscreen->is_lua_screen() || !Core::getInstance().isRenderThread()) + return (dfscreen->*method)(); + + if (dfscreen->isFocused()) + return nullptr; + + return std::nullopt; +} + df::job *Gui::getAnyWorkshopJob(df::viewscreen *top) { auto bld = getAnyBuilding(top); if (!bld) @@ -1074,8 +1112,8 @@ df::job *Gui::getSelectedWorkshopJob(color_ostream &out, bool quiet) { } df::job *Gui::getAnyJob(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedJob(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedJob)) + return *sel; if (matchFocusString("dwarfmode/Info/JOBS")) { auto &cri_job = game->main_interface.info.jobs.cri_job; @@ -1114,8 +1152,8 @@ df::job *Gui::getSelectedJob(color_ostream &out, bool quiet) df::unit *Gui::getAnyUnit(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedUnit(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedUnit)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::UNIT) @@ -1407,8 +1445,8 @@ df::item *Gui::getAnyItem(df::viewscreen *top) { using df::global::game; - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedItem(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedItem)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::ITEM) @@ -1558,8 +1596,8 @@ bool Gui::any_stockpile_hotkey(df::viewscreen* top) } df::building_stockpilest* Gui::getAnyStockpile(df::viewscreen* top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedStockpile(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedStockpile)) + return *sel; if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::STOCKPILE) return game->main_interface.stockpile.cur_bld; @@ -1581,8 +1619,8 @@ bool Gui::any_civzone_hotkey(df::viewscreen* top) { } df::building_civzonest *Gui::getAnyCivZone(df::viewscreen* top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedCivZone(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedCivZone)) + return *sel; if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::ZONE) return game->main_interface.civzone.cur_bld; @@ -1601,8 +1639,8 @@ df::building_civzonest *Gui::getSelectedCivZone(color_ostream &out, bool quiet) df::building *Gui::getAnyBuilding(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedBuilding(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedBuilding)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::BUILDING) @@ -1676,8 +1714,8 @@ df::plant *Gui::getAnyPlant(df::viewscreen *top) { using df::global::cursor; - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedPlant(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedPlant)) + return *sel; if (Gui::dwarfmode_hotkey(top) && has_cursor()) { @@ -1830,7 +1868,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce return -1; else if (message.empty()) { - Core::printerr("Empty announcement %u\n", type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(type)); // DF would print this to errorlog.txt return -1; } @@ -1876,7 +1914,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce new_report->text = message; new_report->color = color; new_report->bright = bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->pos = pos; new_report->id = world->status.next_report_id++; new_report->year = *df::global::cur_year; @@ -1916,18 +1954,6 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce world->status.display_timer = ANNOUNCE_DISPLAY_TIME; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - return world->status.reports.size() - 1; } @@ -1945,6 +1971,8 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, df::report auto alert_type = announcement_alert_type::NONE; switch (slot) { + case unit_report_type::NONE: /* should never happen? */ + return false; case unit_report_type::Combat: world->status.flags.bits.combat = true; alert_type = announcement_alert_type::COMBAT; @@ -2018,14 +2046,6 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright) auto &popups = world->status.popups; popups.push_back(popup); - // Delete excess popups - while (popups.size() > MAX_REPORTS_SIZE) - { - if (popups[0] != NULL) - delete popups[0]; - popups.erase(popups.begin()); - } - Gui::MTB_clean(&world->status.mega_text); Gui::MTB_parse(&world->status.mega_text, popups[0]->text); Gui::MTB_set_width(&world->status.mega_text); @@ -2056,17 +2076,17 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Based on reverse-engineering of "make_announcement" FUN_1400574e0 (v50.11 win64 Steam) if (!world->allow_announcements) { - DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n{}\n", message); return false; } else if (!is_valid_enum_item(info.type) || info.type == df::announcement_type::NONE) { - WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); + WARN(gui).print("Invalid announcement type:\n{}\n", message); return false; } else if (message.empty()) { - Core::printerr("Empty announcement %u\n", info.type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement {}\n", ENUM_AS_STR(info.type)); // DF would print this to errorlog.txt return false; } @@ -2077,7 +2097,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.A_DISPLAY && !a_flags.bits.DO_MEGA) { - DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for adventure mode:\n{}\n", message); return false; } @@ -2091,7 +2111,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility if (!Maps::isValidTilePos(info.pos) || (Maps::getTileDesignation(info.pos)->whole & 0x10) == 0x0) { - DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Adventure mode announcement not detected:\n{}\n", message); return false; } } @@ -2101,7 +2121,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if ((info.unit_a || info.unit_d) && (!info.unit_a || Units::isHidden(info.unit_a)) && (!info.unit_d || Units::isHidden(info.unit_d))) { - DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str()); + DEBUG(gui).print("Dwarf mode announcement not detected:\n{}\n", message); return false; } @@ -2111,7 +2131,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!info.unit_a && !info.unit_d) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n{}\n", message); return false; } } @@ -2119,12 +2139,12 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) { if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) { - DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped announcement not enabled at all for dwarf mode:\n{}\n", message); return false; } else if (!recent_report_any(info.unit_a) && !recent_report_any(info.unit_d)) { - DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n%s\n", message.c_str()); + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n{}\n", message); return false; } } @@ -2157,7 +2177,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (samp_index >= 0) { - DEBUG(gui).print("Playing sound #%d for announcement.\n", samp_index); + DEBUG(gui).print("Playing sound #{} for announcement.\n", samp_index); //play_sound(musicsound_info, samp_index, 255, true); // g_src/music_and_sound_g.h // TODO: implement sounds } } @@ -2184,7 +2204,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) if (a_flags.bits.D_DISPLAY) world->status.display_timer = info.display_timer; - DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded as repeat:\n{}\n", message); return true; } } @@ -2196,7 +2216,7 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) new_report->text = message; new_report->color = info.color; new_report->bright = info.bright; - new_report->flags.whole = adv_unconscious ? df::report::T_flags::mask_unconscious : 0x0; + new_report->flags.whole = adv_unconscious ? df::announcement_flag::mask_unconscious : 0x0; new_report->zoom_type = info.zoom_type; new_report->pos = info.pos; new_report->zoom_type2 = info.zoom_type2; @@ -2254,26 +2274,14 @@ bool Gui::autoDFAnnouncement(df::announcement_infost info, string message) world->status.display_timer = info.display_timer; } - // Delete excess reports - while (reports.size() > MAX_REPORTS_SIZE) - { // Report destructor - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - if (*gamemode == game_mode::DWARF || // Did dwarf announcement or UCR (*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || // Did adventure announcement (a_flags.bits.DO_MEGA && !adv_unconscious)) // Did popup { - DEBUG(gui).print("Announcement succeeded and displayed:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement succeeded and displayed:\n{}\n", message); } else - DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); + DEBUG(gui).print("Announcement added internally and to gamelog.txt but didn't qualify to be displayed anywhere:\n{}\n", message); return true; } @@ -2479,8 +2487,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) if (buff1 == "VAR") // Color from dipscript var { - DEBUG(gui).print("MTB_parse received:\n[C:VAR:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff2.c_str(), buff3.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[C:VAR:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff2, buff3, mtb->environment ? "Active" : "NULL"); //MTB_set_color_on_var(mtb, buff2, buff3); } else @@ -2534,8 +2542,8 @@ void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) string buff_var_name = grab_token_string_pos(parse_text, i, ':'); i += buff_var_name.size(); - DEBUG(gui).print("MTB_parse received:\n[VAR:%s:%s:%s]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: %s\n", - buff_format.c_str(), buff_var_type.c_str(), buff_var_name.c_str(), mtb->environment ? "Active" : "NULL"); + DEBUG(gui).print("MTB_parse received:\n[VAR:{}:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", + buff_format, buff_var_type, buff_var_name, mtb->environment ? "Active" : "NULL"); //MTB_append_variable(mtb, str, buff_format, buff_var_type, buff_var_name); } else if (token_buffer == "R" || token_buffer == "B" || token_buffer == "P") @@ -2672,10 +2680,9 @@ void Gui::MTB_set_width(df::markup_text_boxst *mtb, int32_t n_width) df::widget * Gui::getWidget(df::widget_container *container, string name) { CHECK_NULL_POINTER(container); // ensure the compiler catches the change if we ever fix the template parameters - std::map & orig_field = container->children_by_name; - auto children_by_name = reinterpret_cast> *>(&orig_field); - if (children_by_name->contains(name)) - return (*children_by_name)[name].get(); + auto & children_by_name = container->children_by_name; + if (children_by_name.contains(name)) + return (children_by_name)[name].get(); return NULL; } @@ -2707,7 +2714,7 @@ df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) return ws; } -df::viewscreen *Gui::getViewscreenByIdentity (virtual_identity &id, int n) +df::viewscreen *Gui::getViewscreenByIdentity (const virtual_identity &id, int n) { bool limit = (n > 0); df::viewscreen *screen = Gui::getCurViewscreen(); @@ -2744,9 +2751,18 @@ df::coord Gui::getViewportPos() df::coord Gui::getCursorPos() { using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return df::coord(); + auto &look = game->main_interface.adventure.look; + if (!look.open) + return df::coord(); + return look.cursor; + } + if (!cursor) return df::coord(); - return df::coord(cursor->x, cursor->y, cursor->z); } @@ -2819,34 +2835,32 @@ bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center, boo unfollow(); + if (!Maps::isValidTilePos(x, y, z)) + return false; + auto dims = getDwarfmodeViewDims(); int32_t w = dims.map_x2 - dims.map_x1 + 1; int32_t h = dims.map_y2 - dims.map_y1 + 1; int32_t new_win_x, new_win_y, new_win_z; getViewCoords(new_win_x, new_win_y, new_win_z); - if (Maps::isValidTilePos(x, y, z)) - { - if (center) - { - new_win_x = x - w / 2; - new_win_y = y - h / 2; - } - else // just bring it on screen - { - if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" - new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; - if (new_win_y > (y - 5)) - new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; - if (new_win_x < (x + 5 - w)) - new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; - if (new_win_y < (y + 5 - h)) - new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; - } - - new_win_z = z; + if (center) { + new_win_x = x - w / 2; + new_win_y = y - h / 2; + } else { + // just bring it on screen + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" + new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; + if (new_win_y > (y - 5)) + new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; + if (new_win_x < (x + 5 - w)) + new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; + if (new_win_y < (y + 5 - h)) + new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; } + new_win_z = z; + *window_x = new_win_x; *window_y = new_win_y; *window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); @@ -2911,7 +2925,7 @@ bool Gui::inRenameBuilding() return false; } -bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getViewCoords(int32_t &x, int32_t &y, int32_t &z) { x = *df::global::window_x; y = *df::global::window_y; @@ -2919,7 +2933,7 @@ bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z) return true; } -bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setViewCoords(const int32_t x, const int32_t y, const int32_t z) { (*df::global::window_x) = x; (*df::global::window_y) = y; @@ -2927,32 +2941,53 @@ bool Gui::setViewCoords (const int32_t x, const int32_t y, const int32_t z) return true; } -bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getCursorCoords(int32_t &x, int32_t &y, int32_t &z) { - x = df::global::cursor->x; - y = df::global::cursor->y; - z = df::global::cursor->z; + using df::global::cursor; + bool is_adv = World::isAdventureMode(); + if (is_adv || !cursor) + { + df::coord p; + if (is_adv && game) + { + auto &look = game->main_interface.adventure.look; + if (look.open) + p = look.cursor; + } + x = p.x; y = p.y; z = p.z; + return p.isValid(); + } + + x = cursor->x; y = cursor->y; z = cursor->z; return has_cursor(); } -bool Gui::getCursorCoords (df::coord &pos) +bool Gui::getCursorCoords(df::coord &pos) { - pos.x = df::global::cursor->x; - pos.y = df::global::cursor->y; - pos.z = df::global::cursor->z; - return has_cursor(); + pos = getCursorPos(); + return pos.isValid(); } //FIXME: confine writing of coords to map bounds? -bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setCursorCoords(const int32_t x, const int32_t y, const int32_t z) { - df::global::cursor->x = x; - df::global::cursor->y = y; - df::global::cursor->z = z; + using df::global::cursor; + if (World::isAdventureMode()) + { + if (!game) + return false; + auto &look = game->main_interface.adventure.look; + look.cursor = df::coord(x, y, z); + return true; + } + if (!cursor) + return false; + + cursor->x = x; cursor->y = y; cursor->z = z; return true; } -bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) +bool Gui::getDesignationCoords(int32_t &x, int32_t &y, int32_t &z) { x = selection_rect->start_x; y = selection_rect->start_y; @@ -2960,7 +2995,7 @@ bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) return (x >= 0) ? false : true; } -bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) +bool Gui::setDesignationCoords(const int32_t x, const int32_t y, const int32_t z) { selection_rect->start_x = x; selection_rect->start_y = y; diff --git a/library/modules/Hotkey.cpp b/library/modules/Hotkey.cpp new file mode 100644 index 0000000000..6433879d85 --- /dev/null +++ b/library/modules/Hotkey.cpp @@ -0,0 +1,404 @@ +#include "modules/Hotkey.h" + +#include +#include + +#include "Core.h" +#include "ColorText.h" +#include "MiscUtils.h" +#include "PluginManager.h" + +#include "modules/DFSDL.h" +#include "modules/Gui.h" + +#include "df/global_objects.h" +#include "df/viewscreen.h" +#include "df/interfacest.h" + + +using namespace DFHack; +using Hotkey::KeySpec; +using Hotkey::KeyBinding; + +enum HotkeySignal : uint8_t { + None = 0, + CmdReady, + Shutdown, +}; + +static bool operator==(const KeySpec& a, const KeySpec& b) { + return a.modifiers == b.modifiers && a.sym == b.sym && + a.focus.size() == b.focus.size() && + std::equal(a.focus.begin(), a.focus.end(), b.focus.begin()); +} + +// Equality operator for key bindings +static bool operator==(const KeyBinding& a, const KeyBinding& b) { + return a.spec == b.spec && + a.command == b.command && + a.cmdline == b.cmdline; +} + +std::string KeySpec::toString(bool include_focus) const { + std::string out; + if (modifiers & DFH_MOD_CTRL) out += "Ctrl-"; + if (modifiers & DFH_MOD_ALT) out += "Alt-"; + if (modifiers & DFH_MOD_SUPER) out += "Super-"; + if (modifiers & DFH_MOD_SHIFT) out += "Shift-"; + + std::string key_name; + if (this->sym < 0) { + key_name = "MOUSE" + std::to_string(-this->sym); + } else { + key_name = DFSDL::DFSDL_GetKeyName(this->sym); + } + out += key_name; + + if (include_focus && !this->focus.empty()) { + out += "@"; + bool first = true; + for (const auto& fc : this->focus) { + if (first) { + first = false; + out += fc; + } else { + out += "|" + fc; + } + } + } + + return out; +} + +std::optional KeySpec::parse(std::string spec, std::string* err) { + KeySpec out; + + // Determine focus string, if present + size_t focus_idx = spec.find('@'); + if (focus_idx != std::string::npos) { + split_string(&out.focus, spec.substr(focus_idx + 1), "|"); + spec.erase(focus_idx); + } + + // Treat remaining keyspec as lowercase for case-insensitivity. + std::transform(spec.begin(), spec.end(), spec.begin(), tolower); + + // Determine modifier flags + auto match_modifier = [&out, &spec](std::string_view prefix, int mod) { + bool found = spec.starts_with(prefix); + if (found) { + out.modifiers |= mod; + spec.erase(0, prefix.size()); + } + return found; + }; + while (match_modifier("shift-", DFH_MOD_SHIFT) + || match_modifier("ctrl-", DFH_MOD_CTRL) + || match_modifier("alt-", DFH_MOD_ALT) + || match_modifier("super-", DFH_MOD_SUPER)) {} + + out.sym = DFSDL::DFSDL_GetKeyFromName(spec.c_str()); + if (out.sym != SDLK_UNKNOWN) + return out; + + // Attempt to parse as a mouse binding + if (spec.starts_with("mouse")) { + spec.erase(0, 5); + // Read button number, ensuring between 4 and 15 inclusive + try { + int mbutton = std::stoi(spec); + if (mbutton >= 4 && mbutton <= 15) { + out.sym = -mbutton; + return out; + } + } catch (...) { + // If integer parsing fails, it isn't valid + } + if (err) + *err = "Invalid mouse button '" + spec + "', only 4-15 are valid"; + return std::nullopt; + } + + if (err) + *err = "Unknown key '" + spec + "'"; + + // Invalid key binding + return std::nullopt; +} + +bool KeySpec::isDisruptive() const { + // SDLK enum uses the actual characters for a key as its value. + // Escaped values included are Return, Escape, Backspace, and Tab + const std::string essential_key_set = "\r\x1B\b\t -=[]\\;',./"; + + // Letters A-Z, 0-9, and other special keys such as return/escape, and other general typing keys + bool is_essential_key = (this->sym >= SDLK_a && this->sym <= SDLK_z) // A-Z + || (this->sym >= SDLK_0 && this->sym <= SDLK_9) // 0-9 + || (this->sym < CHAR_MAX && essential_key_set.find((char)this->sym) != std::string::npos) + || (this->sym >= SDLK_LEFT && this->sym <= SDLK_UP); // Arrow keys + + // Essential keys are safe, so long as they have a modifier that isn't Shift + if (is_essential_key && !(this->modifiers & ~DFH_MOD_SHIFT)) + return true; + + return false; +} + +// Hotkeys actions are executed from an external thread to avoid deadlocks +// that may occur if running commands from the render or simulation threads. +void HotkeyManager::hotkey_thread_fn() { + auto& core = DFHack::Core::getInstance(); + + std::unique_lock l(lock); + while (true) { + cond.wait(l, [this]() { return this->hotkey_sig != HotkeySignal::None; }); + if (hotkey_sig == HotkeySignal::Shutdown) + return; + if (hotkey_sig != HotkeySignal::CmdReady) + continue; + + // Copy and reset important data, then release the lock + this->hotkey_sig = HotkeySignal::None; + std::string cmd = this->queued_command; + this->queued_command.clear(); + l.unlock(); + + // Attempt execution of command + DFHack::color_ostream_proxy out(core.getConsole()); + auto res = core.runCommand(out, cmd); + if (res == DFHack::CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", cmd.c_str()); + l.lock(); + } +} + + +bool HotkeyManager::addKeybind(KeySpec spec, std::string_view cmd) { + // No point in a hotkey with no action + if (cmd.empty()) + return false; + + KeyBinding binding; + binding.spec = std::move(spec); + binding.cmdline = cmd; + size_t space_idx = cmd.find(' '); + binding.command = space_idx == std::string::npos ? cmd : cmd.substr(0, space_idx); + + std::lock_guard l(lock); + auto& bindings = this->bindings[binding.spec.sym]; + for (auto& bind : bindings) { + // Don't set a keybind twice, but return true as there isn't an issue + if (bind == binding) + return true; + } + + bindings.emplace_back(binding); + return true; +} + +bool HotkeyManager::addKeybind(std::string keyspec, std::string_view cmd) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return false; + return this->addKeybind(spec_opt.value(), cmd); +} + +bool HotkeyManager::removeKeybind(const KeySpec& spec, bool match_focus, std::string_view cmdline) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return false; + auto& binds = bindings[spec.sym]; + + auto new_end = std::remove_if(binds.begin(), binds.end(), [match_focus, spec, &cmdline](const auto& v) { + return v.spec.sym == spec.sym + && v.spec.modifiers == spec.modifiers + && (!match_focus || v.spec.focus == spec.focus) + && (cmdline.empty() || v.cmdline == cmdline); + }); + if (new_end == binds.end()) + return false; // No bindings removed + + binds.erase(new_end, binds.end()); + return true; +} + +bool HotkeyManager::removeKeybind(std::string keyspec, bool match_focus, std::string_view cmdline) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return false; + return this->removeKeybind(spec_opt.value(), match_focus, cmdline); +} + +std::vector HotkeyManager::listKeybinds(const KeySpec& spec) { + std::lock_guard l(lock); + if (!bindings.contains(spec.sym)) + return {}; + + std::vector out; + + auto& binds = bindings[spec.sym]; + for (const auto& bind : binds) { + if (bind.spec.modifiers != spec.modifiers) + continue; + + // If no focus is required, it is always active + if (spec.focus.empty() || bind.spec.focus.empty()) { + out.push_back(bind.cmdline); + continue; + } + + // If a focus is required, determine if search spec if the same or more specific + for (const auto& requested : spec.focus) { + for (const auto& to_match : bind.spec.focus) { + if (prefix_matches(to_match, requested)) + out.push_back("@" + to_match + ":" + bind.cmdline); + } + } + } + + return out; +} + +std::vector HotkeyManager::listKeybinds(std::string keyspec) { + std::optional spec_opt = KeySpec::parse(std::move(keyspec)); + if (!spec_opt.has_value()) + return {}; + return this->listKeybinds(spec_opt.value()); +} + +std::vector HotkeyManager::listActiveKeybinds() { + std::lock_guard l(lock); + std::vector out; + + for(const auto& [_, bind_set] : bindings) { + for (const auto& binding : bind_set) { + if (binding.spec.focus.empty()) { + // Binding always active + out.emplace_back(binding); + continue; + } + for (const auto& focus : binding.spec.focus) { + // Determine if focus string allows this binding + if (Gui::matchFocusString(focus)) { + out.emplace_back(binding); + break; + } + } + } + } + + return out; +} + +std::vector HotkeyManager::listAllKeybinds() { + std::lock_guard l(lock); + std::vector out; + + for (const auto& [_, bind_set] : bindings) { + for (const auto& bind : bind_set) { + out.emplace_back(bind); + } + } + return out; +} + +bool HotkeyManager::handleKeybind(int sym, int modifiers) { + // Ensure gamestate is ready + if (!df::global::gview || !df::global::plotinfo) + return false; + + // Get topmost active screen + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + // Map keypad return to return + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; + + std::unique_lock l(lock); + + // If reading input for a keybinding screen, save the input and exit early + if (keybind_save_requested) { + KeySpec spec; + spec.sym = sym; + spec.modifiers = modifiers; + requested_keybind = spec.toString(false); + keybind_save_requested = false; + return true; + } + + if (!bindings.contains(sym)) + return false; + auto& binds = bindings[sym]; + + auto& core = Core::getInstance(); + bool mortal_mode = core.getMortalMode(); + + // Iterate in reverse, prioritizing the last added keybinds + for (const auto& bind : binds | std::views::reverse) { + if (bind.spec.modifiers != modifiers) + continue; + + if (!bind.spec.focus.empty()) { + bool matched = false; + for (const auto& focus : bind.spec.focus) { + if (Gui::matchFocusString(focus)) { + matched = true; + break; + } + } + if (!matched) + continue; + } + + if (!core.getPluginManager()->CanInvokeHotkey(bind.command, screen)) + continue; + + if (mortal_mode && core.isArmokTool(bind.command)) + continue; + + queued_command = bind.cmdline; + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); + return true; + } + + return false; +} + +void HotkeyManager::setHotkeyCommand(std::string cmd) { + std::unique_lock l(lock); + queued_command = std::move(cmd); + hotkey_sig = HotkeySignal::CmdReady; + l.unlock(); + cond.notify_all(); +} + +void HotkeyManager::requestKeybindingInput(bool cancel) { + std::lock_guard l(lock); + keybind_save_requested = !cancel; + requested_keybind.clear(); +} + +std::string HotkeyManager::getKeybindingInput() { + std::lock_guard l(lock); + return requested_keybind; +} + +HotkeyManager::HotkeyManager() { + this->hotkey_thread = std::thread(&HotkeyManager::hotkey_thread_fn, this); +} + +HotkeyManager::~HotkeyManager() { + // Set shutdown signal and notify thread + { + std::lock_guard l(lock); + this->hotkey_sig = HotkeySignal::Shutdown; + } + cond.notify_all(); + + if (this->hotkey_thread.joinable()) + this->hotkey_thread.join(); +} diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 47d68b1654..dcd83fce88 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -28,7 +28,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" @@ -44,10 +43,12 @@ distribution. #include "df/building.h" #include "df/building_actual.h" #include "df/building_tradedepotst.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/caravan_state.h" #include "df/creature_raw.h" #include "df/dfhack_material_category.h" +#include "df/d_init.h" #include "df/entity_buy_prices.h" #include "df/entity_buy_requests.h" #include "df/entity_raw.h" @@ -63,6 +64,7 @@ distribution. #include "df/historical_entity.h" #include "df/item.h" #include "df/item_bookst.h" +#include "df/item_magicalst.h" #include "df/item_plant_growthst.h" #include "df/item_toolst.h" #include "df/item_type.h" @@ -152,7 +154,7 @@ string ItemTypeInfo::getToken() { if (custom) rv += ":" + custom->id; else if (subtype != -1 && type != item_type::PLANT_GROWTH) - rv += stl_sprintf(":%d", subtype); + rv += fmt::format(":{}", subtype); return rv; } @@ -676,14 +678,54 @@ string Items::getDescription(df::item *item, int type, bool decorate) { item->getItemDescription(&tmp, type); if (decorate) { - addQuality(tmp, item->getQuality()); + // Special indicators get added in a specific order + // Innermost is at the top, and outermost is at the bottom - if (item->isImproved()) { + // First, figure out the quality levels we're going to display + int craftquality = item->getQuality(); + int craftquality_only_imps = item->getImprovementQuality(); + bool has_displayed_item_improvements = item->isImproved(); + if (!has_displayed_item_improvements && (craftquality < craftquality_only_imps)) + craftquality = craftquality_only_imps; + + // First, actual item quality + addQuality(tmp, craftquality); + + // Next, magic enchants + if (item->getMagic() != NULL) + tmp = '\x11' + tmp + '\x10'; // <| |> + + // Next, improvements + if (has_displayed_item_improvements) { tmp = '\xAE' + tmp + '\xAF'; // («) + tmp + (») - addQuality(tmp, item->getImprovementQuality()); + if (df::global::d_init->display.flags.is_set(d_init_flags1::SHOW_IMP_QUALITY)) + addQuality(tmp, craftquality_only_imps); + } + + // Dwarf mode only, forbid/foreign + if (*df::global::gamemode == game_mode::DWARF) { + if (item->flags.bits.forbid) + tmp = '{' + tmp + '}'; + if (item->flags.bits.foreign) + tmp = '(' + tmp + ')'; + } + + // Wear + switch (item->getWear()) + { + case 1: tmp = 'x' + tmp + 'x'; break; + case 2: tmp = 'X' + tmp + 'X'; break; + case 3: tmp = "XX" + tmp + "XX"; break; } - if (item->flags.bits.foreign) - tmp = "(" + tmp + ")"; + + // Fire + if (item->flags.bits.on_fire) + tmp = '\x13' + tmp + '\x13'; // !! !! + + // Finally, Adventure civzone + if ((*df::global::gamemode == game_mode::ADVENTURE) && + Items::getGeneralRef(item, general_ref_type::BUILDING_CIVZONE_ASSIGNED) != NULL) + tmp = '$' + tmp + '$'; } return tmp; } @@ -719,13 +761,6 @@ string Items::getReadableDescription(df::item *item) { CHECK_NULL_POINTER(item); auto desc = get_base_desc(item); - switch (item->getWear()) - { - case 1: desc = "x" + desc + "x"; break; // Worn - case 2: desc = "X" + desc + "X"; break; // Threadbare - case 3: desc = "XX" + desc + "XX"; break; // Tattered - } - if (auto gref = Items::getGeneralRef(item, general_ref_type::CONTAINS_UNIT)) { if (auto unit = gref->getUnit()) { @@ -765,13 +800,13 @@ static bool removeItemOnGround(df::item *item) } static void resetUnitInvFlags(df::unit *unit, df::unit_inventory_item *inv_item) { - if (inv_item->mode == df::unit_inventory_item::Worn || - inv_item->mode == df::unit_inventory_item::WrappedAround) + if (inv_item->mode == df::inv_item_role_type::Worn || + inv_item->mode == df::inv_item_role_type::WrappedAround) { unit->flags2.bits.calculated_inventory = false; unit->flags2.bits.calculated_insulation = false; } - else if (inv_item->mode == df::unit_inventory_item::StuckIn) + else if (inv_item->mode == df::inv_item_role_type::StuckIn) unit->flags3.bits.stuck_weapon_computed = false; } @@ -814,13 +849,16 @@ static bool detachItem(df::item *item) } } - if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) - return linked_list_remove(&world->proj_list, ref->projectile_id) - && removeRef(item->general_refs, general_ref_type::PROJECTILE, ref->getID()); + if (auto ref = virtual_cast(Items::getGeneralRef(item, general_ref_type::PROJECTILE))) { + auto proj_id = ref->projectile_id; + bool isRefRemoved = removeRef(item->general_refs, general_ref_type::PROJECTILE, proj_id); + bool isLinkRemoved = linked_list_remove(&world->projectiles.all, proj_id); + return isRefRemoved && isLinkRemoved; + } if (item->flags.bits.on_ground) { if (!removeItemOnGround(item)) - Core::printerr("Item was marked on_ground, but not in block: %d (%d,%d,%d)\n", + Core::printerr("Item was marked on_ground, but not in block: {} ({},{},{})\n", item->id, item->pos.x, item->pos.y, item->pos.z); item->flags.bits.on_ground = false; return true; @@ -960,7 +998,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building ref->building_id = building->id; item->general_refs.push_back(ref); - auto con = new df::building_actual::T_contained_items; + auto con = new df::buildingitemst; con->item = item; con->use_mode = use_mode; building->contained_items.push_back(con); @@ -969,7 +1007,7 @@ bool DFHack::Items::moveToBuilding(df::item *item, df::building_actual *building } bool DFHack::Items::moveToInventory(df::item *item, df::unit *unit, - df::unit_inventory_item::T_mode mode, int body_part) + df::inv_item_role_type mode, int body_part) { CHECK_NULL_POINTER(item); CHECK_NULL_POINTER(unit); @@ -1063,7 +1101,7 @@ df::proj_itemst *Items::makeProjectile(df::item *item) ref->projectile_id = proj->id; item->general_refs.push_back(ref); - linked_list_append(&world->proj_list, proj->link); + linked_list_append(&world->projectiles.all, proj->link); return proj; } @@ -1161,6 +1199,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, break; case CATAPULTPARTS: case BALLISTAPARTS: + case BOLT_THROWER_PARTS: case TRAPPARTS: value = 30; break; @@ -1237,7 +1276,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, value = craw->misc.petvalue; return value; case FOOD: - return 10; + return 1; case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1644,7 +1683,10 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { case 3: value = value / 4; break; // (XX) tattered } - // Ignore value bonuses from magic, since that never actually happens + // Magical powers have 500 value each + auto magic = item->getMagic(); + if (magic != NULL) + value += magic->power.size() * 500; // Artifacts have 10x value if (item->flags.bits.artifact_mood) @@ -1709,8 +1751,39 @@ int Items::getValue(df::item *item, df::caravan_state *caravan) { return value; } +// Automatically choose a growth print variant for the specified plant growth subtype+material +int32_t Items::pickGrowthPrint(int16_t subtype, int16_t mat, int32_t matg) +{ + int growth_print = -1; + // Make sure it's made of a valid plant material, then grab its definition + if (mat >= df::builtin_mats::PLANT_1 && mat <= df::builtin_mats::PLANT_200 && matg >= 0 && (unsigned)matg < world->raws.plants.all.size()) + { + auto plant_def = world->raws.plants.all[matg]; + // Make sure it subtype is also valid + if (subtype >= 0 && (unsigned)subtype < plant_def->growths.size()) + { + auto growth_def = plant_def->growths[subtype]; + // Try and find a growth print matching the current time + // (in practice, only tree leaves use this for autumn color changes) + for (size_t i = 0; i < growth_def->prints.size(); i++) + { + auto print_def = growth_def->prints[i]; + if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) + { + growth_print = i; + break; + } + } + // If we didn't find one, then pick the first one (if it exists) + if (growth_print == -1 && !growth_def->prints.empty()) + growth_print = 0; + } + } + return growth_print; +} + bool Items::createItem(vector &out_items, df::unit *unit, df::item_type item_type, - int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor) + int16_t item_subtype, int16_t mat_type, int32_t mat_index, bool no_floor, int32_t count) { // Based on Quietust's plugins/createitem.cpp CHECK_NULL_POINTER(unit); auto pos = Units::getPosition(unit); @@ -1723,7 +1796,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t prod->mat_type = mat_type; prod->mat_index = mat_index; prod->probability = 100; - prod->count = 1; + prod->count = count; switch(item_type) { using namespace df::enums::item_type; @@ -1731,6 +1804,7 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t case POWDER_MISC: case LIQUID_MISC: case DRINK: + case GLOB: prod->product_dimension = 150; break; case THREAD: @@ -1754,39 +1828,12 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t World::isFortressMode() ? df::world_site::find(World::GetCurrentSiteId()) : NULL, NULL); delete prod; - DEBUG(items).print("produced %zd items\n", out_items.size()); + DEBUG(items).print("produced {} items\n", out_items.size()); for (auto out_item : out_items) { // Plant growths need a valid "growth print", otherwise they behave oddly if (auto growth = virtual_cast(out_item)) - { - int growth_print = -1; - // Make sure it's made of a valid plant material, then grab its definition - if (growth->mat_type >= 419 && growth->mat_type <= 618 && growth->mat_index >= 0 && (unsigned)growth->mat_index < world->raws.plants.all.size()) - { - auto plant_def = world->raws.plants.all[growth->mat_index]; - // Make sure it subtype is also valid - if (growth->subtype >= 0 && (unsigned)growth->subtype < plant_def->growths.size()) - { - auto growth_def = plant_def->growths[growth->subtype]; - // Try and find a growth print matching the current time - // (in practice, only tree leaves use this for autumn color changes) - for (size_t i = 0; i < growth_def->prints.size(); i++) - { - auto print_def = growth_def->prints[i]; - if (print_def->timing_start <= *df::global::cur_year_tick && *df::global::cur_year_tick <= print_def->timing_end) - { - growth_print = i; - break; - } - } - // If we didn't find one, then pick the first one (if it exists) - if (growth_print == -1 && !growth_def->prints.empty()) - growth_print = 0; - } - } - growth->growth_print = growth_print; - } + growth->growth_print = pickGrowthPrint(growth->subtype, growth->mat_type, growth->mat_index); if (!no_floor) out_item->moveToGround(pos.x, pos.y, pos.z); } @@ -1799,8 +1846,8 @@ bool Items::createItem(vector &out_items, df::unit *unit, df::item_t bool Items::checkMandates(df::item *item) { CHECK_NULL_POINTER(item); - for (auto mandate : world->mandates) { - if ((mandate->mode == df::mandate::Export) && + for (auto mandate : world->mandates.all) { + if ((mandate->mode == df::mandate_type::Export) && (item->getType() == mandate->item_type) && (mandate->item_subtype == -1 || item->getSubtype() == mandate->item_subtype) && (mandate->mat_type == -1 || item->getMaterial() == mandate->mat_type) && @@ -1881,7 +1928,7 @@ bool Items::markForTrade(df::item *item, df::building_tradedepotst *depot) { job->pos = df::coord(depot->centerx, depot->centery, depot->z); // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + if (!Job::attachJobItem(job, item, df::job_role_type::Hauled)) { delete job; delete href; return false; @@ -1919,17 +1966,10 @@ bool Items::isRequestedTradeGood(df::item *item, df::caravan_state *caravan) { return false; } -/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() -/// (i.e., returning true if and only if the item has a "designate for melting" button in game) -bool Items::canMelt(df::item *item, bool game_ui) { - CHECK_NULL_POINTER(item); - MaterialInfo mat(item); - if (mat.getCraftClass() != craft_material_class::Metal) - return false; - - switch(item->getType()) +bool Items::usesStandardMaterial(df::item_type item_type) +{ + switch(item_type) { using namespace df::enums::item_type; - // These are not meltable, even if made from metal case CORPSE: case CORPSEPIECE: case REMAINS: @@ -1941,8 +1981,20 @@ bool Items::canMelt(df::item *item, bool game_ui) { case EGG: return false; default: - break; + return true; } +} + +/// When called with game_ui = true, this is equivalent to Bay12's itemst::meltable() +/// (i.e., returning true if and only if the item has a "designate for melting" button in game) +bool Items::canMelt(df::item *item, bool game_ui) { + CHECK_NULL_POINTER(item); + MaterialInfo mat(item); + if (mat.getCraftClass() != craft_material_class::Metal) + return false; + + if (!usesStandardMaterial(item->getType())) + return false; if (item->flags.bits.artifact) return false; diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 86809ba5ba..9d1c6ed2eb 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -38,6 +38,7 @@ distribution. #include "modules/References.h" #include "df/building.h" +#include "df/building_workshopst.h" #include "df/general_ref.h" #include "df/general_ref_unit_workerst.h" #include "df/general_ref_building_holderst.h" @@ -46,6 +47,9 @@ distribution. #include "df/job.h" #include "df/job_item.h" #include "df/job_list_link.h" +#include "df/job_postingst.h" +#include "df/job_restrictionst.h" +#include "df/manager_order.h" #include "df/plotinfost.h" #include "df/specific_ref.h" #include "df/unit.h" @@ -54,6 +58,7 @@ distribution. #include #include #include +#include #include using namespace DFHack; @@ -323,11 +328,11 @@ void DFHack::Job::setJobCooldown(df::building *workshop, df::unit *worker, int c if (cooldown <= 0) return; - int idx = linear_index(workshop->job_claim_suppress, &df::building::T_job_claim_suppress::unit, worker); + int idx = linear_index(workshop->job_claim_suppress, &df::job_restrictionst::unit, worker); if (idx < 0) { - auto obj = new df::building::T_job_claim_suppress; + auto obj = new df::job_restrictionst; obj->unit = worker; obj->timer = cooldown; workshop->job_claim_suppress.push_back(obj); @@ -398,27 +403,23 @@ bool DFHack::Job::disconnectJobGeneralRef(df::job *job, df::general_ref *ref) { } bool DFHack::Job::removeJob(df::job* job) { - using df::global::world; CHECK_NULL_POINTER(job); - // cancel_job below does not clean up all refs, so we have to do some work + // remove_job below does not clean up all refs, so we have to do some work for (auto &item_ref : job->items) { disconnectJobItem(job, item_ref); if (item_ref) delete item_ref; } job->items.resize(0); - // call the job cancel vmethod graciously provided by The Toady One. - // job_handler::cancel_job calls job::~job, and then deletes job (this has + // job_handlerst_remove_job calls job::~job, and then deletes job (this has // been confirmed by disassembly). + auto fp = df::global::job_handlerst_remove_job; + CHECK_NULL_POINTER(fp); - // HACK: GCC (starting around GCC 10 targeting C++20 as of v50.09) optimizes - // out the vmethod call here regardless of optimization level, so we need to - // invoke the vmethod manually through a pointer, as the Lua wrapper does. - // `volatile` does not seem to be necessary but is included for good - // measure. - volatile auto cancel_job_method = &df::job_handler::cancel_job; - (world->jobs.*cancel_job_method)(job); + using FT = std::function; + auto f = reinterpret_cast(fp); + (*f)(job); return true; } @@ -515,6 +516,27 @@ bool DFHack::Job::linkIntoWorld(df::job *job, bool new_id) } } +df::job* DFHack::Job::createLinked() +{ + auto job = new df::job(); + DFHack::Job::linkIntoWorld(job, true); + return job; +} + +bool DFHack::Job::assignToWorkshop(df::job *job, df::building_workshopst *workshop) +{ + CHECK_NULL_POINTER(job); + CHECK_NULL_POINTER(workshop); + + if (workshop->jobs.size() >= 10) { + return false; + } + job->pos = df::coord(workshop->centerx, workshop->centery, workshop->z); + DFHack::Job::addGeneralRef(job, df::general_ref_type::BUILDING_HOLDER, workshop->id); + workshop->jobs.push_back(job); + return true; +} + bool DFHack::Job::removePostings(df::job *job, bool remove_all) { using df::global::world; @@ -575,7 +597,7 @@ bool DFHack::Job::listNewlyCreated(std::vector *pvec, int *id_var) } bool DFHack::Job::attachJobItem(df::job *job, df::item *item, - df::job_item_ref::T_role role, + df::job_role_type role, int filter_idx, int insert_idx) { CHECK_NULL_POINTER(job); @@ -585,7 +607,7 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item, * Functionality 100% reverse-engineered from DF code. */ - if (role != df::job_item_ref::TargetContainer) + if (role != df::job_role_type::TargetContainer) { if (item->flags.bits.in_job) return false; @@ -653,6 +675,33 @@ std::string Job::getName(df::job *job) button->matgloss = job->mat_index; button->specflag = job->specflag; button->job_item_flag = job->material_category; + button->specdata = job->specdata; + button->art_specifier_id1 = job->art_spec.id; + button->art_specifier_id2 = job->art_spec.subid; + + button->text(&desc); + delete button; + + return desc; +} + +std::string Job::getManagerOrderName(df::manager_order *order) +{ + CHECK_NULL_POINTER(order); + + std::string desc; + auto button = df::allocate(); + button->mstring = order->reaction_name; + button->jobtype = order->job_type; + button->itemtype = order->item_type; + button->subtype = order->item_subtype; + button->material = order->mat_type; + button->matgloss = order->mat_index; + button->specflag = order->specflag; + button->job_item_flag = order->material_category; + button->specdata = order->specdata; + button->art_specifier_id1 = order->art_spec.id; + button->art_specifier_id2 = order->art_spec.subid; button->text(&desc); delete button; diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index d6c2b657f5..fd1bfe002f 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -14,7 +14,6 @@ using namespace std; #include "Types.h" #include "Error.h" #include "modules/Kitchen.h" -#include "ModuleFactory.h" #include "Core.h" using namespace DFHack; @@ -33,14 +32,14 @@ void Kitchen::debug_print(color_ostream &out) out.print("Kitchen Exclusions\n"); for(std::size_t i = 0; i < size(); ++i) { - out.print("%2zu: IT:%2i IS:%i MT:%3i MI:%2i ET:%i %s\n", + out.print("{:2}: IT:{:2} IS:{:} MT:{:3} MI:{:2} ET:{:} {}\n", i, - plotinfo->kitchen.item_types[i], + ENUM_KEY_STR(item_type,plotinfo->kitchen.item_types[i]), plotinfo->kitchen.item_subtypes[i], plotinfo->kitchen.mat_types[i], plotinfo->kitchen.mat_indices[i], plotinfo->kitchen.exc_types[i].whole, - (plotinfo->kitchen.mat_types[i] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id.c_str() : "n/a" + (plotinfo->kitchen.mat_types[i] >= df::builtin_mats::PLANT_1 && plotinfo->kitchen.mat_types[i] <= df::builtin_mats::PLANT_200) ? world->raws.plants.all[plotinfo->kitchen.mat_indices[i]]->id : "n/a" ); } out.print("\n"); diff --git a/library/modules/MapCache.cpp b/library/modules/MapCache.cpp index c01f5bf576..8f8f2eb4c7 100644 --- a/library/modules/MapCache.cpp +++ b/library/modules/MapCache.cpp @@ -30,7 +30,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" @@ -97,8 +96,6 @@ const BiomeInfo MapCache::biome_stub = { -1, -1, -1, -1, -1, -1, -1, -1 } }; -#define COPY(a,b) memcpy(&a,&b,sizeof(a)) - MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent), designated_tiles{} @@ -124,20 +121,19 @@ void MapExtras::Block::init() if(block) { - COPY(designation, block->designation); - COPY(occupancy, block->occupancy); - - COPY(temp1, block->temperature_1); - COPY(temp2, block->temperature_2); + designation = block->designation; + occupancy = block->occupancy; + temp1 = block->temperature_1; + temp2 = block->temperature_2; valid = true; } else { - memset(designation,0,sizeof(designation)); - memset(occupancy,0,sizeof(occupancy)); - memset(temp1,0,sizeof(temp1)); - memset(temp2,0,sizeof(temp2)); + designation.fill({}); + occupancy.fill({}); + temp1.fill({}); + temp2.fill({}); } } @@ -199,10 +195,10 @@ void MapExtras::Block::init_tiles(bool basemat) MapExtras::Block::TileInfo::TileInfo() { dirty_raw.clear(); - memset(raw_tiles,0,sizeof(raw_tiles)); + raw_tiles.fill({}); ice_info = NULL; con_info = NULL; - memset(base_tiles,0,sizeof(base_tiles)); + base_tiles.fill({}); } MapExtras::Block::TileInfo::~TileInfo() @@ -219,6 +215,15 @@ void MapExtras::Block::TileInfo::init_iceinfo() ice_info = new IceInfo(); } +template +constexpr T arr40d_neg1() { + T tmp{}; + std::remove_reference_t tmp2{}; + tmp2.fill(-1); + tmp.fill(tmp2); + return tmp; +}; + void MapExtras::Block::TileInfo::init_coninfo() { if (con_info) @@ -226,17 +231,17 @@ void MapExtras::Block::TileInfo::init_coninfo() con_info = new ConInfo(); con_info->constructed.clear(); - COPY(con_info->tiles, base_tiles); - memset(con_info->mat_type, -1, sizeof(con_info->mat_type)); - memset(con_info->mat_index, -1, sizeof(con_info->mat_index)); + con_info->tiles = base_tiles; + con_info->mat_type = arr40d_neg1(); + con_info->mat_index = arr40d_neg1(); } MapExtras::Block::BasematInfo::BasematInfo() { vein_dirty.clear(); - memset(mat_type,0,sizeof(mat_type)); - memset(mat_index,-1,sizeof(mat_index)); - memset(veinmat,-1,sizeof(veinmat)); + mat_type.fill({}); + mat_index = arr40d_neg1(); + veinmat = arr40d_neg1(); } bool MapExtras::Block::setFlagAt(df::coord2d p, df::tile_designation::Mask mask, bool set) @@ -482,7 +487,7 @@ void MapExtras::Block::ParseTiles(TileInfo *tiles) tiletypes40d icetiles; BlockInfo::SquashFrozenLiquids(block, icetiles); - COPY(tiles->raw_tiles, block->tiletype); + tiles->raw_tiles = block->tiletype; for (int x = 0; x < 16; x++) { @@ -599,7 +604,7 @@ void MapExtras::Block::WriteTiles(TileInfo *tiles) if (tiles->ice_info && tiles->ice_info->dirty.has_assignments()) { - df::tiletype (*newtiles)[16] = (tiles->con_info ? tiles->con_info->tiles : tiles->base_tiles); + auto newtiles = (tiles->con_info ? tiles->con_info->tiles : tiles->base_tiles); for (int i = block->block_events.size()-1; i >= 0; i--) { @@ -647,8 +652,8 @@ void MapExtras::Block::ParseBasemats(TileInfo *tiles, BasematInfo *bmats) info.prepare(this); - COPY(bmats->veinmat, info.veinmats); - COPY(bmats->veintype, info.veintype); + bmats->veinmat = info.veinmats; + bmats->veintype = info.veintype; for (int x = 0; x < 16; x++) { @@ -780,7 +785,7 @@ bool MapExtras::Block::Write () if(dirty_designations) { - COPY(block->designation, designation); + block->designation = designation; block->flags.bits.designated = true; block->dsgn_check_cooldown = 0; dirty_designations = false; @@ -799,13 +804,13 @@ bool MapExtras::Block::Write () } if(dirty_temperatures) { - COPY(block->temperature_1, temp1); - COPY(block->temperature_2, temp2); + block->temperature_1 = temp1; + block->temperature_2 = temp2; dirty_temperatures = false; } if(dirty_occupancies) { - COPY(block->occupancy, occupancy); + block->occupancy = occupancy; dirty_occupancies = false; } return true; @@ -938,7 +943,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case ROOT: case TREE: case PLANT: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto plant = plants[block->map_pos + df::coord(x,y,0)]) { if (auto raw = df::plant_raw::find(plant->material)) @@ -953,7 +958,7 @@ t_matpair MapExtras::BlockInfo::getBaseMaterial(df::tiletype tt, df::coord2d pos case GRASS_DARK: case GRASS_DRY: case GRASS_DEAD: - rv.mat_type = MaterialInfo::PLANT_BASE; + rv.mat_type = df::builtin_mats::PLANT_1; if (auto raw = df::plant_raw::find(grass[x][y])) { rv.mat_type = raw->material_defs.type[plant_material_def::basic_mat]; @@ -1035,8 +1040,8 @@ void MapExtras::BlockInfo::SquashVeins(df::map_block *mb, t_blockmaterials & mat { std::vector veins; Maps::SortBlockEvents(mb,&veins); - memset(materials,-1,sizeof(materials)); - memset(veintype, 0, sizeof(t_veintype)); + materials = arr40d_neg1(); + veintype.fill({}); for (uint32_t x = 0;x<16;x++) for (uint32_t y = 0; y< 16;y++) { @@ -1055,7 +1060,7 @@ void MapExtras::BlockInfo::SquashFrozenLiquids(df::map_block *mb, tiletypes40d & { std::vector ices; Maps::SortBlockEvents(mb,NULL,&ices); - memset(frozen,0,sizeof(frozen)); + frozen.fill({}); for (uint32_t x = 0; x < 16; x++) for (uint32_t y = 0; y < 16; y++) { for (size_t i = 0; i < ices.size(); i++) @@ -1090,7 +1095,7 @@ void MapExtras::BlockInfo::SquashGrass(df::map_block *mb, t_blockmaterials &mate { std::vector grasses; Maps::SortBlockEvents(mb, NULL, NULL, NULL, &grasses); - memset(materials,-1,sizeof(materials)); + materials = arr40d_neg1(); for (uint32_t x = 0; x < 16; x++) for (uint32_t y = 0; y < 16; y++) { int amount = 0; @@ -1107,7 +1112,7 @@ void MapExtras::BlockInfo::SquashGrass(df::map_block *mb, t_blockmaterials &mate int MapExtras::Block::biomeIndexAt(df::coord2d p) { - if (!block) + if (!block || !world->world_data) return -1; auto des = index_tile(designation,p); @@ -1115,9 +1120,15 @@ int MapExtras::Block::biomeIndexAt(df::coord2d p) if (idx >= 9) return -1; idx = block->region_offset[idx]; - if (idx >= parent->biomes.size()) + if (idx >= 9) return -1; - return idx; + // Biome indices are relative to the block's own region_pos, which may + // differ from the map's anchor region on embarks that cross midmap + // cell boundaries. + auto it = parent->biome_index.find(Maps::getBiomeRgnPos(block->region_pos, idx)); + if (it == parent->biome_index.end()) + return -1; + return it->second; } const BiomeInfo &Block::biomeInfoAt(df::coord2d p) @@ -1239,57 +1250,95 @@ MapExtras::MapCache::MapCache() valid = 0; Maps::getSize(x_bmax, y_bmax, z_max); x_tmax = x_bmax*16; y_tmax = y_bmax*16; - std::vector geoidx; - std::vector > layer_mats; - validgeo = Maps::ReadGeology(&layer_mats, &geoidx); valid = true; + validgeo = false; if (auto data = df::global::world->world_data) { + validgeo = true; + for (size_t i = 0; i < data->midmap_data.region_details.size(); i++) { auto info = data->midmap_data.region_details[i]; region_details[info->pos] = info; } - } - biomes.resize(layer_mats.size()); + // Biome indices in map block designations are relative to each + // block's own region_pos, so the biome list must cover the + // neighborhood of every cell the map touches: embarks can span + // multiple midmap cells. The set of reachable cells is the + // neighborhood rectangle of all block cells, clipped to the world. + df::coord2d lo(data->world_width, data->world_height); + df::coord2d hi(-1, -1); - for (size_t i = 0; i < layer_mats.size(); i++) - { - biomes[i].pos = geoidx[i]; - biomes[i].biome = Maps::getRegionBiome(geoidx[i]); - biomes[i].details = region_details[geoidx[i]]; + for (auto block : world->map.map_blocks) + { + if (!block) + continue; + lo.x = std::min(lo.x, block->region_pos.x - 1); + lo.y = std::min(lo.y, block->region_pos.y - 1); + hi.x = std::max(hi.x, block->region_pos.x + 1); + hi.y = std::max(hi.y, block->region_pos.y + 1); + } + + if (hi.x < 0) + { + // no map blocks; fall back to the map anchor region + lo.x = world->map.region_x / 16 - 1; + lo.y = world->map.region_y / 16 - 1; + hi.x = lo.x + 2; + hi.y = lo.y + 2; + } - biomes[i].geo_index = biomes[i].biome ? biomes[i].biome->geo_index : -1; - biomes[i].geobiome = df::world_geo_biome::find(biomes[i].geo_index); + lo.x = std::max(lo.x, 0); + lo.y = std::max(lo.y, 0); + hi.x = std::min(hi.x, data->world_width - 1); + hi.y = std::min(hi.y, data->world_height - 1); - biomes[i].lava_stone = -1; - biomes[i].default_soil = -1; - biomes[i].default_stone = -1; + for (int16_t y = lo.y; y <= hi.y; y++) + { + for (int16_t x = lo.x; x <= hi.x; x++) + { + df::coord2d pos(x, y); + biome_index[pos] = (int)biomes.size(); + biomes.push_back(BiomeInfo()); - if (biomes[i].details) - biomes[i].lava_stone = biomes[i].details->lava_stone; + auto &info = biomes.back(); + info.pos = pos; + info.biome = Maps::getRegionBiome(pos); + info.details = region_details[pos]; - memset(biomes[i].layer_stone, -1, sizeof(biomes[i].layer_stone)); + info.geo_index = info.biome ? info.biome->geo_index : -1; + info.geobiome = df::world_geo_biome::find(info.geo_index); - for (size_t j = 0; j < std::min(BiomeInfo::MAX_LAYERS,layer_mats[i].size()); j++) - { - biomes[i].layer_stone[j] = layer_mats[i][j]; + info.lava_stone = info.details ? info.details->lava_stone : -1; + info.default_soil = -1; + info.default_stone = -1; - auto raw = df::inorganic_raw::find(layer_mats[i][j]); - if (!raw) - continue; + memset(info.layer_stone, -1, sizeof(info.layer_stone)); - bool is_soil = raw->flags.is_set(inorganic_flags::SOIL_ANY); - if (is_soil) - biomes[i].default_soil = layer_mats[i][j]; - else if (biomes[i].default_stone == -1) - biomes[i].default_stone = layer_mats[i][j]; - } + if (info.geobiome) + { + auto &geolayers = info.geobiome->layers; - while (layer_mats[i].size() < 16) - layer_mats[i].push_back(-1); + for (size_t j = 0; j < std::min(BiomeInfo::MAX_LAYERS, geolayers.size()); j++) + { + int16_t mat = geolayers[j]->mat_index; + info.layer_stone[j] = mat; + + auto raw = df::inorganic_raw::find(mat); + if (!raw) + continue; + + bool is_soil = raw->flags.is_set(inorganic_flags::SOIL_ANY); + if (is_soil) + info.default_soil = mat; + else if (info.default_stone == -1) + info.default_stone = mat; + } + } + } + } } } @@ -1363,6 +1412,50 @@ void MapExtras::MapCache::discardBlock(Block *block) delete block; } +void MapExtras::MapCache::propagateVerticalFlags(DFCoord pos) +{ + df::tiletype_shape shape = tileShape(tiletypeAt(pos)); + bool open = shape == tiletype_shape::EMPTY || shape == tiletype_shape::RAMP_TOP; + + df::tile_designation td = designationAt(pos); + if (open) + { + if (!ensureBlockAt(DFCoord(pos.x, pos.y, pos.z+1))) { + // only the sky above + td.bits.light = true; + td.bits.outside = true; + td.bits.subterranean = false; + } + } + else + { + // a solid tile covers the column below; subterranean is a property + // of each tile and is preserved + td.bits.light = false; + td.bits.outside = false; + } + + for (int32_t z = pos.z - 1; + z >= 0 && ensureBlockAt(DFCoord(pos.x, pos.y, z)); + --z) + { + DFCoord pos_below(pos.x, pos.y, z); + df::tile_designation td_below = designationAt(pos_below); + bool sub = open ? td.bits.subterranean : td_below.bits.subterranean; + if (td_below.bits.light == td.bits.light + && td_below.bits.outside == td.bits.outside + && td_below.bits.subterranean == sub) + break; + td_below.bits.light = td.bits.light; + td_below.bits.outside = td.bits.outside; + td_below.bits.subterranean = sub; + setDesignationAt(pos_below, td_below); + shape = tileShape(tiletypeAt(pos_below)); + if (shape != tiletype_shape::EMPTY && shape != tiletype_shape::RAMP_TOP) + break; + } +} + void MapExtras::MapCache::resetTags() { for (auto it = blocks.begin(); it != blocks.end(); ++it) diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac4..cd6b6fa46c 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -31,41 +31,62 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" +#include "TileTypes.h" #include "VersionInfo.h" #include "modules/Buildings.h" +#include "modules/Constructions.h" #include "modules/MapCache.h" #include "modules/Maps.h" #include "df/biome_type.h" -#include "df/block_burrow.h" #include "df/block_burrow_link.h" +#include "df/block_burrow.h" +#include "df/block_column_print_infost.h" #include "df/block_square_event_grassst.h" -#include "df/building.h" +#include "df/block_square_event_item_spatterst.h" +#include "df/block_square_event_material_spatterst.h" +#include "df/block_square_event_spoorst.h" #include "df/building_type.h" +#include "df/building.h" #include "df/builtin_mats.h" #include "df/burrow.h" +#include "df/construction.h" +#include "df/entity_plot_invasion_mapst.h" #include "df/feature_init.h" +#include "df/feature_map_shellst.h" +#include "df/feature_mapst.h" #include "df/flow_info.h" -#include "df/map_block.h" +#include "df/historical_entity.h" +#include "df/invasion_info.h" #include "df/map_block_column.h" -#include "df/plant.h" +#include "df/map_block.h" +#include "df/material.h" #include "df/plant_root_tile.h" #include "df/plant_tree_info.h" #include "df/plant_tree_tile.h" +#include "df/plant.h" +#include "df/plot_invasion_mapst.h" +#include "df/plotinfost.h" #include "df/region_map_entry.h" -#include "df/world.h" +#include "df/site_map_infost.h" +#include "df/weather_type.h" #include "df/world_data.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" #include "df/world_region_details.h" +#include "df/world_site_type.h" +#include "df/world_site.h" #include "df/world_underground_region.h" +#include "df/world.h" #include "df/z_level_flags.h" + +#include #include #include #include +#include #include #include #include @@ -195,10 +216,10 @@ bool cuboid::containsPos(int16_t x, int16_t y, int16_t z) const { x <= x_max && y <= y_max && z <= z_max; } -void cuboid::forCoord(std::function fn) const +void cuboid::forCoord(std::function fn, bool row_major) const { if (isValid()) // Only iterate if valid cuboid. - Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min); + Maps::forCoord(fn, x_min, y_min, z_max, x_max, y_max, z_min, row_major); } void cuboid::forBlock(std::function fn, bool ensure_block) const @@ -228,18 +249,27 @@ bool Maps::IsValid() { } void Maps::forCoord(std::function fn, int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) + int16_t x2, int16_t y2, int16_t z2, bool row_major) { int16_t dx = x1 > x2 ? -1 : 1; int16_t dy = y1 > y2 ? -1 : 1; int16_t dz = z1 > z2 ? -1 : 1; - // Process z, y, then x. - for (int16_t x = x1; x != x2 + dx; x += dx) - for (int16_t y = y1; y != y2 + dy; y += dy) - for (int16_t z = z1; z != z2 + dz; z += dz) - if (!fn(df::coord(x, y, z))) - return; // Break iterator. + if (row_major) { + // Process z, y, then x. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t y = y1; y != y2 + dy; y += dy) + for (int16_t x = x1; x != x2 + dx; x += dx) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } else { + // Process z, x, then y. + for (int16_t z = z1; z != z2 + dz; z += dz) + for (int16_t x = x1; x != x2 + dx; x += dx) + for (int16_t y = y1; y != y2 + dy; y += dy) + if (!fn(df::coord(x, y, z))) + return; // Break iterator. + } } // getter for map size in blocks @@ -410,6 +440,22 @@ df::tile_occupancy *Maps::getTileOccupancy(int32_t x, int32_t y, int32_t z) return block ? &block->occupancy[x&15][y&15] : NULL; } +df::coord2d Maps::addRegionBiomeOffset(df::coord2d world_pos, int8_t offset_dir) { + // Note 1: textual order of offfsets is upside down compared to the keypad order used by the biome offset + // Note 2: also upside down compared to (fort) map block region offsets + constexpr auto biome_offset = std::to_array>({ + {-1, 1}, {0, 1}, {1, 1}, + {-1, 0}, {0, 0}, {1, 0}, + {-1,-1}, {0,-1}, {1,-1} + }); + + auto [diff_x, diff_y] = biome_offset[std::clamp(offset_dir, (int8_t)1, (int8_t)9) - 1]; + return { + (int16_t)std::clamp(world_pos.x + diff_x,0,world->world_data->world_width - 1), + (int16_t)std::clamp(world_pos.y + diff_y,0,world->world_data->world_height - 1) + }; +} + df::region_map_entry *Maps::getRegionBiome(df::coord2d rgn_pos) { auto data = world->world_data; @@ -515,7 +561,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index) df::coord2d sub = rgn_pos & 15; - vector &features = fptr->feature_init[sub.x][sub.y]; + vector &features = fptr->feature_init[sub.x][sub.y]; return vector_get(features, index); } @@ -640,11 +686,256 @@ bool Maps::SortBlockEvents(df::map_block *block, if (priorities) priorities->push_back((df::block_square_event_designation_priorityst *)evt); break; + default: + assert("Unhandled block event type" && false); // FIXME temporary - replace with NONE case after structure are updated + break; } } return true; } +// Based on worldst::add_material_spatter_tile_capped +int32_t Maps::addMaterialSpatter (df::coord pos, int16_t mat, int32_t matg, df::matter_state state, int32_t amount) +{ + // Hardcoded maximum + int32_t cap = 255; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + // DF rejects materials of NONE:* + if (mat == -1) + return amount; + + // Extra check: make sure the material correctly exists + MaterialInfo matinfo(mat, matg); + if (!matinfo.isValid()) + return amount; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + // Extra check: specify state == NONE to auto-pick based on tile temperature + // Note that this won't choose POWDER/PASTE/PRESSED + if (state == df::matter_state::None) + { + uint16_t tile = block->temperature_1[bx][by]; + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + if (boil != 60001 && tile >= boil) + state = df::matter_state::Gas; + else if (melt != 60001 && tile >= melt) + state = df::matter_state::Liquid; + else + state = df::matter_state::Solid; + } + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Find existing matching material spatter + df::block_square_event_material_spatterst *spatter = nullptr; + // DF: get_material_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::material_spatter) + continue; + auto spt = (df::block_square_event_material_spatterst *)evt; + if (spt->mat_type == mat && spt->mat_index == matg && + spt->mat_state == state) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->mat_type = mat; + spatter->mat_index = matg; + spatter->mat_state = state; + spatter->amount.fill({}); + spatter->min_temperature = spatter->max_temperature = 60001; + + uint16_t melt = matinfo.material->heat.melting_point; + uint16_t boil = matinfo.material->heat.boiling_point; + + switch (state) + { using namespace df::enums::matter_state; + case Solid: + case Powder: + case Paste: + case Pressed: + if (melt != 60001) + boil = melt; + spatter->max_temperature = boil; + break; + case Liquid: + if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + spatter->max_temperature = boil; + break; + // Can't really have gas spatters, but DF has this check + // presumably, DF could convert this into a flow + case Gas: + if (boil != 60001 && boil != 0) + spatter->min_temperature = boil - 1; + else if (melt != 60001 && melt != 0) + spatter->min_temperature = melt - 1; + break; + case None: + // impossible + break; + } + // DF doesn't check heatdam/colddam/ignite points here + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = (uint8_t)newamount; + block->flags.bits.may_have_material_spatter = 1; + + return amount; +} + +// Based on worldst::add_item_spatter_tile_capped +int32_t Maps::addItemSpatter (df::coord pos, df::item_type i_type, int16_t i_subtype, int16_t i_subcat1, int32_t i_subcat2, int32_t print_variant, int32_t amount) +{ + // DF passes this as a parameter, but it's always the same + int32_t cap = 10000; + + // Sanity checks + if (amount > cap) + amount = cap; + // DF doesn't handle negative numbers, so disallow them + if (amount < 0) + amount = 0; + + df::map_block *block = Maps::getTileBlock(pos); + if (!block) + return amount; + + int16_t bx = pos.x & 0xF, by = pos.y & 0xF; + + if (amount > 0) + { + // scan all SPOOR events and clear the PRESENT flag if the type is HFID_COMBINEDCASTE_BP, ITEMT_ITEMST_ORIENT, or MESS + for (size_t i = 0; i < block->block_events.size(); i++) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::spoor) + continue; + auto spoor = (df::block_square_event_spoorst *)evt; + if (!spoor->info.flags[bx][by].bits.present) + continue; + if (spoor->info.type[bx][by] == df::spoor_type::HFID_COMBINEDCASTE_BP || + spoor->info.type[bx][by] == df::spoor_type::ITEMT_ITEMST_ORIENT || + spoor->info.type[bx][by] == df::spoor_type::MESS) + spoor->info.flags[bx][by].bits.present = false; + } + } + + // Allow auto-selecting growth print for plant growths + if (i_type == df::item_type::PLANT_GROWTH && print_variant == -1) + print_variant = Items::pickGrowthPrint(i_subtype, i_subcat1, i_subcat2); + + // Find existing matching item spatter + df::block_square_event_item_spatterst *spatter = nullptr; + // DF: get_item_spatter_event_even_if_empty(...) + for (int i = block->block_events.size() - 1; i >= 0; i--) + { + df::block_square_event *evt = block->block_events[i]; + if (evt->getType() != block_square_event_type::item_spatter) + continue; + auto spt = (df::block_square_event_item_spatterst *)evt; + if (spt->item_type == i_type && spt->item_subtype == i_subtype && + spt->mattype == i_subcat1 && spt->matindex == i_subcat2 && + spt->print_variant == print_variant) + { + spatter = spt; + break; + } + } + + // If we didn't find one, make a new one + if (!spatter) + { + spatter = df::allocate(); + spatter->item_type = i_type; + spatter->item_subtype = i_subtype; + spatter->mattype = i_subcat1; + spatter->matindex = i_subcat2; + spatter->print_variant = print_variant; + spatter->amount.fill({}); + spatter->flag.fill({}); + spatter->min_temperature = spatter->max_temperature = 60001; + + if (Items::usesStandardMaterial(i_type)) + { + MaterialInfo info(i_subcat1, i_subcat2); + if (info.isValid()) + { + uint16_t melt = info.material->heat.melting_point; + uint16_t boil = info.material->heat.melting_point; + if (melt != 60001) + spatter->max_temperature = melt; + else + spatter->max_temperature = boil; + // DF doesn't look at the heatdam/colddam/ignite temperatures + } + } + block->block_events.push_back(spatter); + } + + int32_t newamount = spatter->amount[bx][by] + amount; + if (newamount > cap) + { + amount = newamount - cap; + newamount = cap; + } + else + amount = 0; + + spatter->amount[bx][by] = newamount; + spatter->flag[bx][by].bits.season_full_timer = 7; + block->flags.bits.may_have_item_spatter = 1; + + return amount; +} + inline bool RemoveBlockEventInline(int32_t x, int32_t y, int32_t z, df::block_square_event * which) { df::map_block *block = Maps::getBlock(x, y, z); @@ -675,8 +966,11 @@ static df::coord2d biome_offsets[9] = { df::coord2d(-1,1), df::coord2d(0,1), df::coord2d(1,1) }; -inline df::coord2d getBiomeRgnPos(df::coord2d base, int idx) +df::coord2d Maps::getBiomeRgnPos(df::coord2d base, int idx) { + if (!world->world_data || idx < 0 || idx >= eBiomeCount) + return df::coord2d(); + auto r = base + biome_offsets[idx]; int world_width = world->world_data->world_width; @@ -702,6 +996,28 @@ df::coord2d Maps::getBlockTileBiomeRgn(df::map_block *block, df::coord2d pos) return df::coord2d(); } +df::weather_type Maps::getCurrentWeather(df::coord pos) +{ + if (df::global::current_weather){ + auto &map = world->map; + auto [biome_x, biome_y] = Maps::getTileBiomeRgn(pos); + const int weather_x = biome_x - map.region_x / 16 + 1; + const int weather_y = biome_y - map.region_y / 16 + 1; + return (*df::global::current_weather)[weather_x][weather_y]; + } + return df::weather_type::None; +} + +df::weather_type Maps::getCurrentWeather() +{ + auto &map = world->map; + if (map.x_count > 0 && map.y_count > 0 && map.z_count > 0) { + return Maps::getCurrentWeather(df::coord( map.x_count / 2, map.y_count / 2, map.z_count / 2 )); + } else { + return df::weather_type::None; + } +} + /* * Layer geology */ @@ -947,13 +1263,13 @@ static int16_t basic_wet_dry_effect(int16_t region_y, int16_t rain) auto dimy = world->world_data->world_height; auto pole = world->world_data->flip_latitude; - if (dimy > 65 && pole != df::world_data::T_flip_latitude::None) + if (dimy > 65 && pole != df::pole_type::None) { // Medium and Large worlds with at least one pole auto latitude = region_y; - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_y < dimy / 2) latitude = region_y * 2; @@ -993,7 +1309,7 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in bool tropical; // Determine tropicality - if (pole == df::world_data::T_flip_latitude::None) + if (pole == df::pole_type::None) { potential_tropical = region->temperature > 74; tropical = region->temperature > 84; @@ -1002,9 +1318,9 @@ df::enums::biome_type::biome_type Maps::getBiomeTypeWithRef(int16_t region_x, in { auto latitude = region_ref_y; // DF just uses region_y, but embark assistant needs region_ref_y - if (pole == df::world_data::T_flip_latitude::South) + if (pole == df::pole_type::South) latitude = dimy - region_ref_y - 1; - else if (pole == df::world_data::T_flip_latitude::Both) + else if (pole == df::pole_type::Both) { if (region_ref_y < dimy / 2) latitude = region_ref_y * 2; @@ -1263,3 +1579,350 @@ int Maps::removeAreaAquifer(df::coord pos1, df::coord pos2, std::functionsurroundings{ + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + }; + auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); + auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); + return surroundings[3 * evilness_index + savagery_index]; +} + +void Maps::addBlockColumns(int32_t new_height) +{ + auto quantity = new_height - world->map.z_count_block; + if (quantity <= 0) + return; + + auto world = df::global::world; + int32_t z_count_block = world->map.z_count_block; + df::map_block**** block_index = world->map.block_index; + + cuboid last_air_layer( + 0, 0, world->map.z_count_block - 1, + world->map.x_count_block - 1, world->map.y_count_block - 1, world->map.z_count_block - 1); + + last_air_layer.forCoord([&] (df::coord bpos) { + // Allocate a new block column and copy over data from the old + df::map_block** blockColumn = + new df::map_block * [z_count_block + quantity]; + std::memcpy(blockColumn, block_index[bpos.x][bpos.y], + z_count_block * sizeof(df::map_block*)); + delete[] block_index[bpos.x][bpos.y]; + block_index[bpos.x][bpos.y] = blockColumn; + + df::map_block* last_air_block = blockColumn[bpos.z]; + for (int32_t count = 0; count < quantity; count++) + { + df::map_block* air_block = new df::map_block(); + std::fill(&air_block->tiletype[0][0], + &air_block->tiletype[0][0] + (16 * 16), + df::tiletype::OpenSpace); + + // Set block positions properly (based on prior air layer) + air_block->map_pos = last_air_block->map_pos + df::coord{0, 0, int16_t(count + 1)}; + air_block->region_pos = last_air_block->region_pos; + + // Copy other potentially important metadata from prior air + // layer + air_block->lighting = last_air_block->lighting; + air_block->temperature_1 = last_air_block->temperature_1; + air_block->temperature_2 = last_air_block->temperature_2; + air_block->region_offset = last_air_block->region_offset; + + // Create tile designations to inform lighting and + // outside markers + df::tile_designation designation{}; + designation.bits.light = true; + designation.bits.outside = true; + std::fill(&air_block->designation[0][0], + &air_block->designation[0][0] + (16 * 16), designation); + + blockColumn[z_count_block + count] = air_block; + world->map.map_blocks.push_back(air_block); + + // deal with map_block_column stuff even though it'd probably be + // fine + df::map_block_column* column = + world->map.column_index[bpos.x][bpos.y]; + if (!column) + { + continue; + } + df::block_column_print_infost* glyphs = new df::block_column_print_infost; + glyphs->x = {0,1,2,3}; + glyphs->y = {0,0,0,0}; + glyphs->tile = {'e','x','p','^'}; + column->unmined_glyphs.push_back(glyphs); + } + return true; + }); + + // Update global z level flags + df::z_level_flags* flags = new df::z_level_flags[z_count_block + quantity]; + memcpy(flags, world->map_extras.z_level_flags, + z_count_block * sizeof(df::z_level_flags)); + for (int32_t count = 0; count < quantity; count++) + { + flags[z_count_block + count].whole = 0; + flags[z_count_block + count].bits.update = 1; + } + world->map.z_count_block += quantity; + world->map.z_count += quantity; + delete[] world->map_extras.z_level_flags; + world->map_extras.z_level_flags = flags; + + auto updateInvasionMap = [](int32_t new_height, df::plot_invasion_mapst & map) -> void + { + if (map.blockz == 0) + return; // Unused invasion map + if (map.blockz >= new_height) + return; // No change required + + cuboid blocks(0, 0, 0, map.blockx - 1, map.blocky - 1, 0); + blocks.forCoord([&] (df::coord bpos) { + // Create new vertical block + df::pim_blockst** new_block = new df::pim_blockst * [new_height](); + std::memcpy(new_block, map.block_index[bpos.x][bpos.y], map.blockz * sizeof(df::pim_blockst*)); + // Fill new block with nullptr (no information) + std::fill_n(&new_block[map.blockz], new_height - map.blockz, nullptr); + delete[] map.block_index[bpos.x][bpos.y]; + map.block_index[bpos.x][bpos.y] = new_block; + return true; + }); + + map.blockz = new_height; + }; + + auto plotinfo = df::global::plotinfo; + + for (auto& invasion : plotinfo->invasions.list) + { + updateInvasionMap(world->map.z_count, invasion->map); + } + for (auto& entity : world->entities.all) + { + for (auto& map : entity->plot_invasion_map | std::views::filter([&](df::entity_plot_invasion_mapst* map) { return map->site_id == plotinfo->site_id; })) + { + updateInvasionMap(world->map.z_count, map->map); + } + } +} + + + +// reverse engineered from DF 50.13 (FUN_140d82ca0, likely sitest::get_site_type_name) +const char* Maps::getSiteTypeName(df::world_site *site) { + using wst = df::enums::world_site_type::world_site_type; + switch (site->type) { + case wst::PlayerFortress: + case wst::MountainHalls: + if (site->min_depth == 0 && (0 < site->max_depth)){ + return "fortress"; + } + if (site->min_depth > 0) { + return "mountain halls"; + } + return "hillocks"; + + case wst::DarkFortress: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "fortress" : "pits"; + } + + case wst::Cave: + return "cave"; + + case wst::ForestRetreat: + return "forest retreat"; + + case wst::Town: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "town" : "hamlet"; + } + + case wst::ImportantLocation: + return "important location"; + + case wst::LairShrine: + if (site->subtype_info) { + switch (site->subtype_info->lair_type) { + case df::enums::lair_type::LABYRINTH: + return "labyrinth"; + case df::enums::lair_type::SHRINE: + return "shrine"; + default: + break; + } + } + return "lair"; + + case wst::Fortress: + if (site->subtype_info) { + switch (site->subtype_info->fortress_type) { + case df::enums::fortress_type::TOWER: + return "tower"; + case df::enums::fortress_type::MONASTERY: + return "monastery"; + case df::enums::fortress_type::FORT: + return "fort"; + default: + return "castle"; + } + } + return "fortress"; + + case wst::Camp: + return "camp"; + + case wst::Monument: + if (site->subtype_info) { + switch (site->subtype_info->monument_type) { + case df::enums::monument_type::TOMB: + return "tomb"; + case df::enums::monument_type::VAULT: + return "vault"; + default: + break; + } + } + return "monument"; + + default: + return "site"; + } +} + +/* + * Filtered tile scan + */ + +bool Maps::TileFilter::matches(df::map_block *block, df::coord2d local, df::coord pos) const +{ + df::tiletype tt = block->tiletype[local.x][local.y]; + + if (!tiletypes.empty() && !tiletypes.count(tt)) + return false; + if (!materials.empty() && !materials.count(int16_t(tileMaterial(tt)))) + return false; + if (!shapes.empty() && !shapes.count(int16_t(tileShape(tt)))) + return false; + if (!shapes_basic.empty() && + !shapes_basic.count(int16_t(tileShapeBasic(tileShape(tt))))) + return false; + if (!specials.empty() && !specials.count(int16_t(tileSpecial(tt)))) + return false; + if (!variants.empty() && !variants.count(int16_t(tileVariant(tt)))) + return false; + + if (designation_mask && + (block->designation[local.x][local.y].whole & designation_mask) != designation_bits) + return false; + if (occupancy_mask && + (block->occupancy[local.x][local.y].whole & occupancy_mask) != occupancy_bits) + return false; + + if (extra && !extra(block, local, pos)) + return false; + + return true; +} + +Maps::TileScanResult Maps::forEachTile( + const cuboid &bounds, const TileFilter &filter, const TileActions &actions) +{ + TileScanResult result; + if (!IsValid()) + return result; + + // Construction records are buffered during the scan and merged at the + // end, so a bulk spawn costs one sorted merge instead of one insertion + // per tile. The records are plain data; no df::construction objects are + // allocated until the merge. + struct PendingConstruction { + df::coord pos; + df::tiletype original_tile; + }; + std::vector pending; + bool aborted = false; + + bounds.forBlock([&](df::map_block *block, cuboid area) { + int bx = block->map_pos.x, by = block->map_pos.y, bz = block->map_pos.z; + int lx1 = area.x_min - bx, lx2 = area.x_max - bx; + int ly1 = area.y_min - by, ly2 = area.y_max - by; + + for (int lx = lx1; lx <= lx2 && !aborted; lx++) { + for (int ly = ly1; ly <= ly2 && !aborted; ly++) { + df::coord2d local(lx, ly); + df::coord pos(bx + lx, by + ly, bz); + result.tiles_scanned++; + + if (!filter.matches(block, local, pos)) + continue; + result.tiles_matched++; + + if (actions.set_tiletype || !actions.tiletype_map.empty()) { + df::tiletype &cur = block->tiletype[lx][ly]; + auto it = actions.tiletype_map.find(cur); + if (it != actions.tiletype_map.end()) { + cur = it->second; + result.tiletypes_changed++; + } + else if (actions.set_tiletype) { + cur = *actions.set_tiletype; + result.tiletypes_changed++; + } + } + + if (actions.designation_set || actions.designation_clear) { + auto &des = block->designation[lx][ly]; + des.whole = (des.whole & ~actions.designation_clear) | actions.designation_set; + } + if (actions.occupancy_set || actions.occupancy_clear) { + auto &occ = block->occupancy[lx][ly]; + occ.whole = (occ.whole & ~actions.occupancy_clear) | actions.occupancy_set; + } + + if (actions.construct && !Constructions::findAtTile(pos)) { + pending.push_back({ pos, block->tiletype[lx][ly] }); + if (actions.construct_tiletype) { + block->tiletype[lx][ly] = *actions.construct_tiletype; + result.tiletypes_changed++; + } + } + + if (actions.callback && !actions.callback(block, local, pos)) + aborted = true; + } + } + return !aborted; + }); + result.aborted = aborted; + + if (!pending.empty()) { + auto &vec = world->event.constructions; + auto pos_less = [](const df::construction *a, const df::construction *b) { + return a->pos < b->pos; + }; + size_t old_size = vec.size(); + for (auto &p : pending) { + auto *con = new df::construction(); + con->pos = p.pos; + con->item_type = actions.construct_item_type; + con->item_subtype = actions.construct_item_subtype; + con->mat_type = actions.construct_mat_type; + con->mat_index = actions.construct_mat_index; + con->flags.whole = actions.construct_flags; + con->original_tile = p.original_tile; + vec.push_back(con); + } + std::sort(vec.begin() + old_size, vec.end(), pos_less); + std::inplace_merge(vec.begin(), vec.begin() + old_size, vec.end(), pos_less); + result.constructions_added = (int64_t)pending.size(); + } + + return result; +} diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 341ddeabc4..f91882337a 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "MiscUtils.h" @@ -68,7 +67,7 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; -bool MaterialInfo::decode(df::item *item) +bool MaterialInfo::decode(df::item* item) { if (!item) return decode(-1); @@ -76,7 +75,7 @@ bool MaterialInfo::decode(df::item *item) return decode(item->getActualMaterial(), item->getActualMaterialIndex()); } -bool MaterialInfo::decode(const df::material_vec_ref &vr, int idx) +bool MaterialInfo::decode(const df::material_vec_ref& vr, int idx) { if (size_t(idx) >= vr.mat_type.size() || size_t(idx) >= vr.mat_index.size()) return decode(-1); @@ -94,21 +93,22 @@ bool MaterialInfo::decode(int16_t type, int32_t index) inorganic = NULL; plant = NULL; creature = NULL; figure = NULL; - if (type < 0) { + if (type < 0) + { mode = None; return false; } - df::world_raws &raws = world->raws; + auto& raws = world->raws; - if (size_t(type) >= sizeof(raws.mat_table.builtin)/sizeof(void*)) + if (size_t(type) >= sizeof(raws.mat_table.builtin) / sizeof(void*)) return false; if (index < 0) { material = raws.mat_table.builtin[type]; } - else if (type == 0) + else if (type == df::builtin_mats::INORGANIC) { mode = Inorganic; inorganic = df::inorganic_raw::find(index); @@ -116,23 +116,23 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = &inorganic->material; } - else if (type < CREATURE_BASE) + else if (type < df::builtin_mats::CREATURE_1) { material = raws.mat_table.builtin[type]; } - else if (type < FIGURE_BASE) + else if (type <= df::builtin_mats::CREATURE_200) { mode = Creature; - subtype = type-CREATURE_BASE; + subtype = type - df::builtin_mats::CREATURE_1; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; material = creature->material[subtype]; } - else if (type < PLANT_BASE) + else if (type <= df::builtin_mats::HIST_FIG_200) { mode = Creature; - subtype = type-FIGURE_BASE; + subtype = type - df::builtin_mats::HIST_FIG_1; figure = df::historical_figure::find(index); if (!figure) return false; @@ -141,10 +141,10 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return false; material = creature->material[subtype]; } - else if (type < END_BASE) + else if (type <= df::builtin_mats::PLANT_200) { mode = Plant; - subtype = type-PLANT_BASE; + subtype = type - df::builtin_mats::PLANT_1; plant = df::plant_raw::find(index); if (!plant || size_t(subtype) >= plant->material.size()) return false; @@ -158,24 +158,24 @@ bool MaterialInfo::decode(int16_t type, int32_t index) return (material != NULL); } -bool MaterialInfo::find(const std::string &token) +bool MaterialInfo::find(const std::string& token) { std::vector items; split_string(&items, token, ":"); return find(items); } -bool MaterialInfo::find(const std::vector &items) +bool MaterialInfo::find(const std::vector& items) { if (items.empty()) return false; if (items[0] == "INORGANIC" && items.size() > 1) - return findInorganic(vector_get(items,1)); + return findInorganic(vector_get(items, 1)); if (items[0] == "CREATURE_MAT" || items[0] == "CREATURE") - return findCreature(vector_get(items,1), vector_get(items,2)); + return findCreature(vector_get(items, 1), vector_get(items, 2)); if (items[0] == "PLANT_MAT" || items[0] == "PLANT") - return findPlant(vector_get(items,1), vector_get(items,2)); + return findPlant(vector_get(items, 1), vector_get(items, 2)); if (items.size() == 1) { @@ -188,7 +188,8 @@ bool MaterialInfo::find(const std::vector &items) } else if (items.size() == 2) { - if (items[0] == "COAL" && findBuiltin(items[0])) { + if (items[0] == "COAL" && findBuiltin(items[0])) + { if (items[1] == "COKE") this->index = 0; else if (items[1] == "CHARCOAL") @@ -206,18 +207,19 @@ bool MaterialInfo::find(const std::vector &items) return false; } -bool MaterialInfo::findBuiltin(const std::string &token) +bool MaterialInfo::findBuiltin(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(-1); return true; } - df::world_raws &raws = world->raws; - for (int i = 0; i < NUM_BUILTIN; i++) + auto& raws = world->raws; + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { auto obj = raws.mat_table.builtin[i]; if (obj && obj->id == token) @@ -226,34 +228,35 @@ bool MaterialInfo::findBuiltin(const std::string &token) return decode(-1); } -bool MaterialInfo::findInorganic(const std::string &token) +bool MaterialInfo::findInorganic(const std::string& token) { if (token.empty()) return decode(-1); - if (token == "NONE") { + if (token == "NONE") + { decode(0, -1); return true; } - df::world_raws &raws = world->raws; - for (size_t i = 0; i < raws.inorganics.size(); i++) + auto& raws = world->raws; + for (size_t i = 0; i < raws.inorganics.all.size(); i++) { - df::inorganic_raw *p = raws.inorganics[i]; + df::inorganic_raw* p = raws.inorganics.all[i]; if (p->id == token) return decode(0, i); } return decode(-1); } -bool MaterialInfo::findPlant(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findPlant(const std::string& token, const std::string& subtoken) { if (token.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.plants.all.size(); i++) { - df::plant_raw *p = raws.plants.all[i]; + df::plant_raw* p = raws.plants.all[i]; if (p->id != token) continue; @@ -263,39 +266,39 @@ bool MaterialInfo::findPlant(const std::string &token, const std::string &subtok for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(PLANT_BASE+j, i); + return decode(df::builtin_mats::PLANT_1 + j, i); break; } return decode(-1); } -bool MaterialInfo::findCreature(const std::string &token, const std::string &subtoken) +bool MaterialInfo::findCreature(const std::string& token, const std::string& subtoken) { if (token.empty() || subtoken.empty()) return decode(-1); - df::world_raws &raws = world->raws; + auto& raws = world->raws; for (size_t i = 0; i < raws.creatures.all.size(); i++) { - df::creature_raw *p = raws.creatures.all[i]; + df::creature_raw* p = raws.creatures.all[i]; if (p->creature_id != token) continue; for (size_t j = 0; j < p->material.size(); j++) if (p->material[j]->id == subtoken) - return decode(CREATURE_BASE+j, i); + return decode(df::builtin_mats::CREATURE_1 + j, i); break; } return decode(-1); } -bool MaterialInfo::findProduct(df::material *material, const std::string &name) +bool MaterialInfo::findProduct(df::material* material, const std::string& name) { if (!material || name.empty()) return decode(-1); - auto &pids = material->reaction_product.id; + auto& pids = material->reaction_product.id; for (size_t i = 0; i < pids.size(); i++) if ((*pids[i]) == name) return decode(material->reaction_product.material, i); @@ -309,11 +312,13 @@ std::string MaterialInfo::getToken() const return "NONE"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); - switch (mode) { + switch (mode) + { case Builtin: - if (material->id == "COAL") { + if (material->id == "COAL") + { if (index == 0) return "COAL:COKE"; else if (index == 1) @@ -327,7 +332,7 @@ std::string MaterialInfo::getToken() const case Plant: return "PLANT:" + plant->id + ":" + material->id; default: - return stl_sprintf("INVALID_MODE:%d:%d", type, index); + return fmt::format("INVALID_MODE:{}:{}", type, index); } } @@ -337,7 +342,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const return "any"; if (!material) - return stl_sprintf("INVALID:%d:%d", type, index); + return fmt::format("INVALID:{}:{}", type, index); df::matter_state state = matter_state::Solid; if (temp >= material->heat.melting_point) @@ -350,7 +355,7 @@ std::string MaterialInfo::toString(uint16_t temp, bool named) const name = material->prefix + " " + name; if (named && figure) - name += stl_sprintf(" of HF %d", index); + name += fmt::format(" of HF {}", index); return name; } @@ -382,10 +387,10 @@ bool MaterialInfo::isAnyCloth() const material->flags.is_set(THREAD_PLANT) || material->flags.is_set(SILK) || material->flags.is_set(YARN) - ); + ); } -bool MaterialInfo::matches(const df::job_material_category &cat) const +bool MaterialInfo::matches(const df::job_material_category& cat) const { if (!material) return false; @@ -414,7 +419,7 @@ bool MaterialInfo::matches(const df::job_material_category &cat) const return false; } -bool MaterialInfo::matches(const df::dfhack_material_category &cat) const +bool MaterialInfo::matches(const df::dfhack_material_category& cat) const { if (!material) return false; @@ -443,7 +448,7 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat) const #undef TEST -bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const +bool MaterialInfo::matches(const df::job_item& jitem, df::item_type itype) const { if (!isValid()) return false; @@ -460,11 +465,11 @@ bool MaterialInfo::matches(const df::job_item &jitem, df::item_type itype) const mask2.whole &= ~xmask2.whole; return bits_match(jitem.flags1.whole, ok1.whole, mask1.whole) && - bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && - bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); + bits_match(jitem.flags2.whole, ok2.whole, mask2.whole) && + bits_match(jitem.flags3.whole, ok3.whole, mask3.whole); } -void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags1& ok, df::job_item_flags1& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -486,10 +491,10 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma TEST(processable_to_vial, structural && FLAG(plant, plant_raw_flags::EXTRACT_VIAL)); TEST(processable_to_barrel, structural && FLAG(plant, plant_raw_flags::EXTRACT_BARREL)); TEST(solid, !(MAT_FLAG(ALCOHOL_PLANT) || - MAT_FLAG(ALCOHOL_CREATURE) || - MAT_FLAG(LIQUID_MISC_PLANT) || - MAT_FLAG(LIQUID_MISC_CREATURE) || - MAT_FLAG(LIQUID_MISC_OTHER))); + MAT_FLAG(ALCOHOL_CREATURE) || + MAT_FLAG(LIQUID_MISC_PLANT) || + MAT_FLAG(LIQUID_MISC_CREATURE) || + MAT_FLAG(LIQUID_MISC_OTHER))); TEST(tameable_vermin, false); TEST(sharpenable, MAT_FLAG(IS_STONE)); TEST(milk, linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0); @@ -497,7 +502,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma //04000000 - "milkable" - vtable[107],1,1 } -void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags2& ok, df::job_item_flags2& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -511,15 +516,15 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(glass_making, MAT_FLAG(CRYSTAL_GLASSABLE)); TEST(fire_safe, material->heat.melting_point > 11000 - && material->heat.boiling_point > 11000 - && material->heat.ignite_point > 11000 - && material->heat.heatdam_point > 11000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); + && material->heat.boiling_point > 11000 + && material->heat.ignite_point > 11000 + && material->heat.heatdam_point > 11000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 11000)); TEST(magma_safe, material->heat.melting_point > 12000 - && material->heat.boiling_point > 12000 - && material->heat.ignite_point > 12000 - && material->heat.heatdam_point > 12000 - && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); + && material->heat.boiling_point > 12000 + && material->heat.ignite_point > 12000 + && material->heat.heatdam_point > 12000 + && (material->heat.colddam_point == 60001 || material->heat.colddam_point < 12000)); TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL)); TEST(non_economic, !inorganic || !(plotinfo && vector_get(plotinfo->economic_stone, index))); @@ -537,7 +542,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(yarn, MAT_FLAG(YARN)); } -void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const +void MaterialInfo::getMatchBits(df::job_item_flags3& ok, df::job_item_flags3& mask) const { ok.whole = mask.whole = 0; if (!isValid()) return; @@ -549,7 +554,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &ma #undef FLAG #undef TEST -bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::job_material_category* cat, const std::string& token) { cat->whole = 0; @@ -565,7 +570,7 @@ bool DFHack::parseJobMaterialCategory(df::job_material_category *cat, const std: return true; } -bool DFHack::parseJobMaterialCategory(df::dfhack_material_category *cat, const std::string &token) +bool DFHack::parseJobMaterialCategory(df::dfhack_material_category* cat, const std::string& token) { cat->whole = 0; @@ -600,32 +605,14 @@ bool DFHack::isStoneInorganic(int material) return true; } -std::unique_ptr DFHack::createMaterials() -{ - return std::make_unique(); -} - -Materials::Materials() -{ -} - -Materials::~Materials() -{ -} - -bool Materials::Finish() -{ - return true; -} - t_matgloss::t_matgloss() { - fore = 0; - back = 0; - bright = 0; + fore = 0; + back = 0; + bright = 0; - value = 0; - wall_tile = 0; + value = 0; + wall_tile = 0; boulder_tile = 0; } @@ -643,240 +630,7 @@ bool t_matglossInorganic::isGem() return is_gem; } -bool Materials::CopyInorganicMaterials (std::vector & inorganic) -{ - size_t size = world->raws.inorganics.size(); - inorganic.clear(); - inorganic.reserve (size); - for (size_t i = 0; i < size;i++) - { - df::inorganic_raw *orig = world->raws.inorganics[i]; - t_matglossInorganic mat; - mat.id = orig->id; - mat.name = orig->material.stone_name; - - mat.ore_types = orig->metal_ore.mat_index; - mat.ore_chances = orig->metal_ore.probability; - mat.strand_types = orig->thread_metal.mat_index; - mat.strand_chances = orig->thread_metal.probability; - mat.value = orig->material.material_value; - mat.wall_tile = orig->material.tile; - mat.boulder_tile = orig->material.item_symbol; - mat.fore = orig->material.basic_color[0]; - mat.bright = orig->material.basic_color[1]; - mat.is_gem = orig->material.flags.is_set(material_flags::IS_GEM); - inorganic.push_back(mat); - } - return true; -} - -bool Materials::CopyOrganicMaterials (std::vector & organic) -{ - size_t size = world->raws.plants.all.size(); - organic.clear(); - organic.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.all[i]->id; - organic.push_back(mat); - } - return true; -} - -bool Materials::CopyWoodMaterials (std::vector & tree) -{ - size_t size = world->raws.plants.trees.size(); - tree.clear(); - tree.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.trees[i]->id; - tree.push_back(mat); - } - return true; -} - -bool Materials::CopyPlantMaterials (std::vector & plant) -{ - size_t size = world->raws.plants.bushes.size(); - plant.clear(); - plant.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.plants.bushes[i]->id; - plant.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypes (void) -{ - size_t size = world->raws.creatures.all.size(); - race.clear(); - race.reserve (size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.creatures.all[i]->creature_id; - race.push_back(mat); - } - return true; -} - -bool Materials::ReadOthers(void) -{ - other.clear(); - FOR_ENUM_ITEMS(builtin_mats, i) - { - t_matglossOther mat; - mat.id = world->raws.mat_table.builtin[i]->id; - other.push_back(mat); - } - return true; -} - -bool Materials::ReadDescriptorColors (void) -{ - size_t size = world->raws.descriptors.colors.size(); - - color.clear(); - if(size == 0) - return false; - color.reserve(size); - for (size_t i = 0; i < size;i++) - { - df::descriptor_color *c = world->raws.descriptors.colors[i]; - t_descriptor_color col; - col.id = c->id; - col.name = c->name; - col.red = c->red; - col.green = c->green; - col.blue = c->blue; - color.push_back(col); - } - - size = world->raws.descriptors.patterns.size(); - alldesc.clear(); - alldesc.reserve(size); - for (size_t i = 0; i < size;i++) - { - t_matgloss mat; - mat.id = world->raws.descriptors.patterns[i]->id; - alldesc.push_back(mat); - } - return true; -} - -bool Materials::ReadCreatureTypesEx (void) -{ - size_t size = world->raws.creatures.all.size(); - raceEx.clear(); - raceEx.reserve (size); - for (size_t i = 0; i < size; i++) - { - df::creature_raw *cr = world->raws.creatures.all[i]; - t_creaturetype mat; - mat.id = cr->creature_id; - mat.tile_character = cr->creature_tile; - mat.tilecolor.fore = cr->color[0]; - mat.tilecolor.back = cr->color[1]; - mat.tilecolor.bright = cr->color[2]; - - size_t sizecas = cr->caste.size(); - for (size_t j = 0; j < sizecas;j++) - { - df::caste_raw *ca = cr->caste[j]; - /* caste name */ - t_creaturecaste caste; - caste.id = ca->caste_id; - caste.singular = ca->caste_name[0]; - caste.plural = ca->caste_name[1]; - caste.adjective = ca->caste_name[2]; - - // color mod reading - // Caste + offset > color mod vector - auto & colorings = ca->color_modifiers; - size_t sizecolormod = colorings.size(); - caste.ColorModifier.resize(sizecolormod); - for(size_t k = 0; k < sizecolormod;k++) - { - // color mod [0] -> color list - auto & indexes = colorings[k]->pattern_index; - size_t sizecolorlist = indexes.size(); - caste.ColorModifier[k].colorlist.resize(sizecolorlist); - for(size_t l = 0; l < sizecolorlist; l++) - caste.ColorModifier[k].colorlist[l] = indexes[l]; - // color mod [color_modifier_part_offset] = string part - caste.ColorModifier[k].part = colorings[k]->part; - caste.ColorModifier[k].startdate = colorings[k]->start_date; - caste.ColorModifier[k].enddate = colorings[k]->end_date; - } - - // body parts - caste.bodypart.clear(); - size_t sizebp = ca->body_info.body_parts.size(); - for (size_t k = 0; k < sizebp; k++) - { - df::body_part_raw *bp = ca->body_info.body_parts[k]; - t_bodypart part; - part.id = bp->token; - part.category = bp->category; - caste.bodypart.push_back(part); - } - using namespace df::enums::mental_attribute_type; - using namespace df::enums::physical_attribute_type; - for (int32_t k = 0; k < 7; k++) - { - auto & physical = ca->attributes.phys_att_range; - caste.strength[k] = physical[STRENGTH][k]; - caste.agility[k] = physical[AGILITY][k]; - caste.toughness[k] = physical[TOUGHNESS][k]; - caste.endurance[k] = physical[ENDURANCE][k]; - caste.recuperation[k] = physical[RECUPERATION][k]; - caste.disease_resistance[k] = physical[DISEASE_RESISTANCE][k]; - - auto & mental = ca->attributes.ment_att_range; - caste.analytical_ability[k] = mental[ANALYTICAL_ABILITY][k]; - caste.focus[k] = mental[FOCUS][k]; - caste.willpower[k] = mental[WILLPOWER][k]; - caste.creativity[k] = mental[CREATIVITY][k]; - caste.intuition[k] = mental[INTUITION][k]; - caste.patience[k] = mental[PATIENCE][k]; - caste.memory[k] = mental[MEMORY][k]; - caste.linguistic_ability[k] = mental[LINGUISTIC_ABILITY][k]; - caste.spatial_sense[k] = mental[SPATIAL_SENSE][k]; - caste.musicality[k] = mental[MUSICALITY][k]; - caste.kinesthetic_sense[k] = mental[KINESTHETIC_SENSE][k]; - caste.empathy[k] = mental[EMPATHY][k]; - caste.social_awareness[k] = mental[SOCIAL_AWARENESS][k]; - } - mat.castes.push_back(caste); - } - for (size_t j = 0; j < world->raws.creatures.all[i]->material.size(); j++) - { - t_creatureextract extract; - extract.id = world->raws.creatures.all[i]->material[j]->id; - mat.extract.push_back(extract); - } - raceEx.push_back(mat); - } - return true; -} - -bool Materials::ReadAllMaterials(void) -{ - bool ok = true; - ok &= this->ReadCreatureTypes(); - ok &= this->ReadCreatureTypesEx(); - ok &= this->ReadDescriptorColors(); - ok &= this->ReadOthers(); - return ok; -} - -std::string Materials::getDescription(const t_material & mat) +std::string Materials::getDescription(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); if (mi.creature) @@ -889,7 +643,7 @@ std::string Materials::getDescription(const t_material & mat) // type of material only so we know which vector to retrieve // This is completely worthless now -std::string Materials::getType(const t_material & mat) +std::string Materials::getType(const t_material& mat) { MaterialInfo mi(mat.mat_type, mat.mat_index); switch (mi.mode) diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp index 0477c366b0..d3fb436826 100644 --- a/library/modules/Military.cpp +++ b/library/modules/Military.cpp @@ -4,18 +4,34 @@ #include "MiscUtils.h" #include "modules/Military.h" #include "modules/Translation.h" +#include "modules/Units.h" +#include "df/activity_entry.h" +#include "df/activity_event.h" #include "df/building.h" #include "df/building_civzonest.h" +#include "df/building_squad_infost.h" +#include "df/histfig_entity_link_former_positionst.h" +#include "df/histfig_entity_link_former_squadst.h" +#include "df/histfig_entity_link_positionst.h" +#include "df/histfig_entity_link_squadst.h" #include "df/historical_figure.h" #include "df/historical_entity.h" +#include "df/history_event_remove_hf_entity_linkst.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" +#include "df/equipment_update.h" #include "df/plotinfost.h" +#include "df/military_routinest.h" #include "df/squad.h" -#include "df/squad_position.h" -#include "df/squad_schedule_order.h" +#include "df/squad_barracks_infost.h" +#include "df/squad_month_positionst.h" #include "df/squad_order.h" #include "df/squad_order_trainst.h" +#include "df/squad_position.h" +#include "df/squad_routine_schedulest.h" +#include "df/squad_schedule_entry.h" +#include "df/squad_schedule_order.h" +#include "df/unit.h" #include "df/world.h" using namespace DFHack; @@ -90,7 +106,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) result->id = *df::global::squad_next_id; result->uniform_priority = result->id + 1; //no idea why, but seems to hold result->supplies.carry_food = 2; - result->supplies.carry_water = df::squad::T_supplies::Water; + result->supplies.carry_water = df::squad_water_level_type::Water; result->entity_id = df::global::plotinfo->group_id; result->leader_position = corresponding_position->id; result->leader_assignment = found_assignment->id; @@ -110,7 +126,8 @@ df::squad* Military::makeSquad(int32_t assignment_id) for (const auto& routine : routines) { - df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + df::squad_routine_schedulest* schedule = new df::squad_routine_schedulest(); + auto &asched = schedule->month; for(int kk=0; kk < 12; kk++) { @@ -118,14 +135,14 @@ df::squad* Military::makeSquad(int32_t assignment_id) for(int jj=0; jj < squad_size; jj++) { - int32_t* order_assignments = new int32_t(); - *order_assignments = -1; + df::squad_month_positionst* order_assignments = new df::squad_month_positionst(); + order_assignments->assigned_order_idx = -1; asched[kk].order_assignments.push_back(order_assignments); } } - auto insert_training_order = [asched, squad_size](int month) + auto insert_training_order = [&](int month) { df::squad_schedule_order* order = new df::squad_schedule_order(); order->min_count = squad_size; @@ -206,7 +223,7 @@ df::squad* Military::makeSquad(int32_t assignment_id) } } - result->schedule.push_back(reinterpret_cast(asched)); + result->schedule.routine.push_back(schedule); } //Modify necessary world state @@ -228,8 +245,8 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (squad == nullptr || zone == nullptr) return; - df::squad::T_rooms* room_from_squad = nullptr; - df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + df::squad_barracks_infost* room_from_squad = nullptr; + df::building_squad_infost* room_from_building = nullptr; for (auto room : squad->rooms) { @@ -257,18 +274,18 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s if (!avoiding_squad_roundtrip && room_from_squad == nullptr) { - room_from_squad = new df::squad::T_rooms(); + room_from_squad = new df::squad_barracks_infost(); room_from_squad->building_id = civzone_id; - insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); + insert_into_vector(squad->rooms, &df::squad_barracks_infost::building_id, room_from_squad); } if (room_from_building == nullptr) { - room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building = new df::building_squad_infost(); room_from_building->squad_id = squad_id; - insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); + insert_into_vector(zone->squad_room_info, &df::building_squad_infost::squad_id, room_from_building); } if (room_from_squad) @@ -289,3 +306,229 @@ void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::s } } } + +static void remove_soldier_entity_link(df::historical_figure* hf, df::squad* squad) +{ + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::SQUAD) + continue; + + auto squad_link = strict_virtual_cast(link); + if (squad_link == nullptr || squad_link->squad_id != squad->id) + continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + start_year = squad_link->start_year; + + delete squad_link; + break; + } + + if (start_year == -1) + return; + + auto former_squad = df::allocate(); + former_squad->squad_id = squad->id; + former_squad->entity_id = squad->entity_id; + former_squad->start_year = start_year; + former_squad->end_year = *df::global::cur_year; + former_squad->link_strength = 100; + + hf->entity_links.push_back(former_squad); + + // Unassigning a normal soldier does not seem to generate an event record. +} + +static void remove_officer_entity_link(df::historical_figure* hf, df::squad* squad) +{ + std::vector nps; + if (!Units::getNoblePositions(&nps, hf)) + return; + + int32_t assignment_id = -1; + int32_t pos_id = -1; + for (auto& np : nps) + { + if (np.entity->id != squad->entity_id || np.assignment->squad_id != squad->id) + continue; + + np.assignment->histfig = -1; + np.assignment->histfig2 = -1; + + assignment_id = np.assignment->id; + pos_id = np.position->id; + break; + } + + if (assignment_id == -1) + return; + + int32_t start_year = -1; + for (size_t i = 0; i < hf->entity_links.size(); i++) + { + df::histfig_entity_link* link = hf->entity_links[i]; + if (link->getType() != df::enums::histfig_entity_link_type::POSITION) + continue; + + auto pos_link = strict_virtual_cast(link); + if (pos_link == nullptr) + continue; + if (pos_link->assignment_id != assignment_id && pos_link->entity_id != squad->entity_id) + continue; + + hf->entity_links.erase(hf->entity_links.begin() + i); + start_year = pos_link->start_year; + + delete pos_link; + break; + } + + if (start_year == -1) + return; + + auto former_pos = df::allocate(); + former_pos->assignment_id = assignment_id; + former_pos->entity_id = squad->entity_id; + former_pos->start_year = start_year; + former_pos->end_year = *df::global::cur_year; + former_pos->link_strength = 100; + + hf->entity_links.push_back(former_pos); + + int32_t event_id = (*df::global::hist_event_next_id)++; + auto former_pos_event = df::allocate(); + former_pos_event->year = *df::global::cur_year; + former_pos_event->seconds = *df::global::cur_year_tick; + former_pos_event->id = event_id; + former_pos_event->civ = squad->entity_id; + former_pos_event->histfig = hf->id; + former_pos_event->position_id = pos_id; + former_pos_event->link_type = df::histfig_entity_link_type::POSITION; + + df::global::world->history.events.push_back(former_pos_event); +} + +static void add_soldier_entity_link(df::historical_figure* hf, df::squad* squad, int32_t squad_pos) +{ + auto squad_link = df::allocate(); + squad_link->squad_id = squad->id; + squad_link->squad_position = squad_pos; + squad_link->entity_id = squad->entity_id; + squad_link->start_year = *df::global::cur_year; + squad_link->link_strength = 100; + + hf->entity_links.push_back(squad_link); +} + +bool Military::addToSquad(int32_t unit_id, int32_t squad_id, int32_t squad_pos) +{ + df::unit* unit = df::unit::find(unit_id); + if (unit == nullptr || unit->military.squad_id != -1) return false; + + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr) + return false; + + df::squad* squad = df::squad::find(squad_id); + if (squad == nullptr) return false; + + if (squad_pos == -1) + { + for (int p = 0; p < 10; p++) + { + auto pp = vector_get(squad->positions, p); + if (pp == nullptr || pp->occupant == -1) + { + squad_pos = p; + break; + } + } + } + if (squad_pos == -1) return false; + + // this function cannot (currently) change the squad commander + if (squad_pos == 0) return false; + + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos == nullptr) + pos = squad->positions[squad_pos] = df::allocate(); + + pos->occupant = hf->id; + // does anything else need to be set here? + + unit->military.squad_id = squad->id; + unit->military.squad_position = squad_pos; + + add_soldier_entity_link(hf, squad, squad_pos); + + #define F(flag) df::equipment_update::mask_##flag + auto constexpr update_flags = F(weapon) | F(armor) | F(shoes) | F(shield) | F(helm) | F(gloves) + | F(ammo) | F(pants) | F(backpack) | F(quiver) | F(flask); + #undef F + + squad->ammo.update.whole |= update_flags; + df::global::plotinfo->equipment.update.whole |= update_flags; + + return true; +} + +bool Military::removeFromSquad(int32_t unit_id) +{ + // based on unitst::remove_squad_info + df::unit *unit = df::unit::find(unit_id); + if (unit == nullptr || unit->military.squad_id == -1 || unit->military.squad_position == -1) + return false; + + // abort individual training + if (unit->individual_drills.size()) + unit->flags3.bits.verify_personal_training = true; + + int32_t squad_id = unit->military.squad_id; + int32_t squad_pos = unit->military.squad_position; + + df::squad* squad = df::squad::find(squad_id); + + if (squad) + { + df::squad_position* pos = vector_get(squad->positions, squad_pos); + if (pos) + { + // based on unitst::remove_squad_activity_info + FOR_ENUM_ITEMS(squad_event_type, i) + { + auto activity_entry = df::activity_entry::find(pos->activities[i]); + if (activity_entry) + { + auto activity_event = binsearch_in_vector(activity_entry->events, &df::activity_event::event_id, pos->events[i]); + if (activity_event) + { + activity_event->removeParticipant(unit->hist_figure_id, unit->id, false); + pos->activities[i] = -1; + pos->events[i] = -1; + } + } + } + + // remove from squad position + pos->occupant = -1; + } + } + // remove from unit information + unit->military.squad_id = -1; + unit->military.squad_position = -1; + + // remove entity squad link + df::historical_figure* hf = df::historical_figure::find(unit->hist_figure_id); + if (hf == nullptr || squad == nullptr) + return false; + + if (squad_pos == 0) // is unit a commander? + remove_officer_entity_link(hf, squad); + else + remove_soldier_entity_link(hf, squad); + + return true; +} diff --git a/library/modules/Persistence.cpp b/library/modules/Persistence.cpp index 0a6f1c3772..b9c3df577b 100644 --- a/library/modules/Persistence.cpp +++ b/library/modules/Persistence.cpp @@ -29,12 +29,14 @@ distribution. #include "LuaTools.h" #include "MemAccess.h" +#include "modules/DFSDL.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "modules/Persistence.h" #include "modules/World.h" #include "df/world.h" +#include "df/init.h" #include @@ -183,12 +185,12 @@ static std::string filterSaveFileName(std::string s) { return s; } -static std::string getSavePath(const std::string &world) { - return "save/" + world; +static std::filesystem::path getSavePath(const std::string &world) { + return Filesystem::getBaseDir() / "save" / world; } -static std::string getSaveFilePath(const std::string &world, const std::string &name) { - return getSavePath(world) + "/dfhack-" + filterSaveFileName(name) + ".dat"; +static std::filesystem::path getSaveFilePath(const std::string &world, const std::string &name) { + return getSavePath(world) / ("dfhack-" + filterSaveFileName(name) + ".dat"); } struct LastLoadSaveTickCountUpdater { @@ -266,7 +268,7 @@ static void add_entry(int entity_id, std::shared_ptr ent add_entry(store[entity_id], entry); } -static bool load_file(const std::string & path, int entity_id) { +static bool load_file(const std::filesystem::path & path, int entity_id) { Json::Value json; try { std::ifstream file(path); @@ -298,30 +300,30 @@ void Persistence::Internal::load(color_ostream& out) { clear(out); std::string world_name = World::ReadWorldFolder(); - std::string save_path = getSavePath(world_name); - std::vector files; + std::filesystem::path save_path = getSavePath(world_name); + std::vector files; if (0 != Filesystem::listdir(save_path, files)) { - DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '%s'\n", save_path.c_str()); + DEBUG(persistence,out).print("not loading state; save directory doesn't exist: '{}'\n", save_path); return; } bool found = false; for (auto & fname : files) { int entity_id = Persistence::WORLD_ENTITY_ID; - if (fname != "dfhack-world.dat" && !get_entity_id(fname, entity_id)) + if (fname != "dfhack-world.dat" && !get_entity_id(fname.string(), entity_id)) continue; found = true; - std::string path = save_path + "/" + fname; + std::filesystem::path path = save_path / fname; if (!load_file(path, entity_id)) - out.printerr("Cannot load data from: '%s'\n", path.c_str()); + out.printerr("Cannot load data from: '{}'\n", path); } if (found) return; // new file formats not found; attempt to load legacy file - const std::string legacy_fname = getSaveFilePath(world_name, "legacy-data"); + const std::filesystem::path legacy_fname = getSaveFilePath(world_name, "legacy-data"); if (Filesystem::exists(legacy_fname)) { int synthesized_entity_id = Persistence::WORLD_ENTITY_ID; if (World::IsSiteLoaded()) diff --git a/library/modules/Random.cpp b/library/modules/Random.cpp index f0d2054c9e..d6d682d921 100644 --- a/library/modules/Random.cpp +++ b/library/modules/Random.cpp @@ -35,7 +35,6 @@ using namespace std; #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "VTableInterpose.h" diff --git a/library/modules/Renderer.cpp b/library/modules/Renderer.cpp index e134fa4f3c..dc80390960 100644 --- a/library/modules/Renderer.cpp +++ b/library/modules/Renderer.cpp @@ -2,6 +2,8 @@ #include "MiscUtils.h" #include "modules/Renderer.h" +#include "df/global_objects.h" + using namespace DFHack; using df::global::enabler; using df::global::gps; diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 1a98837739..133caa82b2 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -28,7 +28,6 @@ distribution. #include "VersionInfo.h" #include "Types.h" #include "Error.h" -#include "ModuleFactory.h" #include "Core.h" #include "PluginManager.h" #include "LuaTools.h" @@ -55,6 +54,8 @@ distribution. #include "df/renderer.h" #include "df/plant.h" +#include +#include #include #include #include @@ -136,13 +137,8 @@ static bool doSetTile_map(const Pen &pen, int x, int y, int32_t * df::graphic_vi return true; } -static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field) +static bool doSetTile_char(const Pen &pen, int x, int y, bool use_graphics) { - bool use_graphics = Screen::inGraphicsMode(); - - if (map && use_graphics) - return doSetTile_map(pen, x, y, texpos_field); - if (x < 0 || x >= gps->dimx || y < 0 || y >= gps->dimy) return false; @@ -183,17 +179,21 @@ static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * *flag &= flag_mask; } - uint8_t fg = pen.fg | (pen.bold << 3); - uint8_t bg = pen.bg; + // out-of-range colors (including COLOR_RESET, which only has meaning + // on text consoles) get the Pen defaults, grey text on black, so + // they can't reach the buffers + uint8_t fg = (pen.fg < 0 || pen.fg > COLOR_MAX ? COLOR_GREY : pen.fg) + | (pen.bold << 3); + uint8_t bg = pen.bg < 0 || pen.bg > COLOR_MAX ? COLOR_BLACK : pen.bg; if (pen.tile_mode == Screen::Pen::CharColor) *flag |= 2; // SCREENTEXPOS_FLAG_ADDCOLOR else if (pen.tile_mode == Screen::Pen::TileColor) { *flag |= 1; // SCREENTEXPOS_FLAG_GRAYSCALE if (pen.tile_fg) - fg = pen.tile_fg; + fg = pen.tile_fg < 0 || pen.tile_fg > COLOR_MAX ? COLOR_GREY : pen.tile_fg; if (pen.tile_bg) - bg = pen.tile_bg; + bg = pen.tile_bg < 0 || pen.tile_bg > COLOR_MAX ? COLOR_BLACK : pen.tile_bg; } if (pen.tile && use_graphics) { @@ -215,18 +215,29 @@ static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * *texpos_lower = df::global::init->texpos_border_interior; // basic black background } - auto rgb_fg = &gps->uccolor[fg][0]; - auto rgb_bg = &gps->uccolor[bg][0]; - screen[1] = rgb_fg[0]; - screen[2] = rgb_fg[1]; - screen[3] = rgb_fg[2]; - screen[4] = rgb_bg[0]; - screen[5] = rgb_bg[1]; - screen[6] = rgb_bg[2]; + if (fg >= 0 && fg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[fg], &screen[1]); + else + WARN(screen).print("in doSetTile_char, fg {} out of range\n", fg); + + if (bg >= 0 && bg <= COLOR_MAX) + std::ranges::copy(gps->uccolor[bg], &screen[4]); + else + WARN(screen).print("in doSetTile_char, bg {} out of range\n", bg); return true; } +static bool doSetTile_default(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field) +{ + bool use_graphics = Screen::inGraphicsMode(); + + if (map && use_graphics) + return doSetTile_map(pen, x, y, texpos_field); + + return doSetTile_char(pen, x, y, use_graphics); +} + GUI_HOOK_DEFINE(Screen::Hooks::set_tile, doSetTile_default); static bool doSetTile(const Pen &pen, int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { @@ -287,12 +298,7 @@ static uint8_t to_16_bit_color(uint8_t *rgb) { return 0; } -static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { - bool use_graphics = Screen::inGraphicsMode(); - - if (map && use_graphics) - return doGetTile_map(x, y, texpos_field); - +static Pen doGetTile_char(int x, int y, bool use_graphics) { if (x < 0 || x >= gps->dimx || y < 0 || y >= gps->dimy) return Pen(0, 0, 0, -1); @@ -352,6 +358,14 @@ static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewp return ret; } +static Pen doGetTile_default(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { + bool use_graphics = Screen::inGraphicsMode(); + + if (map && use_graphics) + return doGetTile_map(x, y, texpos_field); + return doGetTile_char(x, y, use_graphics); +} + GUI_HOOK_DEFINE(Screen::Hooks::get_tile, doGetTile_default); static Pen doGetTile(int x, int y, bool map, int32_t * df::graphic_viewportst::*texpos_field = NULL) { @@ -365,6 +379,64 @@ Pen Screen::readTile(int x, int y, bool map, int32_t * df::graphic_viewportst::* return doGetTile(x, y, map, texpos_field); } +bool Screen::paintMapPortTile(const Pen &pen, int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + if (!gps || !pen.valid()) return false; + + bool use_graphics = Screen::inGraphicsMode(); + if (!use_graphics) + return doSetTile_char(pen, x, y, use_graphics); + + if (!texpos_field) + texpos_field = &df::graphic_map_portst::screentexpos_interface; + + auto &vp = gps->main_map_port; + if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) + return false; + + size_t max_index = vp->dim_y * vp->dim_x - 1; + size_t index = (y * vp->dim_x) + x; + + if (index > max_index) + return false; + + long texpos = pen.tile; + if (!texpos && pen.ch) + texpos = init->font.large_font_texpos[(uint8_t)pen.ch]; + (vp->*texpos_field)[index] = texpos; + return true; +} + +Pen Screen::readMapPortTile(int x, int y, int32_t * df::graphic_map_portst::*texpos_field) +{ + CHECK_NULL_POINTER(texpos_field) + + if (!gps) return Pen(0,0,0,-1); + + bool use_graphics = Screen::inGraphicsMode(); + + if (!use_graphics) + return doGetTile_char(x, y, use_graphics); + + auto &vp = gps->main_map_port; + + if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) + return Pen(0, 0, 0, -1); + + size_t max_index = vp->dim_x * vp->dim_y - 1; + size_t index = (y * vp->dim_x) + x; + + if (index < 0 || index > max_index) + return Pen(0, 0, 0, -1); + + auto tile = (vp->*texpos_field)[index]; + + char ch = 0; + uint8_t fg = 0; + uint8_t bg = 0; + return Pen(ch, fg, bg, tile, false); +} + bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text, bool map) { auto dim = getWindowSize(); @@ -621,7 +693,7 @@ std::set Screen::normalize_text_keys(const std::setlast_text_input[0]) { char c = df::global::enabler->last_text_input[0]; df::interface_key key = charToKey(c); - DEBUG(screen).print("adding character %c as interface key %ld\n", c, key); + DEBUG(screen).print("adding character {} as interface key {}\n", c, ENUM_AS_STR(key)); combined_keys.emplace(key); } return combined_keys; @@ -780,7 +852,7 @@ void dfhack_viewscreen::logic() bool is_df_screen = !is_instance(p); auto *next_p = p->parent; if (is_df_screen && Screen::isDismissed(p)) { - DEBUG(screen).print("raising dismissed DF viewscreen %p\n", p); + DEBUG(screen).print("raising dismissed DF viewscreen {}\n", static_cast(p)); Screen::raise(p); } if (is_df_screen) diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index 8b485a3a62..dfa4c458fe 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -14,6 +14,7 @@ #include "VTableInterpose.h" #include "df/enabler.h" +#include "df/global_objects.h" #include "df/viewscreen_adopt_regionst.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_new_arenast.h" @@ -54,7 +55,7 @@ static ReservedRange reserved_range{}; static std::unordered_map g_handle_to_texpos; static std::unordered_map g_handle_to_reserved_texpos; static std::unordered_map g_handle_to_surface; -static std::unordered_map> g_tileset_to_handles; +static std::unordered_map> g_tileset_to_handles; static std::vector g_delayed_regs; static std::mutex g_adding_mutex; static std::atomic loading_state = false; @@ -195,23 +196,23 @@ TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) { return handle; } -std::vector Textures::loadTileset(const std::string& file, int tile_px_w, +std::vector Textures::loadTileset(const std::filesystem::path file, int tile_px_w, int tile_px_h, bool reserved) { if (g_tileset_to_handles.contains(file)) return g_tileset_to_handles[file]; if (!enabler) return std::vector{}; - SDL_Surface* surface = DFIMG_Load(file.c_str()); + SDL_Surface* surface = DFIMG_Load(file.string().c_str()); if (!surface) { - ERR(textures).printerr("unable to load textures from '%s'\n", file.c_str()); + ERR(textures).printerr("unable to load textures from '{}'\n", file); return std::vector{}; } surface = canonicalize_format(surface); auto handles = slice_tileset(surface, tile_px_w, tile_px_h, reserved); - DEBUG(textures).print("loaded %zd textures from '%s'\n", handles.size(), file.c_str()); + DEBUG(textures).print("loaded {} textures from '{}'\n", handles.size(), file); g_tileset_to_handles[file] = handles; return handles; @@ -308,7 +309,7 @@ static void reset_surface() { } static void register_delayed_handles() { - DEBUG(textures).print("register delayed handles, size %zd\n", g_delayed_regs.size()); + DEBUG(textures).print("register delayed handles, size {}\n", g_delayed_regs.size()); for (auto& handle : g_delayed_regs) { auto texpos = add_texture(g_handle_to_surface[handle]); g_handle_to_texpos.emplace(handle, texpos); @@ -322,8 +323,9 @@ struct tracking_stage_new_region : df::viewscreen_new_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_raw_load_stage != this->raw_load_stage) { - TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, - this->raw_load_stage); + TRACE(textures).print("raw_load_stage {} -> {}\n", + this->m_raw_load_stage, + static_cast(this->raw_load_stage)); bool tmp_state = loading_state; loading_state = this->raw_load_stage >= 0 && this->raw_load_stage < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -346,7 +348,9 @@ struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -369,7 +373,9 @@ struct tracking_stage_load_region : df::viewscreen_loadgamest { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -392,7 +398,9 @@ struct tracking_stage_new_arena : df::viewscreen_new_arenast { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { if (this->m_cur_step != this->cur_step) { - TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step); + TRACE(textures).print("step {} -> {}\n", + this->m_cur_step, + static_cast(this->cur_step)); bool tmp_state = loading_state; loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; if (tmp_state != loading_state && !loading_state) @@ -446,7 +454,7 @@ void Textures::init(color_ostream& out) { reserve_static_range(); install_reset_point(); DEBUG(textures, out) - .print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start, + .print("dynamic texture loading ready, reserved range {}-{}\n", reserved_range.start, reserved_range.end); } diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index 38ae2c51ba..097de85cc9 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -27,7 +27,6 @@ distribution. #include "VersionInfo.h" #include "MemAccess.h" #include "Types.h" -#include "ModuleFactory.h" #include "Core.h" #include "Error.h" #include "DataDefs.h" diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 933ea53754..4c382266c3 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -27,11 +27,12 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" +#include "TileTypes.h" #include "Types.h" #include "VersionInfo.h" #include "modules/Items.h" +#include "modules/Job.h" #include "modules/Maps.h" #include "modules/Materials.h" #include "modules/Translation.h" @@ -39,10 +40,14 @@ distribution. #include "modules/World.h" #include "df/activity_entry.h" +#include "df/building_cagest.h" +#include "df/building_chainst.h" +#include "df/buildingitemst.h" #include "df/burrow.h" #include "df/caste_raw.h" #include "df/creature_interaction_effect_display_namest.h" #include "df/creature_raw.h" +#include "df/creature_raw_flags.h" #include "df/curse_attr_change.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" @@ -61,16 +66,24 @@ distribution. #include "df/historical_kills.h" #include "df/history_event_hist_figure_diedst.h" #include "df/identity.h" +#include "df/interaction_profilest.h" #include "df/item.h" #include "df/job.h" +#include "df/need_type.h" #include "df/nemesis_record.h" +#include "df/personality_goalst.h" +#include "df/personality_needst.h" #include "df/plotinfost.h" +#include "df/proj_unitst.h" +#include "df/reputation_profilest.h" #include "df/syndrome.h" +#include "df/squad.h" #include "df/tile_occupancy.h" #include "df/training_assignment.h" #include "df/unit.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" +#include "df/unit_active_animationst.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_path_goal.h" @@ -79,16 +92,20 @@ distribution. #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include "df/world_site.h" +#include "df/wrestle_state_type.h" #include +#include #include #include #include #include #include #include +#include #include using std::max; @@ -102,8 +119,8 @@ using df::global::gametype; using df::global::plotinfo; using df::global::world; -#define IS_ACTIVE_CASTE_FLAG(cf) !unit->curse.rem_tags1.bits.cf && \ -(unit->curse.add_tags1.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) +#define IS_ACTIVE_CASTE_FLAG(cf) !unit->uwss_remove_caste_flag.bits.cf && \ +(unit->uwss_add_caste_flag.bits.cf || casteFlagSet(unit->race, unit->caste, caste_raw_flags::cf)) // Common flags to exclude for fort members constexpr uint32_t exclude_flags1 = ( @@ -136,7 +153,10 @@ bool Units::isCitizen(df::unit *unit, bool include_insane) { // Copied from the conditions used to decide game over, // except that the game appears to let melancholy/raving // dwarves count as citizens. - if (unit->flags1.whole & exclude_flags1 || + // include_insane relaxes only the sanity check; the dead are + // never citizens (intelligent undead are animate, not dead) + if (isDead(unit) || + unit->flags1.whole & exclude_flags1 || unit->flags2.whole & exclude_flags2 || (!include_insane && !isSane(unit))) return false; @@ -145,7 +165,7 @@ bool Units::isCitizen(df::unit *unit, bool include_insane) { bool Units::isResident(df::unit *unit, bool include_insane) { CHECK_NULL_POINTER(unit); - if (!include_insane && !isSane(unit)) + if (isDead(unit) || (!include_insane && !isSane(unit))) return false; return isOwnCiv(unit) && @@ -202,7 +222,7 @@ bool Units::isOwnRace(df::unit *unit) { bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - return !isDead(unit) && !unit->curse.add_tags1.bits.NOT_LIVING; + return !isDead(unit) && !unit->uwss_add_caste_flag.bits.NOT_LIVING; } bool Units::isDead(df::unit *unit) { @@ -325,7 +345,7 @@ bool Units::isNaked(df::unit *unit, bool no_items) { for (auto inv_item : unit->inventory) { // TODO: Check for proper coverage (bad thought) - if (inv_item->mode == df::unit_inventory_item::Worn) + if (inv_item->mode == df::inv_item_role_type::Worn) return false; } return true; @@ -540,6 +560,12 @@ bool Units::isBloodsucker(df::unit *unit) { CHECK_NULL_POINTER(unit); return IS_ACTIVE_CASTE_FLAG(BLOODSUCKER); } + +bool Units::breathes(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return !IS_ACTIVE_CASTE_FLAG(NOBREATHE); +} #undef IS_ACTIVE_CASTE_FLAG bool Units::isDwarf(df::unit *unit) { @@ -600,7 +626,7 @@ bool Units::isInvader(df::unit *unit) { bool Units::isUndead(df::unit *unit, bool hiding_curse) { CHECK_NULL_POINTER(unit); - const auto &cb = unit->curse.add_tags1.bits; + const auto &cb = unit->uwss_add_caste_flag.bits; return unit->flags3.bits.ghostly || ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (hiding_curse || !isHidingCurse(unit))); } @@ -660,6 +686,8 @@ bool Units::isGreatDanger(df::unit *unit) { bool Units::isUnitInBox(df::unit *u, const cuboid &box) { CHECK_NULL_POINTER(u); + if (!isActive(u)) + return false; return box.containsPos(getPosition(u)); } @@ -669,7 +697,7 @@ bool Units::getUnitsInBox(vector &units, const cuboid &box, std::fun units.clear(); for (auto unit : world->units.active) - if (filter(unit) && isUnitInBox(unit, box)) + if (isUnitInBox(unit, box) && filter(unit)) units.push_back(unit); return true; } @@ -754,37 +782,203 @@ df::coord Units::getPosition(df::unit *unit) { bool Units::teleport(df::unit *unit, df::coord target_pos) { // Make sure source and dest map blocks are valid - auto old_occ = Maps::getTileOccupancy(unit->pos); + // getPosition resolves through a containing cage, so the source tile is + // correct even for caged units + const df::coord src_pos = getPosition(unit); + auto old_occ = Maps::getTileOccupancy(src_pos); auto new_occ = Maps::getTileOccupancy(target_pos); if (!old_occ || !new_occ) return false; + if (!unit->flags3.bits.ghostly) { + auto new_tt = Maps::getTileType(target_pos); + if (tileShapeBasic(tileShape(*new_tt)) == df::tiletype_shape_basic::Wall) + return false; + + using namespace df::enums::tile_building_occ; + if (auto b = new_occ->bits.building; + b == Obstacle || b == Well || b == Impassable || b == Dynamic) + return false; + } + + // these won't return nullptr because we already got occupancies from these coords. + if (Maps::getTileDesignation(src_pos)->bits.hidden) + return false; + if (Maps::getTileDesignation(target_pos)->bits.hidden) + return false; + + auto is_wagon = [](df::unit *unit) { + if (!unit) + return false; + auto craw = df::creature_raw::find(unit->race); + return craw && craw->flags.is_set(df::creature_raw_flags::EQUIPMENT_WAGON); + }; + + // EQUIPMENT units (e.g. wagons) occupy a 3x3 footprint centered on their + // position; all other units occupy just their position tile + auto unit_extent = [&](df::unit *unit) { + if (is_wagon(unit)) + return 1; + return 0; + }; + + const int extent = unit_extent(unit); + + auto for_each_occupied_tile = [&](df::coord center, auto &&fn) { + for (int dy = -extent; dy <= extent; ++dy) + for (int dx = -extent; dx <= extent; ++dx) { + df::coord tile(center.x+dx, center.y+dy, center.z); + if (auto occ = Maps::getTileOccupancy(tile)) + fn(tile, *occ); + } + }; + + // Occupancy flags are per-tile, not per-unit: only clear a flag when no + // other unit of the same kind still occupies the tile, matching how the + // game itself updates the flags when a unit leaves a tile + std::unordered_set grounded_tiles, standing_tiles; + for (auto other : world->units.active) { + // caged units and in-flight projectiles don't set tile occupancy, so + // they must not keep occupancy flags alive on their pos tile + if (other == unit || other->flags1.bits.caged || + other->flags1.bits.projectile) + continue; + auto &tiles = other->flags1.bits.on_ground ? grounded_tiles : standing_tiles; + const int other_extent = unit_extent(other); + for (int dy = -other_extent; dy <= other_extent; ++dy) + for (int dx = -other_extent; dx <= other_extent; ++dx) + tiles.emplace(other->pos.x+dx, other->pos.y+dy, other->pos.z); + } + // Clear appropriate occupancy flags at old tile - if (unit->flags1.bits.on_ground) - // This is potentially wrong, but the game will recompute this as needed - old_occ->bits.unit_grounded = false; - else - old_occ->bits.unit = false; + for_each_occupied_tile(src_pos, [&](df::coord tile, df::tile_occupancy &occ) { + if (unit->flags1.bits.on_ground) { + if (!grounded_tiles.contains(tile)) + occ.bits.unit_grounded = false; + } else if (!standing_tiles.contains(tile)) { + occ.bits.unit = false; + } + }); + + // Clear unit projectile info + if (unit->flags1.bits.projectile) { + unit->flags1.bits.projectile = false; + linked_list_remove(&world->projectiles.all, [&](df::projectile *proj) { + if (proj->getType() != df::enums::projectile_type::Unit) + return false; + if (auto unit_proj = virtual_cast(proj)) + return unit_proj->unit == unit; + return false; + }); + } // If there's already somebody standing at the destination, then force the unit to lay down - if (new_occ->bits.unit) + // (except riders never lay down) + if (new_occ->bits.unit && !unit->flags1.bits.rider) unit->flags1.bits.on_ground = true; // Set appropriate occupancy flags at new tile - if (unit->flags1.bits.on_ground) - new_occ->bits.unit_grounded = true; - else - new_occ->bits.unit = true; + for_each_occupied_tile(target_pos, [&](df::coord, df::tile_occupancy &occ) { + if (unit->flags1.bits.on_ground) + occ.bits.unit_grounded = true; + else + occ.bits.unit = true; + }); // Move unit to destination unit->pos = target_pos; unit->idle_area = target_pos; - // Move unit's riders (including babies) to destination - if (unit->flags1.bits.ridden) + // Move unit's riders (including babies and wounded) to destination + if (unit->flags1.bits.ridden || unit->flags4.bits.carrying_somebody) { for (auto rider : world->units.other[units_other_id::ANY_RIDER]) if (rider->relationship_ids[unit_relationship_type::RiderMount] == unit->id) - rider->pos = unit->pos; + // Wagon riders need special handling -- they are not at the center of the wagon, + // and the draft animal riders are not even in the extent of the wagon. + // Unified this case with the normal case by maintaining the rider's delta-pos from + // the mount, and recursively teleporting instead of just setting the rider's pos. + Units::teleport(rider, (rider->pos - src_pos) + unit->pos); + } + + // if the unit itself was a rider and it is not on the same tile as its mount, break that link. + // (except do nothing to wagon-riders). + if (unit->flags1.bits.rider) { + auto mount = df::unit::find(unit->relationship_ids[unit_relationship_type::RiderMount]); + if (mount && unit->pos != mount->pos && !is_wagon(mount)) { + unit->flags1.bits.rider = false; + unit->relationship_ids[unit_relationship_type::RiderMount] = -1; + // units.other.* are not sorted; do not use erase_from_vector(). + std::erase(world->units.other[units_other_id::ANY_RIDER], unit); + mount->flags1.bits.ridden = false; + mount->flags4.bits.carrying_somebody = false; + // rebuild the ridden flags; mothers can carry more than one baby. + for (auto rider : world->units.other[units_other_id::ANY_RIDER]) { + if (rider->relationship_ids[unit_relationship_type::RiderMount] == mount->id) { + if (rider->profession == df::profession::BABY || (mount->job.current_job + && mount->job.current_job->job_type == job_type::RecoverWounded)) + mount->flags4.bits.carrying_somebody = true; + else + mount->flags1.bits.ridden = true; + } + } + } + } + + auto delete_cagee_jobs = [](df::unit* unit, df::building* bld) { + std::vector jobs_to_delete; + for (auto job: bld->jobs) + if (findUnitRef(job->general_refs, df::general_ref_type::UNIT_CAGEE) == unit) + jobs_to_delete.push_back(job); + for (auto job: jobs_to_delete) + Job::removeJob(job); + }; + + if (auto cage = getContainer(unit)) { + auto bld = strict_virtual_cast(Items::getHolderBuilding(cage)); + if (bld && bld->contained_items.size() > 0 && bld->contained_items[0]->item == cage) { + delete_cagee_jobs(unit, bld); + std::erase(bld->assigned_units, unit->id); + bld->cage_flags.bits.check_occupants = true; + } + removeRef(cage->general_refs, df::general_ref_type::CONTAINS_UNIT, unit->id); + removeRef(unit->general_refs, df::general_ref_type::CONTAINED_IN_ITEM, cage->id); + unit->flags1.bits.caged = false; + } + + if (auto bld = strict_virtual_cast + (findBuildingRef(unit->general_refs, df::general_ref_type::BUILDING_CHAIN))) + { + delete_cagee_jobs(unit, bld); + if (bld->chained == unit) + bld->chained = nullptr; + if (bld->assigned == unit) + bld->assigned = nullptr; + removeRef(unit->general_refs, df::general_ref_type::BUILDING_CHAIN, bld->id); + unit->flags1.bits.chained = false; + } + + auto break_dragging_links = [&target_pos](df::unit* dragger, df::unit* draggee) { + if (!dragger || !draggee) + return; + if (dragger->flags1.bits.merchant && draggee->flags1.bits.merchant) { + // this cannot infinite-recurse because at least one of the units is already at target_pos. + if (dragger->pos != target_pos) + Units::teleport(dragger, target_pos); + if (draggee->pos != target_pos) + Units::teleport(draggee, target_pos); + return; + } + if ((isCitizen(dragger, false) || isResident(dragger, false)) && dragger->job.current_job != NULL) + Job::removeJob(dragger->job.current_job); + dragger->relationship_ids[unit_relationship_type::Draggee] = -1; + draggee->relationship_ids[unit_relationship_type::Dragger] = -1; + }; + + if (auto draggee = df::unit::find(unit->relationship_ids[unit_relationship_type::Draggee])) + break_dragging_links(unit, draggee); + if (auto dragger = df::unit::find(unit->relationship_ids[unit_relationship_type::Dragger])) + break_dragging_links(dragger, unit); + return true; } @@ -953,6 +1147,7 @@ void Units::setAutomaticProfessions(df::unit* unit) { // functionality reverse-engineered from DF's unitst::set_goal void Units::setPathGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal) { + CHECK_NULL_POINTER(unit); if (unit->path.dest != pos || unit->path.goal != goal) { unit->path.dest = pos; @@ -994,7 +1189,7 @@ int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr auto &aobj = unit->body.physical_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; if (isHidingCurse(unit)) @@ -1013,7 +1208,7 @@ int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) { auto &aobj = soul->mental_attrs[attr]; int value = max(0, aobj.value - aobj.soft_demotion); - if (auto mod = unit->curse.attr_change) + if (auto mod = unit->uwss_att_change) { int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; if (isHidingCurse(unit)) @@ -1133,7 +1328,27 @@ static string getTameTag(df::unit *unit) { } } -string Units::getReadableName(df::historical_figure *hf) { +template +static string formatReadableName(T *unit_or_hf, const string &prof_name, bool skip_english) { + auto visible_name = Units::getVisibleName(unit_or_hf); + string native_name = Translation::translateName(visible_name); + + if (native_name.empty()) + return prof_name; + + std::string prof_suffix{ (prof_name.size() > 0) ? (", " + prof_name) : "" }; + + if (skip_english) + return native_name + prof_suffix; + + string english_name = Translation::translateName(visible_name, true, true); + if (english_name.empty()) + return native_name + prof_suffix; + + return native_name + " \"" + english_name + "\"" + prof_suffix; +} + +string Units::getReadableName(df::historical_figure *hf, bool skip_english) { CHECK_NULL_POINTER(hf); string prof_name = getProfessionName(hf, false, false, true); @@ -1147,16 +1362,15 @@ string Units::getReadableName(df::historical_figure *hf) { prof_name = "Ghostly " + prof_name; } - string name = Translation::translateName(getVisibleName(hf)); - return name.empty() ? prof_name : name + ", " + prof_name; + return formatReadableName(hf, prof_name, skip_english); } -string Units::getReadableName(df::unit *unit) { +string Units::getReadableName(df::unit *unit, bool skip_english) { CHECK_NULL_POINTER(unit); string prof_name = getProfessionName(unit, false, false, true); - if (unit->curse.name_visible) // Necromancer, etc. - prof_name += " " + unit->curse.name; + if (unit->uwss_use_display_name) // Necromancer, etc. + prof_name += " " + unit->uwss_display_name_sing; else if (isGhost(unit)) // TODO: Should be "Ghost" instead of "Ghostly Peasant" prof_name = "Ghostly " + prof_name; // TODO: impersonating deity/force @@ -1170,8 +1384,7 @@ string Units::getReadableName(df::unit *unit) { if (isTame(unit)) prof_name += " (" + getTameTag(unit) + ")"; - string name = Translation::translateName(getVisibleName(unit)); - return name.empty() ? prof_name : name + ", " + prof_name; + return formatReadableName(unit, prof_name, skip_english); } double Units::getAge(df::unit *unit, bool true_age) { @@ -1247,7 +1460,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { // This is 100% reverse-engineered from DF code int rating = getNominalSkill(unit, skill_id, true); // Apply special states - if (unit->counters.soldier_mood == df::unit::T_counters::None) { + if (unit->counters.soldier_mood == df::soldier_mood_type::None) { if (unit->counters.nausea > 0) rating >>= 1; if (unit->counters.winded > 0) @@ -1260,7 +1473,7 @@ int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id) { rating >>= 1; } - if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance) { + if (unit->counters.soldier_mood != df::soldier_mood_type::MartialTrance) { if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle && !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged && !hasExtravision(unit)) @@ -1608,7 +1821,7 @@ float Units::computeSlowdownFactor(df::unit *unit) { if (!unit->flags1.bits.marauder && casteFlagSet(unit->race, unit->caste, caste_raw_flags::MEANDERER) && !(unit->following && isCitizen(unit)) && - linear_index(unit->inventory, &df::unit_inventory_item::mode, df::unit_inventory_item::Hauled) < 0) + linear_index(unit->inventory, &df::unit_inventory_item::mode, df::inv_item_role_type::Hauled) < 0) { coeff *= 4.0f; } @@ -1981,6 +2194,97 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) { return entry->events[entry->events.size() - 1]; } +int32_t Units::getFocusPenalty(df::unit* unit, need_type_set need_types) { + CHECK_NULL_POINTER(unit); + + int max_penalty = INT_MAX; + if (!unit->status.current_soul) { + return max_penalty; + } + auto& needs = unit->status.current_soul->personality.needs; + for (auto const need : needs) { + if (need_types.test(need->id)) { + max_penalty = min(max_penalty, need->focus_level); + } + } + return max_penalty; +} + +int32_t Units::getFocusPenalty(df::unit* unit, df::need_type need_type) { + auto need_types = need_type_set().set(need_type); + return getFocusPenalty(unit, need_types); +} + +// reverse engineered from unitst::have_unbailable_sp_activities (partial implementation) +bool Units::hasUnbailableSocialActivity(df::unit *unit) +{ + // these can become constexpr with C++23 + static const need_type_set pray_needs = need_type_set() + .set(df::need_type::PrayOrMeditate); + + static const need_type_set socialize_needs = need_type_set() + .set(df::need_type::Socialize) + .set(df::need_type::BeCreative) + .set(df::need_type::Excitement) + .set(df::need_type::AdmireArt); + + static const need_type_set read_needs = need_type_set() + .set(df::need_type::ThinkAbstractly) + .set(df::need_type::LearnSomething); + + CHECK_NULL_POINTER(unit); + + if (unit->social_activities.empty()) { + return false; + } else if (unit->social_activities.size() > 1) { + return true; // is this even possible? + } + + auto activity = df::activity_entry::find(unit->social_activities[0]); + if (activity) { + using df::activity_entry_type; + switch (activity->type) { + case activity_entry_type::Socialize: + return getFocusPenalty(unit, socialize_needs) <= -10000; + case activity_entry_type::Prayer: + return getFocusPenalty(unit, pray_needs) <= -10000; + case activity_entry_type::Read: + return getFocusPenalty(unit, read_needs) <= -10000; + default: + // consider unhandled activities as uninterruptible + return true; + } + } + // this should never happen + return false; +} + +bool Units::isJobAvailable(df::unit *unit, bool preserve_social = false){ + if (unit->job.current_job) + return false; + if (unit->flags1.bits.caged || unit->flags1.bits.chained) + return false; + if (Units::getSpecificRef(unit, df::specific_ref_type::ACTIVITY)) + return false; + if (unit->individual_drills.size() > 0) { + if (unit->individual_drills.size() > 1) + return false; // this is even possible + auto activity = df::activity_entry::find(unit->individual_drills[0]); + if (activity && (activity->type == df::activity_entry_type::FillServiceOrder)) + return false; + } + if (hasUnbailableSocialActivity(unit)) + return false; + if (preserve_social && unit->social_activities.size() > 0) + return false; + if (unit->military.squad_id != -1) { + auto squad = df::squad::find(unit->military.squad_id); + if (squad) + return squad->orders.size() == 0 && squad->activity == -1; + } + return true; +} + // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; @@ -2042,6 +2346,14 @@ static int32_t *getActionTimerPointer(df::unit_action *action) { return &action->data.dismount.timer; case unit_action_type::HoldItem: return &action->data.holditem.timer; + case unit_action_type::LoadRangedWeapon: + return &action->data.loadrangedweapon.movewait; + case unit_action_type::ShootRangedWeapon: + return &action->data.shootrangedweapon.movewait; + case unit_action_type::ThrowItem: + return &action->data.throwitem.movewait; + case unit_action_type::PostShootRecovery: + return &action->data.postshootrecovery.movewait; case unit_action_type::LeadAnimal: case unit_action_type::StopLeadAnimal: case unit_action_type::Jump: @@ -2168,3 +2480,37 @@ void Units::setGroupActionTimers(color_ostream &out, df::unit *unit, } } } + +// this is a (loose) reimplementation of df's `unit_handlerst::get_cached_unit_by_global_id` +df::unit* Units::get_cached_unit_by_global_id(int32_t id, int32_t& index) +{ + auto& vector = df::unit::get_vector(); + auto len = vector.size(); + + if (len == 0 || id == -1) + return nullptr; + + if (index > -1 && (size_t)index < len) + { + auto unit = vector[index]; + if (id == unit->id) + return unit; + } + index = binsearch_index(vector, &df::unit::id, id); + return index != -1 ? vector[index] : nullptr; +} + +// reverse engineered from df's unitst::breathingstate +Units::breathing_state Units::getBreathingState(df::unit* unit) +{ + using enum breathing_state; + + if (!Units::breathes(unit)) return FINE; + if (unit->flags1.bits.drowning) return CANT; + + auto& wrestle_items = unit->status.wrestle_items; + if (std::any_of(wrestle_items.begin(), wrestle_items.end(), [] (auto w) { return w->advantage < 0 && w->state == df::wrestle_state_type::Choke; })) + return CANT; + + return unit->flags2.bits.breathing_problem ? ( unit->flags2.bits.breathing_good ? TROUBLE : CANT ) : FINE; +} diff --git a/library/modules/World.cpp b/library/modules/World.cpp index b41c625f17..d4fd2e0682 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -29,6 +29,7 @@ distribution. #include "modules/Gui.h" #include "modules/Translation.h" #include "modules/Units.h" +#include "modules/Maps.h" #include "modules/World.h" #include "modules/Translation.h" @@ -37,6 +38,7 @@ distribution. #include "df/map_block.h" #include "df/plotinfost.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/weather_type.h" #include "df/world.h" #include "df/world_data.h" #include "df/world_site.h" @@ -153,11 +155,9 @@ uint32_t World::ReadCurrentDay() return ((ReadCurrentTick() / 1200) % 28) + 1; } -uint8_t World::ReadCurrentWeather() +df::weather_type World::ReadCurrentWeather() { - if (df::global::current_weather) - return (*df::global::current_weather)[2][2]; - return 0; + return Maps::getCurrentWeather(); } void World::SetCurrentWeather(uint8_t weather) @@ -208,7 +208,7 @@ bool World::isLegends(df::game_type t) } df::unit * World::getAdventurer() { - if (!isAdventureMode() || !world) + if (!world || !df::global::gamemode || *df::global::gamemode != game_mode::ADVENTURE) return NULL; return world->units.adv_unit; @@ -217,22 +217,29 @@ df::unit * World::getAdventurer() { int32_t World::GetCurrentSiteId() { if (!plotinfo) return -1; - if (isFortressMode()) - return plotinfo->site_id; + if (isFortressMode()) { + // on a reclaimed fortress, site_id isn't assigned until the first + // save; fortress_site is set at embark, so use it as a fallback + if (plotinfo->site_id >= 0) + return plotinfo->site_id; + if (auto site = plotinfo->main.fortress_site) + return site->id; + return -1; + } if (auto adv = getAdventurer(); adv && world->world_data) { DEBUG(world).print("searching for adventure site\n"); auto & world_map = world->map; auto adv_pos = Units::getPosition(adv); - DEBUG(world).print("adv_pos: (%d, %d, %d)\n", adv_pos.x, adv_pos.y, adv_pos.z); + DEBUG(world).print("adv_pos: ({}, {}, {})\n", adv_pos.x, adv_pos.y, adv_pos.z); df::coord2d rgn_pos(world_map.region_x + adv_pos.x/48, world_map.region_y + adv_pos.y/48); for (auto site : world->world_data->sites) { - DEBUG(world).print("scanning site %d: %s\n", site->id, Translation::translateName(&site->name, true).c_str()); - DEBUG(world).print(" rgn_pos: (%d, %d); site bounds: (%d, %d), (%d, %d) \n", + DEBUG(world).print("scanning site {}: {}\n", site->id, Translation::translateName(&site->name, true)); + DEBUG(world).print(" rgn_pos: ({}, {}); site bounds: ({}, {}), ({}, {}) \n", rgn_pos.x, rgn_pos.y, site->global_min_x, site->global_min_y, site->global_max_x, site->global_max_y); if (rgn_pos.x >= site->global_min_x && rgn_pos.x <= site->global_max_x && rgn_pos.y >= site->global_min_y && rgn_pos.y <= site->global_max_y) { - DEBUG(world).print("found site: %d\n", site->id); + DEBUG(world).print("found site: {}\n", site->id); return site->id; } } diff --git a/library/xml b/library/xml index 9d207f7b70..2e38688ea0 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9d207f7b70c7ecf3835b6ca7506e598622d29d04 +Subproject commit 2e38688ea0e4e20f3a7dd48eabe77458b3a9c19f diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5a850c6cf9..3b07abf99a 100644 --- a/package/launchdf.cpp +++ b/package/launchdf.cpp @@ -10,6 +10,9 @@ #include "steam_api.h" #include +#include +#include +#include #define xstr(s) str(s) #define str(s) #s @@ -234,10 +237,85 @@ bool waitForDF(bool nowait) { #endif +constexpr const char* old_filelist[] { + "hack", + "stonesense", +#ifdef WIN32 + "binpatch.exe", + "dfhack-run.exe", + "allegro-5.2.dll", + "allegro_color-5.2.dll", + "allegro_font-5.2.dll", + "allegro_image-5.2.dll", + "allegro_primitives-5.2.dll", + "allegro_ttf-5.2.dll", + "allegro-5.2.dll", + "dfhack-client.dll", + "dfhooks_dfhack.dll", + "lua53.dll", + "protobuf-lite.dll" +#else + "binpatch", + "dfhack-run", + "liballegro-5.2.so", + "liballegro_color-5.2.so", + "liballegro_font-5.2.so", + "liballegro_image-5.2.so", + "liballegro_primitives-5.2.so", + "liballegro_ttf-5.2.so", + "liballegro-5.2.so", + "libdfhack-client.so", + "libdfhooks_dfhack.so", + "liblua53.so", + "libprotobuf-lite.so" +#endif +}; + +bool check_for_old_install(std::filesystem::path df_path) +{ + for (auto file : old_filelist) + { + std::filesystem::path p = df_path / file; + if (std::filesystem::exists(p)) + return true; + } + return false; +} + +void remove_old_install(std::filesystem::path df_path) +{ + std::string message{ + "Removing legacy files:" + }; + + for (auto file : old_filelist) + { + std::error_code ec; + + std::filesystem::path p = df_path / file; + + if (std::filesystem::is_directory(p)) + std::filesystem::remove_all(p, ec); + else if (std::filesystem::is_regular_file(p)) + std::filesystem::remove(p, ec); + else + continue; + + message += "\n" + p.string() + ": " + (ec ? "failed to remove - " + ec.message() : "removed successfully"); + } +#ifdef WIN32 + MessageBoxW(NULL, std::wstring(message.begin(), message.end()).c_str(), L"Legacy Install Cleanup", 0); +#endif +} + #ifdef WIN32 int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { #else int main(int argc, char* argv[]) { +#endif +#ifdef WIN32 + // force UTF-8 + std::setlocale(LC_ALL, ".utf8"); #endif // initialize steam context if (SteamAPI_RestartAppIfNecessary(DFHACK_STEAM_APPID)) { @@ -265,50 +343,136 @@ int main(int argc, char* argv[]) { } #ifdef WIN32 - if (is_running_on_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); - } + bool wine_detected = is_running_on_wine(); #endif - // steam detected and not running in wine + bool df_detected = SteamApps()->BIsAppInstalled(DF_STEAM_APPID); - if (!SteamApps()->BIsAppInstalled(DF_STEAM_APPID)) { + if (!df_detected) { // Steam DF is not installed. Assume DF is installed in same directory as DFHack and do a fallback launch exit(wrap_launch(launch_direct) ? 0 : 1); } - // obtain DF app path + // obtain DF and DFHack app paths - char buf[2048] = ""; + auto get_app_path_from_steam = [] (AppId_t appid) -> std::optional { + constexpr auto BUFSIZE = 2048; + char buf[BUFSIZE] = ""; + int bytes = SteamApps()->GetAppInstallDir(appid, (char*)&buf, BUFSIZE); + if (bytes <= 0) + return std::nullopt; + // steam API includes one or more null terminators after the path, so trim those off + for (; bytes > 0 && buf[bytes-1] == '\0'; bytes--); + return std::string(buf, bytes); + }; - int b1 = SteamApps()->GetAppInstallDir(DFHACK_STEAM_APPID, (char*)&buf, 2048); - std::string dfhack_install_folder = (b1 != -1) ? std::string(buf) : ""; + auto opt_dfhack_install_folder = get_app_path_from_steam(DFHACK_STEAM_APPID); + auto opt_df_install_folder = get_app_path_from_steam(DF_STEAM_APPID); + + if (opt_dfhack_install_folder && opt_df_install_folder && (*opt_df_install_folder != *opt_dfhack_install_folder)) + { + auto& dfhack_install_folder = *opt_dfhack_install_folder; + auto& df_install_folder = *opt_df_install_folder; - int b2 = SteamApps()->GetAppInstallDir(DF_STEAM_APPID, (char*)&buf, 2048); - std::string df_install_folder = (b2 != -1) ? std::string(buf) : ""; +#ifdef WIN32 + constexpr auto dfhooks_dll_name = "dfhooks.dll"; + constexpr auto dfhook_dfhack_dll_name = "dfhooks_dfhack.dll"; +#else + constexpr auto dfhooks_dll_name = "libdfhooks.so"; + constexpr auto dfhook_dfhack_dll_name = "libdfhooks_dfhack.so"; +#endif + // DF and DFHack are not co-installed (modern case) + // inject dfhooks.dll and dfhooks_dfhack.ini into DF install folder + std::filesystem::path dfhooks_dll_src = dfhack_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_dll_dst = df_install_folder / dfhooks_dll_name; + std::filesystem::path dfhooks_ini_dst = df_install_folder / "dfhooks_dfhack.ini"; + std::filesystem::path dfhooks_dfhack_dll_src = dfhack_install_folder / "hack" / dfhook_dfhack_dll_name; + std::error_code ec; - if (df_install_folder != dfhack_install_folder) { - // DF and DFHack are not installed in the same library + std::filesystem::copy(dfhooks_dll_src, dfhooks_dll_dst, std::filesystem::copy_options::update_existing, ec); + if (!ec) + { + std::string indirection; + if (std::filesystem::exists(dfhooks_ini_dst)) + { + std::ifstream ini(dfhooks_ini_dst); + std::getline(ini, indirection); + } + + if (indirection != dfhooks_dfhack_dll_src.string()) + { + std::ofstream ini(dfhooks_ini_dst); + ini << dfhooks_dfhack_dll_src.string() << std::endl; + } + } + else + { #ifdef WIN32 - MessageBoxW(NULL, L"DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting.", NULL, 0); + std::wstring message{ + L"Failed to inject DFHack into Dwarf Fortress\n\n" + L"Details:\n" + dfhooks_dll_src.wstring() + + L" -> " + dfhooks_dll_dst.wstring() + + L"\n\nError code: " + std::to_wstring(ec.value()) + + L"\nError message: " + std::filesystem::relative(ec.message()).wstring() + }; + + MessageBoxW(NULL, message.c_str(), NULL, 0); #else - notify("DFHack and Dwarf Fortress must be installed in the same Steam library.\nAborting."); + std::string message{ + "Failed to inject DFHack into Dwarf Fortress\n\n" + "Details:\n" + dfhooks_dll_src.string() + + " -> " + dfhooks_dll_dst.string() + + "\n\nError code: " + std::to_string(ec.value()) + + "\nError message: " + std::filesystem::relative(ec.message()).string() + }; + + notify(message.c_str()); #endif - exit(1); + exit(1); + } + bool dirty = check_for_old_install(df_install_folder); + if (dirty) + { +#ifdef WIN32 + int ok = MessageBoxW(NULL, L"A legacy install of DFHack has been detected in the Dwarf Fortress folder. This likely means that you have installed DFHack with the old Steam client (or manually). This legacy installation will almost certainly interfere with using DFHack. Do you want to remove the old files now? (recommended)", L"Legacy DFHack Install Detected", MB_OKCANCEL); + + if (ok == IDOK) + remove_old_install(df_install_folder); +#else + std::string filelist; + for (auto file : old_filelist) + if (std::filesystem::exists(df_install_folder / file)) + filelist += (filelist.empty() ? "" : std::string(",")) + file; + + std::string message{ + "A legacy install of DFHack has been detected in the Dwarf Fortress directory.This likely means that you have installed DFHack with the old Steam client (or manually).This installation will almost certainly interfere with using DFHack. \n\n" + "To remove these files, run the following command: rm -r " + df_install_folder.string() + "/{ " + filelist + "}\n\n" + }; + + notify(message.c_str()); +#endif + } + } + +#ifdef WIN32 + if (wine_detected) + { + // 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); } +#endif if (!wrap_launch(launch_via_steam)) exit(1); @@ -329,6 +493,5 @@ int main(int argc, char* argv[]) { usleep(1000000); #endif } - exit(0); } diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index 3ed5455d0f..30f270fbc0 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -8,6 +8,7 @@ #include "modules/Random.h" #include "modules/World.h" +#include "df/global_objects.h" #include "df/inorganic_raw.h" #include "df/map_block.h" #include "df/world.h" @@ -433,13 +434,13 @@ struct GeoLayer void print_mineral_stats(color_ostream &out) { for (auto it = mineral_count.begin(); it != mineral_count.end(); ++it) - INFO(process, out).print("3dveins: %s %s: %d (%f)\n", - MaterialInfo(0, it->first.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, it->first.second).c_str(), + INFO(process, out).print("3dveins: {} {}: {} ({})\n", + MaterialInfo(0, it->first.first).getToken(), + ENUM_KEY_STR(inclusion_type, it->first.second), it->second, (float(it->second) / unmined_tiles)); - INFO(process, out).print ("3dveins: Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); + INFO(process, out).print ("3dveins: Total tiles: {} ({} unmined)\n", tiles, unmined_tiles); } bool form_veins(color_ostream &out); @@ -471,12 +472,12 @@ struct GeoBiome void print_mineral_stats(color_ostream &out) { - INFO(process,out).print("3dveins: Geological biome %d:\n", info.geo_index); + INFO(process,out).print("3dveins: Geological biome {}:\n", info.geo_index); for (size_t i = 0; i < layers.size(); i++) if (layers[i]) { - INFO(process, out).print("3dveins: Layer %ld\n", i); + INFO(process, out).print("3dveins: Layer {}\n", i); layers[i]->print_mineral_stats(out); } } @@ -568,7 +569,7 @@ struct VeinGenerator bool VeinGenerator::init_biomes() { auto &mats = df::inorganic_raw::get_vector(); - materials.resize(world->raws.inorganics.size()); + materials.resize(world->raws.inorganics.all.size()); for (size_t i = 0; i < mats.size(); i++) { @@ -590,7 +591,7 @@ bool VeinGenerator::init_biomes() if (info.geo_index < 0 || !info.geobiome) { - WARN(process, out).print("Biome %zd is not defined.\n", i); + WARN(process, out).print("Biome {} is not defined.\n", i); return false; } @@ -654,7 +655,9 @@ bool GeoBiome::init_layers() GeoLayer *VeinGenerator::mapLayer(Block *pb, df::coord2d tile) { int idx = pb->biomeIndexAt(tile); - GeoBiome *biome = biome_by_idx.at(idx); + if (idx < 0) + return NULL; + GeoBiome *biome = biome_by_idx[idx]; int lidx = pb->layerIndexAt(tile); if (unsigned(lidx) >= biome->layers.size()) @@ -801,7 +804,7 @@ bool VeinGenerator::scan_layer_depth(Block *b, df::coord2d column, int z) { if (z != min_level[idx]-1 && min_level[idx] <= top_solid) { - WARN(process, out).print("Discontinuous layer %d at (%d,%d,%d).\n", + WARN(process, out).print("Discontinuous layer {} at ({} {} {}).\n", layer->index, x+column.x*16, y+column.y*16, z ); return false; @@ -852,7 +855,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) if (max_level[i+1] != min_level[i]-1) { WARN(process, out).print( - "Gap or overlap with next layer %d at (%d,%d,%d-%d).\n", + "Gap or overlap with next layer {} at ({} {} {}-{}).\n", i+1, x+column.x*16, y+column.y*16, max_level[i+1], min_level[i] ); return false; @@ -895,7 +898,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) } WARN(process, out).print( - "Layer height change in layer %d at (%d,%d,%d): %d instead of %d.\n", + "Layer height change in layer {} at ({} {} {}): {} instead of {}.\n", i, x+column.x*16, y+column.y*16, max_level[i], size, biome->layers[i]->thickness ); @@ -936,7 +939,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) if (unsigned(key.first) >= materials.size() || unsigned(key.second) >= NUM_INCLUSIONS) { - WARN(process, out).print("Invalid vein code: %d %d - aborting.\n",key.first,key.second); + WARN(process, out).print("Invalid vein code: {} {} - aborting.\n",key.first,ENUM_AS_STR(key.second)); return false; } @@ -946,9 +949,9 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) { // Report first occurence of unreasonable vein spec WARN(process, out).print( - "Unexpected vein %s %s - ", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Unexpected vein {} {} - ", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); status = materials[key.first].default_type; @@ -956,8 +959,8 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) WARN(process, out).print("will be left in place.\n"); else WARN(process, out).print( - "correcting to %s.\n", - ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)).c_str() + "correcting to {}.\n", + ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)) ); } @@ -1095,7 +1098,7 @@ void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z) if (!ok) { WARN(process, out).print( - "Couldn't write %d vein at (%d,%d,%d)\n", + "Couldn't write {} vein at ({} {} {})\n", mat, x+column.x*16, y+column.y*16, z ); } @@ -1285,7 +1288,7 @@ bool GeoLayer::form_veins(color_ostream &out) if (parent_id >= (int)refs.size()) { - WARN(process, out).print("Forward vein reference in biome %d.\n", biome->info.geo_index); + WARN(process, out).print("Forward vein reference in biome {}.\n", biome->info.geo_index); return false; } @@ -1306,10 +1309,10 @@ bool GeoLayer::form_veins(color_ostream &out) ctx = "only be in "+MaterialInfo(0,vptr->parent_mat()).getToken(); WARN(process, out).print( - "Duplicate vein %s %s in biome %d layer %d - will %s.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), - biome->info.geo_index, index, ctx.c_str() + "Duplicate vein {} {} in biome {} layer {} - will {}.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), + biome->info.geo_index, index, ctx ); } @@ -1362,9 +1365,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (best.empty()) { WARN(process,out).print( - "Could not place orphaned vein %s %s anywhere.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str() + "Could not place orphaned vein {} {} anywhere.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second) ); return true; @@ -1396,9 +1399,9 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (size > 0) { WARN(process, out).print( - "Could not place all of orphaned vein %s %s: %d left.\n", - MaterialInfo(0,key.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, key.second).c_str(), + "Could not place all of orphaned vein {} {}: {} left.\n", + MaterialInfo(0,key.first).getToken(), + ENUM_KEY_STR(inclusion_type, key.second), size ); } @@ -1546,8 +1549,8 @@ bool VeinGenerator::place_veins(bool verbose) if (!isStoneInorganic(key.first)) { WARN(process, out).print( - "Invalid vein material: %s\n", - MaterialInfo(0, key.first).getToken().c_str() + "Invalid vein material: {}\n", + MaterialInfo(0, key.first).getToken() ); return false; @@ -1555,7 +1558,7 @@ bool VeinGenerator::place_veins(bool verbose) if (!is_valid_enum_item(key.second)) { - WARN(process, out).print("Invalid vein type: %d\n", key.second); + WARN(process, out).print("Invalid vein type: {}\n", ENUM_AS_STR(key.second)); return false; } @@ -1568,16 +1571,16 @@ bool VeinGenerator::place_veins(bool verbose) sort(queue.begin(), queue.end(), vein_cmp); // Place tiles - TRACE(process,out).print("Processing... (%zu)", queue.size()); + TRACE(process,out).print("Processing... ({})", queue.size()); for (size_t j = 0; j < queue.size(); j++) { if (queue[j]->parent && !queue[j]->parent->placed) { WARN(process, out).print( - "\nParent vein not placed for %s %s.\n", - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str() + "\nParent vein not placed for {} {}.\n", + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second) ); return false; @@ -1591,16 +1594,16 @@ bool VeinGenerator::place_veins(bool verbose) } TRACE(process, out).print( - "\nVein layer %zu of %zu: %s %s (%.2f%%)... ", + "\nVein layer {} of {}: {} {} ({:.2})... ", j+1, queue.size(), - MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), - ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str(), + MaterialInfo(0,queue[j]->vein.first).getToken(), + ENUM_KEY_STR(inclusion_type, queue[j]->vein.second), queue[j]->density() * 100 ); } else { - TRACE(process, out).print("\rVein layer %zu of %zu... ", j+1, queue.size()); + TRACE(process, out).print("\rVein layer {} of {}... ", j+1, queue.size()); } queue[j]->place_tiles(); diff --git a/plugins/Brushes.h b/plugins/Brushes.h index 14a4188e76..38847cac5a 100644 --- a/plugins/Brushes.h +++ b/plugins/Brushes.h @@ -5,6 +5,8 @@ #include #include +#include "modules/MapCache.h" + typedef vector coord_vec; class Brush { @@ -92,8 +94,8 @@ class BlockBrush : public Brush coord_vec points(MapExtras::MapCache & mc, DFHack::DFCoord start) { coord_vec v; - DFHack::DFCoord blockc = start / 16; - DFHack::DFCoord iterc = blockc * 16; + DFHack::DFCoord blockc = DFHack::Maps::getTileBlockCoord(start); + DFHack::DFCoord iterc = DFHack::Maps::getBlockOrigin(blockc); if( !mc.testCoord(start) ) return v; auto starty = iterc.y; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5e..f8a75436bd 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -27,22 +27,31 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # Plugins # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. +# +# CAVEAT: currently (June 2026) DFHack's core code will only autoload plugins from +# the hack/plugins directory. They cannot be autoloaded from mod directories. +# This has been explicit policy for several years. # Example plugin that uses protobufs # proto file must be in the proto/ folder # dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) +# warning: for complicated reasons, if a plugin uses a vmethod interpose, it must +# link to lua even if it doesn't otherwise use lua. we'll fix this someday, we promise. +# we apologize for the inconvenience. + option(BUILD_SUPPORTED "Build the supported plugins (reveal, probe, etc.)." ON) if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) - dfhack_plugin(add-spatter add-spatter.cpp) + dfhack_plugin(army-controller-sanity army-controller-sanity.cpp) + dfhack_plugin(add-spatter add-spatter.cpp LINK_LIBRARIES lua) dfhack_plugin(aquifer aquifer.cpp LINK_LIBRARIES lua) dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) dfhack_plugin(autoclothing autoclothing.cpp LINK_LIBRARIES lua) dfhack_plugin(design design.cpp LINK_LIBRARIES lua) dfhack_plugin(autodump autodump.cpp) - dfhack_plugin(autofarm autofarm.cpp) + dfhack_plugin(autofarm autofarm.cpp LINK_LIBRARIES lua) #dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) add_subdirectory(autolabor) dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) @@ -70,7 +79,9 @@ if(BUILD_SUPPORTED) dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) + dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) + dfhack_plugin(export-world-map export-world-map.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) @@ -111,8 +122,9 @@ if(BUILD_SUPPORTED) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(steam-engine steam-engine.cpp) - add_subdirectory(spectate) - #dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) + add_subdirectory(smooth-movement) + dfhack_plugin(spectate spectate.cpp LINK_LIBRARIES lua) + dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp LINK_LIBRARIES lua) dfhack_plugin(strangemood strangemood.cpp) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 3726b86c7c..7408e44031 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -124,7 +124,7 @@ macro(dfhack_plugin) target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") target_link_libraries(${PLUGIN_NAME} protobuf-lite) endif() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${PLUGIN_LINK_LIBRARIES}) + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version lua ${FMTLIB} ${PLUGIN_LINK_LIBRARIES}) if(UNIX) set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") @@ -141,6 +141,10 @@ macro(dfhack_plugin) set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dll) endif() + if (UNIX) + set_target_properties(${PLUGIN_NAME} PROPERTIES INSTALL_RPATH "$ORIGIN/..") + endif() + install(TARGETS ${PLUGIN_NAME} LIBRARY DESTINATION ${DFHACK_PLUGIN_DESTINATION} RUNTIME DESTINATION ${DFHACK_PLUGIN_DESTINATION}) diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp index 0ece73d982..a2685983fb 100644 --- a/plugins/add-spatter.cpp +++ b/plugins/add-spatter.cpp @@ -83,7 +83,7 @@ static void find_material(int *type, int *index, df::item *input, MaterialSource if (!info.findProduct(info, mat.product_name)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.printerr("Cannot find product '%s'\n", mat.product_name.c_str()); + out.printerr("Cannot find product '{}'\n", mat.product_name); } } @@ -293,7 +293,7 @@ static void find_reagent( return; } - out.printerr("Invalid reagent name '%s' in '%s'\n", name.c_str(), react->code.c_str()); + out.printerr("Invalid reagent name '{}' in '{}'\n", name, react->code); } static void parse_product( diff --git a/plugins/aquifer.cpp b/plugins/aquifer.cpp index 124fa29838..98968a9496 100644 --- a/plugins/aquifer.cpp +++ b/plugins/aquifer.cpp @@ -3,9 +3,11 @@ #include "PluginManager.h" #include "PluginLua.h" #include "TileTypes.h" +#include "Types.h" #include "modules/Maps.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" @@ -35,7 +37,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded map.\n", plugin_name); + out.printerr("Cannot run {} without a loaded map.\n", plugin_name); return CR_FAILURE; } @@ -142,21 +144,22 @@ static int get_max_aq_z(color_ostream &out, const df::coord &pos1, const df::coo static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::coord & pos1, const df::coord & pos2, int levels, bool top_is_aq = false, int skip_top = 0) { - DEBUG(log,out).print("get_z_range: top_is_aq=%d, skip_top=%d\n", top_is_aq, skip_top); + DEBUG(log,out).print("get_z_range: top_is_aq={}, skip_top={}\n", top_is_aq, skip_top); if (!top_is_aq) maxz = get_max_ground_z(out, pos1, pos2) - skip_top; else maxz = get_max_aq_z(out, pos1, pos2) - skip_top; + maxz = std::min((int)pos2.z, maxz); minz = std::max((int)pos1.z, maxz - levels + 1); - DEBUG(log,out).print("calculated z range: minz=%d, maxz=%d\n", minz, maxz); + DEBUG(log,out).print("calculated z range: minz={}, maxz={}\n", minz, maxz); } static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int levels, bool leaky) { - DEBUG(log,out).print("entering aquifer_list: pos1=%d,%d,%d, pos2=%d,%d,%d, levels=%d, leaky=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, levels, leaky); + DEBUG(log,out).print("entering aquifer_list: pos1={}, pos2={}, levels={}, leaky={}\n", + pos1, pos2, levels, leaky); std::map> light_tiles, heavy_tiles; @@ -173,81 +176,109 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int }); if (light_tiles.empty() && heavy_tiles.empty()) { - out.print("No %saquifer tiles in the specified range.\n", leaky ? "leaking " : ""); + out.print("No {}aquifer tiles in the specified range.\n", leaky ? "leaking " : ""); } else { int elev_off = world->map.region_z - 100; for (int z = maxz; z >= minz; --z) { int lcount = light_tiles.contains(z) ? light_tiles[z] : 0; int hcount = heavy_tiles.contains(z) ? heavy_tiles[z] : 0; if (lcount || hcount) - out.print("z-level %3d (elevation %4d) has %6d %slight aquifer tile(s) and %6d %sheavy aquifer tile(s)\n", + out.print("z-level {} (elevation {}) has {} {}light aquifer tile(s) and {} {}heavy aquifer tile(s)\n", z, z+elev_off, lcount, leaky ? "leaking " : "", hcount, leaky ? "leaking " : ""); } } } +// adjust the pos range to honor --skip-top and top-relative --levels, +// which are only meaningful when the target is the whole map (--all). +// for drain/convert the reference level is the topmost existing aquifer; +// for add it is the topmost level that can hold an aquifer tile. +static bool apply_top_relative_z(color_ostream &out, df::coord & pos1, df::coord & pos2, + int skip_top, int levels, bool top_is_aq) +{ + int minz = 0, maxz = -1; + get_z_range(out, minz, maxz, pos1, pos2, levels, top_is_aq, skip_top); + if (maxz < minz) + return false; + pos1.z = minz; + pos2.z = maxz; + return true; +} + static int aquifer_drain(color_ostream &out, string aq_type, - df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) + df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky, bool top_relative) { - DEBUG(log,out).print("entering aquifer_drain: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_drain: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}, top_relative={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky, top_relative); + + if (top_relative && !apply_top_relative_z(out, pos1, pos2, skip_top, levels, true)) { + DEBUG(log, out).print("no aquifer levels in range after skip_top/levels\n"); + return 0; + } const bool all = aq_type == "all"; const bool heavy_state = aq_type == "heavy"; int modified = Maps::removeAreaAquifer(pos1, pos2, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && (all || Maps::isTileHeavyAquifer(pos) == heavy_state) && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("drained aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, all=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, all, modified); + DEBUG(log, out).print("drained aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, all={}, count={}\n", + pos1, pos2, heavy_state, all, modified); return modified; } static int aquifer_convert(color_ostream &out, string aq_type, - df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) + df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky, bool top_relative) { - DEBUG(log,out).print("entering aquifer_convert: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_convert: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}, top_relative={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky, top_relative); + + if (top_relative && !apply_top_relative_z(out, pos1, pos2, skip_top, levels, true)) { + DEBUG(log, out).print("no aquifer levels in range after skip_top/levels\n"); + return 0; + } const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return Maps::isTileAquifer(pos) && Maps::isTileHeavyAquifer(pos) != heavy_state && (!leaky || is_leaky(pos)); }); - DEBUG(log, out).print("converted aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("converted aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } static int aquifer_add(color_ostream &out, string aq_type, - df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) + df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky, bool top_relative) { - DEBUG(log,out).print("entering aquifer_add: aq_type=%s, pos1=%d,%d,%d, pos2=%d,%d,%d," - " skip_top=%d, levels=%d, leaky=%d\n", aq_type.c_str(), - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, skip_top, levels, leaky); + DEBUG(log,out).print("entering aquifer_add: aq_type={}, pos1={}, pos2={}, skip_top={}, levels={}, leaky={}, top_relative={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky, top_relative); + + if (top_relative && !apply_top_relative_z(out, pos1, pos2, skip_top, levels, false)) { + DEBUG(log, out).print("no levels in range after skip_top/levels\n"); + return 0; + } const bool heavy_state = aq_type == "heavy"; int modified = Maps::setAreaAquifer(pos1, pos2, heavy_state, [&](df::coord pos, df::map_block* block) -> bool { - TRACE(log, out).print("examining tile: pos=%d,%d,%d\n", pos.x, pos.y, pos.z); + TRACE(log, out).print("examining tile: pos={}\n", pos); return (leaky || !is_leaky(pos)) && (!Maps::isTileAquifer(pos) || Maps::isTileHeavyAquifer(pos) != heavy_state); }); - DEBUG(log, out).print("added aquifer tiles in area: pos1=%d,%d,%d, pos2=%d,%d,%d, heavy_state=%d, count=%d\n", - pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z, heavy_state, modified); + DEBUG(log, out).print("added aquifer tiles in area: pos1={}, pos2={}, heavy_state={}, count={}\n", + pos1, pos2, heavy_state, modified); return modified; } diff --git a/plugins/army-controller-sanity.cpp b/plugins/army-controller-sanity.cpp new file mode 100644 index 0000000000..1924cfc9e9 --- /dev/null +++ b/plugins/army-controller-sanity.cpp @@ -0,0 +1,135 @@ +#include "Debug.h" +#include "PluginManager.h" + +#include "df/world.h" +#include "df/army_controller.h" +#include "df/army.h" +#include "df/historical_entity.h" +#include "df/unit.h" +#include "df/global_objects.h" + +#include + +DFHACK_PLUGIN("army-controller-sanity"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(army_controller_next_id); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(pause_state); +REQUIRE_GLOBAL(cur_year_tick); + +namespace DFHack { + DBG_DECLARE(army_controller_sanity, log, DebugCategory::LWARNING); +} + +namespace { + bool checkArmyControllerSanity() + { + // army controllers are found: + // in viewscreen_worldst (army_controller (list), last_hover_ac) (not checked) + // in entitst (army_controller (list)) + // in armyst (controller) + // in unitst (army_controller) + // + // master list is in army_controller_handlerst + + static size_t last_ac_vec_size = 0; + static int last_army_controller_next_id = 0; + + if (last_army_controller_next_id == *army_controller_next_id && + last_ac_vec_size == world->army_controllers.all.size()) + return true; + + std::unordered_set ac_set{}; + + for (auto ac : world->army_controllers.all) + { + ac_set.insert(ac); + } + + bool ok = true; + + for (auto ent : world->entities.all) + { + for (auto ac : ent->army_controllers) + { + if (ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller {} found in entity id {}\n", static_cast(ac), ent->id); + ok = false; + } + if (ac_set.count(ac) != 0 && ac->entity_id != ent->id) + { + WARN(log).print("acValidationError: Army controller {} has entity id {} but is linked from entity with id {}\n", ac->id, ac->entity_id, ent->id); + } + } + } + + for (auto ar : world->armies.all) + { + auto ac = ar->controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller {} found in army id {}\n", static_cast(ac), ar->id); + ok = false; + } + else if (ac && ac->id != ar->controller_id) + { + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in army {}\n", static_cast(ac), ar->controller_id, ac->id, ar->id); + ok = false; + } + else if (!ac && ar->controller_id != -1) + { + WARN(log).print("acValidationError: army {} has nonzero controller {} but controller pointer is null\n", ar->id, ar->controller_id); + ok = false; + } + } + + for (auto un : world->units.all) + { + auto ac = un->enemy.army_controller; + if (ac && ac_set.count(ac) == 0) { + WARN(log).print("acValidationError: Bad controller {} found in unit id {}\n", static_cast(ac), un->id); + ok = false; + } + else if (ac && ac->id != un->enemy.army_controller_id) + { + WARN(log).print("acValidationError: controller {} id mismatch ({} != {}) in unit {}\n", static_cast(ac), un->enemy.army_controller_id, ac->id, un->id); + ok = false; + } + else if (!ac && un->enemy.army_controller_id != -1) + { + WARN(log).print("acValidationError: unit {} has has nonzero controller {} but controller pointer is null\n", un->id, un->enemy.army_controller_id); + ok = false; + } + } + + last_army_controller_next_id = *army_controller_next_id; + last_ac_vec_size = world->army_controllers.all.size(); + + INFO(log).print("acValidation: controller count = {}, next id = {}, season tick count = {}\n", + last_ac_vec_size, last_army_controller_next_id, *cur_year_tick); + + return ok; + } +} + +DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, std::vector & commands) +{ + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_enable(DFHack::color_ostream& out, bool enable) +{ + is_enabled = enable; + return DFHack::CR_OK; +} + +DFhackCExport DFHack::command_result plugin_onupdate(DFHack::color_ostream& out) +{ + bool ok = checkArmyControllerSanity(); + if (!ok) { + ERR(log).print("Army controller sanity check failed! Game pause forced.\n"); + *pause_state = true; + is_enabled = false; + } + return DFHack::CR_OK; +} diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 9e93018cc9..8f7bbb16fa 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -82,13 +82,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector fortress_age > 0) autobutcher_cycle(out); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); cleanup_autobutcher(out); return CR_OK; } @@ -128,7 +128,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // load the persisted watchlist @@ -140,7 +140,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -209,8 +209,8 @@ static bool isHighPriority(df::unit *unit) { } static void doMarkForSlaughter(df::unit *unit) { - DEBUG(cycle).print("marking unit %d for slaughter: %s, %s, high priority: %s, age: %.2f\n", - unit->id, Units::getReadableName(unit).c_str(), + DEBUG(cycle).print("marking unit {} for slaughter: {}, {}, high priority: {}, age: {:.2f}\n", + unit->id, Units::getReadableName(unit), Units::isFemale(unit) ? "female" : "male", isHighPriority(unit) ? "yes" : "no", Units::getAge(unit)); @@ -266,7 +266,7 @@ struct WatchedRace { fk_prot(0), fa_prot(0), mk_prot(0), ma_prot(0), fk_units(compareKids), mk_units(compareKids), fa_units(compareAdults), ma_units(compareAdults) { - TRACE(control,out).print("creating new WatchedRace: id=%d, watched=%s, fk=%u, mk=%u, fa=%u, ma=%u\n", + TRACE(control,out).print("creating new WatchedRace: id={}, watched={}, fk={}, mk={}, fa={}, ma={}\n", id, watch ? "true" : "false", fk, mk, fa, ma); } @@ -293,8 +293,8 @@ struct WatchedRace { rconfig.ival(5) = ma; } else { - ERR(control,out).print("could not create persistent key for race: %s", - Units::getRaceNameById(raceId).c_str()); + ERR(control,out).print("could not create persistent key for race: {}", + Units::getRaceNameById(raceId)); } } @@ -375,14 +375,14 @@ static void init_autobutcher(color_ostream &out) { vector watchlist; World::GetPersistentSiteData(&watchlist, WATCHLIST_CONFIG_KEY_PREFIX, true); for (auto & p : watchlist) { - DEBUG(control,out).print("Reading from save: %s\n", p.key().c_str()); + DEBUG(control,out).print("Reading from save: {}\n", p.key()); WatchedRace *w = new WatchedRace(out, p); watched_races.emplace(w->raceId, w); } } static void cleanup_autobutcher(color_ostream &out) { - DEBUG(control,out).print("cleaning %s state\n", plugin_name); + DEBUG(control,out).print("cleaning {} state\n", plugin_name); race_to_id.clear(); for (auto w : watched_races) delete w.second; @@ -396,7 +396,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o static command_result df_autobutcher(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -508,7 +508,7 @@ static void autobutcher_control(color_ostream &out) { static void autobutcher_target(color_ostream &out, const autobutcher_options &opts) { if (opts.races_new) { - DEBUG(control,out).print("setting targets for new races to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for new races to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); config.set_int(CONFIG_DEFAULT_FK, opts.fk); config.set_int(CONFIG_DEFAULT_MK, opts.mk); @@ -517,7 +517,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op } if (opts.races_all) { - DEBUG(control,out).print("setting targets for all races on watchlist to fk=%u, mk=%u, fa=%u, ma=%u\n", + DEBUG(control,out).print("setting targets for all races on watchlist to fk={}, mk={}, fa={}, ma={}\n", opts.fk, opts.mk, opts.fa, opts.ma); for (auto w : watched_races) { w.second->fk = opts.fk; @@ -530,7 +530,7 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } int id = race_to_id[*race]; @@ -559,7 +559,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o for (auto race : opts.races) { if (!race_to_id.count(*race)) { - out.printerr("race not found: '%s'", race->c_str()); + out.printerr("race not found: '{}'", race->c_str()); continue; } ids.emplace(race_to_id[*race]); @@ -576,7 +576,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (!watched_races[id]->isWatched) { - DEBUG(control,out).print("watching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("watching: {}\n", opts.command); watched_races[id]->isWatched = true; } } @@ -590,13 +590,13 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o config.get_int(CONFIG_DEFAULT_MA))); } else if (watched_races[id]->isWatched) { - DEBUG(control,out).print("unwatching: %s\n", opts.command.c_str()); + DEBUG(control,out).print("unwatching: {}\n", opts.command); watched_races[id]->isWatched = false; } } else if (opts.command == "forget") { if (watched_races.count(id)) { - DEBUG(control,out).print("forgetting: %s\n", opts.command.c_str()); + DEBUG(control,out).print("forgetting: {}\n", opts.command); watched_races[id]->RemoveConfig(out); delete watched_races[id]; watched_races.erase(id); @@ -666,9 +666,10 @@ static bool isProtectedUnit(df::unit *unit) { || Units::isHunter(unit) // ignore hunting dogs etc || Units::isMarkedForWarTraining(unit) // ignore units marked for any kind of training || Units::isMarkedForHuntTraining(unit) - // ignore creatures in built cages which are defined as rooms to leave zoos alone + || unit->flags1.bits.chained // ignore chained animals + // ignore creatures in built cages which are members of zones to leave zoos alone // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) || (unit->pregnancy_timer != 0) // do not butcher pregnant animals (which includes brooding female egglayers) || Units::isAvailableForAdoption(unit) || unit->name.has_name @@ -679,7 +680,7 @@ static void autobutcher_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // check if there is anything to watch before walking through units vector if (!config.get_bool(CONFIG_AUTOWATCH)) { @@ -719,8 +720,8 @@ static void autobutcher_cycle(color_ostream &out) { w->UpdateConfig(out); watched_races.emplace(unit->race, w); - INFO(cycle,out).print("New race added to autobutcher watchlist: %s\n", - Units::getRaceNamePluralById(unit->race).c_str()); + INFO(cycle,out).print("New race added to autobutcher watchlist: {}\n", + Units::getRaceNamePluralById(unit->race)); } if (w->isWatched) { @@ -739,8 +740,8 @@ static void autobutcher_cycle(color_ostream &out) { if (slaughter_count) { std::stringstream ss; ss << slaughter_count; - INFO(cycle,out).print("%s marked for slaughter: %s\n", - Units::getRaceNamePluralById(w.first).c_str(), ss.str().c_str()); + INFO(cycle,out).print("{} marked for slaughter: {}\n", + Units::getRaceNamePluralById(w.first), ss.str()); } } } @@ -816,7 +817,7 @@ static bool autowatch_isEnabled() { } static void autowatch_setEnabled(color_ostream &out, bool enable) { - DEBUG(control,out).print("auto-adding to watchlist %s\n", enable ? "started" : "stopped"); + DEBUG(control,out).print("auto-adding to watchlist {}\n", enable ? "started" : "stopped"); config.set_bool(CONFIG_AUTOWATCH, enable); if (config.get_bool(CONFIG_IS_ENABLED)) autobutcher_cycle(out); @@ -842,7 +843,7 @@ static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsign WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); w->UpdateConfig(out); watched_races.emplace(id, w); - INFO(control,out).print("New race added to autobutcher watchlist: %s\n", + INFO(control,out).print("New race added to autobutcher watchlist: {}\n", Units::getRaceNamePluralById(id).c_str()); } diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index 7c2cfc9a5e..f85bbdd756 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -79,7 +79,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { if (watched_burrows_indices.count(id)) return watched_burrows[watched_burrows_indices[id]]; string keyname = BURROW_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for burrow %d\n", id); + DEBUG(control,out).print("creating new persistent key for burrow {}\n", id); watched_burrows.emplace_back(World::GetPersistentSiteData(keyname, true)); size_t idx = watched_burrows.size()-1; watched_burrows_indices.emplace(id, idx); @@ -88,7 +88,7 @@ static PersistentDataItem & ensure_burrow_config(color_ostream &out, int id) { static void remove_burrow_config(color_ostream &out, int id) { if (!watched_burrows_indices.count(id)) return; - DEBUG(control,out).print("removing persistent key for burrow %d\n", id); + DEBUG(control,out).print("removing persistent key for burrow {}\n", id); size_t idx = watched_burrows_indices[id]; World::DeletePersistentData(watched_burrows[idx]); watched_burrows.erase(watched_burrows.begin()+idx); @@ -110,7 +110,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int32_t do_cycle(color_ostream &out, bool force_designate = false); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -123,19 +123,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector frame_counter - cycle_timestamp >= CYCLE_TICKS) { int32_t designated = do_cycle(out); if (0 < designated) - out.print("autochop: designated %d tree(s) for chopping\n", designated); + out.print("autochop: designated {} tree(s) for chopping\n", designated); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -301,7 +301,7 @@ static int32_t estimate_logs(const df::plant *plant) { return 0; MaterialInfo mi; - mi.decode(MaterialInfo::PLANT_BASE, plant->material); + mi.decode(df::builtin_mats::PLANT_1, plant->material); bool is_shroom = mi.plant->flags.is_set(df::plant_raw_flags::TREE_HAS_MUSHROOM_CAP); int32_t trunks = 0, parent_dir = 0; @@ -371,7 +371,7 @@ static int32_t scan_tree(color_ostream & out, df::plant *plant, int32_t *expecte map *designated_tree_counts, map &clearcut_burrows, map &chop_burrows) { - TRACE(cycle,out).print(" scanning tree at %d,%d,%d\n", + TRACE(cycle,out).print(" scanning tree at {},{},{}\n", plant->pos.x, plant->pos.y, plant->pos.z); if (!is_valid_tree(plant)) @@ -518,7 +518,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, *inaccessible_logs = 0; for (auto &item : world->items.other[items_other_id::IN_PLAY]) { - TRACE(cycle,out).print(" scanning log %d\n", item->id); + TRACE(cycle,out).print(" scanning log {}\n", item->id); if (item->flags.whole & bad_flags.whole) continue; @@ -538,7 +538,7 @@ static void scan_logs(color_ostream &out, int32_t *usable_logs, } static int32_t do_cycle(color_ostream &out, bool force_designate) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -580,7 +580,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { // of accessible trees int32_t needed = config.get_int(CONFIG_MAX_LOGS) - (usable_logs + expected_yield); - DEBUG(cycle,out).print("needed logs for this cycle: %d\n", needed); + DEBUG(cycle,out).print("needed logs for this cycle: {}\n", needed); for (auto & entry : designatable_trees_by_size) { if (!Designations::markPlant(entry.second)) continue; @@ -590,7 +590,7 @@ static int32_t do_cycle(color_ostream &out, bool force_designate) { return newly_marked; } } - out.print("autochop: insufficient accessible trees to reach log target! Still need %d logs!\n", + out.print("autochop: insufficient accessible trees to reach log target! Still need {} logs!\n", needed); return newly_marked; } @@ -623,8 +623,8 @@ static const char * get_protect_str(bool protect_brewable, bool protect_edible, static void autochop_printStatus(color_ostream &out) { DEBUG(control,out).print("entering autochop_printStatus\n"); validate_burrow_configs(out); - out.print("autochop is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" keeping log counts between %d and %d\n", + out.print("autochop is {}\n\n", is_enabled ? "enabled" : "disabled"); + out.print(" keeping log counts between {} and {}\n", config.get_int(CONFIG_MIN_LOGS), config.get_int(CONFIG_MAX_LOGS)); if (config.get_bool(CONFIG_WAITING_FOR_MIN)) out.print(" currently waiting for min threshold to be crossed before designating more trees\n"); @@ -643,19 +643,19 @@ static void autochop_printStatus(color_ostream &out) { &designated_trees, &accessible_yield, &tree_counts, &designated_tree_counts); out.print("summary:\n"); - out.print(" accessible logs (usable stock): %d\n", usable_logs); - out.print(" inaccessible logs: %d\n", inaccessible_logs); - out.print(" total visible logs: %d\n", usable_logs + inaccessible_logs); + out.print(" accessible logs (usable stock): {}\n", usable_logs); + out.print(" inaccessible logs: {}\n", inaccessible_logs); + out.print(" total visible logs: {}\n", usable_logs + inaccessible_logs); out.print("\n"); - out.print(" accessible trees: %d\n", accessible_trees); - out.print(" inaccessible trees: %d\n", inaccessible_trees); - out.print(" total visible trees: %d\n", accessible_trees + inaccessible_trees); + out.print(" accessible trees: {}\n", accessible_trees); + out.print(" inaccessible trees: {}\n", inaccessible_trees); + out.print(" total visible trees: {}\n", accessible_trees + inaccessible_trees); out.print("\n"); - out.print(" designated trees: %d\n", designated_trees); - out.print(" expected logs from designated trees: %d\n", expected_yield); - out.print(" expected logs from all accessible trees: %d\n", accessible_yield); + out.print(" designated trees: {}\n", designated_trees); + out.print(" expected logs from designated trees: {}\n", expected_yield); + out.print(" expected logs from all accessible trees: {}\n", accessible_yield); out.print("\n"); - out.print(" total trees harvested: %d\n", plotinfo->trees_removed); + out.print(" total trees harvested: {}\n", plotinfo->trees_removed); out.print("\n"); if (!plotinfo->burrows.list.size()) { @@ -669,11 +669,10 @@ static void autochop_printStatus(color_ostream &out) { for (auto &burrow : plotinfo->burrows.list) { name_width = std::max(name_width, (int)burrow->name.size()); } - name_width = -name_width; // left justify - const char *fmt = "%*s %4s %4s %8s %5s %6s %7s\n"; - out.print(fmt, name_width, "burrow name", " id ", "chop", "clearcut", "trees", "marked", "protect"); - out.print(fmt, name_width, "-----------", "----", "----", "--------", "-----", "------", "-------"); + constexpr auto fmt = "{:<{}} {:4} {:4} {:8} {:5} {:6} {:7}\n"; + out.print(fmt, "burrow name", name_width, " id ", "chop", "clearcut", "trees", "marked", "protect"); + out.print(fmt, "-----------", name_width, "----", "----", "--------", "-----", "------", "-------"); for (auto &burrow : plotinfo->burrows.list) { bool chop = false; @@ -689,17 +688,17 @@ static void autochop_printStatus(color_ostream &out) { protect_edible = c.get_bool(BURROW_CONFIG_PROTECT_EDIBLE); protect_cookable = c.get_bool(BURROW_CONFIG_PROTECT_COOKABLE); } - out.print(fmt, name_width, burrow->name.c_str(), int_to_string(burrow->id).c_str(), + out.print(fmt, Burrows::getName(burrow), name_width, burrow->id, chop ? "[x]" : "[ ]", clearcut ? "[x]" : "[ ]", - int_to_string(tree_counts[burrow->id]).c_str(), - int_to_string(designated_tree_counts[burrow->id]).c_str(), + tree_counts[burrow->id], + designated_tree_counts[burrow->id], get_protect_str(protect_brewable, protect_edible, protect_cookable)); } } static void autochop_designate(color_ostream &out) { DEBUG(control,out).print("entering autochop_designate\n"); - out.print("designated %d tree(s) for chopping\n", do_cycle(out, true)); + out.print("designated {} tree(s) for chopping\n", do_cycle(out, true)); } static void autochop_undesignate(color_ostream &out) { @@ -709,7 +708,7 @@ static void autochop_undesignate(color_ostream &out) { if (is_valid_tree(plant) && Designations::unmarkPlant(plant)) ++count; } - out.print("undesignated %d tree(s)\n", count); + out.print("undesignated {} tree(s)\n", count); } static void autochop_setTargets(color_ostream &out, int32_t max_logs, int32_t min_logs) { diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp index fac721a5c5..d9b718f0ba 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -20,6 +20,7 @@ #include "df/item_helmst.h" #include "df/item_pantsst.h" #include "df/item_shoesst.h" +#include "df/itemdef_handlerst.h" #include "df/itemdef_armorst.h" #include "df/itemdef_glovesst.h" #include "df/itemdef_helmst.h" @@ -64,8 +65,8 @@ enum ConfigValues { struct ClothingRequirement; command_result autoclothing(color_ostream &out, vector ¶meters); static void do_autoclothing(); -static bool validateMaterialCategory(ClothingRequirement *requirement); -static bool setItem(string name, ClothingRequirement *requirement); +static bool validateMaterialCategory(ClothingRequirement& requirement); +static std::optional setItem(string name); static void generate_control(color_ostream &out); static bool isAvailableItem(df::item *item); @@ -87,11 +88,11 @@ struct ClothingRequirement { int16_t needed_per_citizen = 0; map total_needed_per_race; - bool matches(ClothingRequirement *b) { - return b->jobType == this->jobType - && b->itemType == this->itemType - && b->item_subtype == this->item_subtype - && b->material_category.whole == this->material_category.whole; + bool operator==(ClothingRequirement& b) const { + return b.jobType == this->jobType + && b.itemType == this->itemType + && b.item_subtype == this->item_subtype + && b.material_category.whole == this->material_category.whole; } string Serialize() { @@ -127,19 +128,41 @@ struct ClothingRequirement { stream >> needed_per_citizen; } - bool SetFromParameters(color_ostream &out, vector ¶meters) + //FIXME: when C++23, use std::expected here + static std::optional createFromParameters(color_ostream &out, vector ¶meters) { - if (!set_bitfield_field(&material_category, parameters[0], 1)) - out << "Unrecognized material type: " << parameters[0] << endl; - if (!setItem(parameters[1], this)) { - out << "Unrecognized item name or token: " << parameters[1] << endl; - return false; + df::job_material_category material_category; + if (parameters.size() < 1) return std::nullopt; + + size_t idx = 0; + if (parameters[0] == "clear") idx++; + + if (parameters.size() < idx + 1) return std::nullopt; + + if (!set_bitfield_field(&material_category, parameters[idx], 1)) + { + out << "Unrecognized material type: " << parameters[idx] << endl; + return std::nullopt; } - else if (!validateMaterialCategory(this)) { - out << parameters[0] << " is not a valid material category for " << parameters[1] << endl; - return false; + + auto req = setItem(parameters[idx + 1]); + + if (!req) + { + out << "Unrecognized item name or token: " << parameters[idx+1] << endl; + return std::nullopt; + } + + req->material_category = material_category; + + if (!validateMaterialCategory(*req)) { + out << parameters[idx] << " is not a valid material category for " << parameters[idx+1] << endl; + return std::nullopt; + } + else + { + return req; } - return true; } string ToReadableLabel() { @@ -224,7 +247,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { } is_enabled = enabled.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); // Parse constraints @@ -269,21 +292,21 @@ DFhackCExport command_result plugin_save_site_data(color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { auto enabled = World::GetPersistentSiteData(CONFIG_KEY); is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); enabled.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_autoclothing(); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -298,94 +321,98 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } -static bool setItemFromName(string name, ClothingRequirement *requirement) +std::optional setItemFromName(string name) { -#define SEARCH_ITEM_RAWS(rawType, job, item) \ -for (auto &itemdef : world->raws.itemdefs.rawType) { \ - string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; \ - if (fullName == name) { \ - requirement->jobType = job_type::job; \ - requirement->itemType = item_type::item; \ - requirement->item_subtype = itemdef->subtype; \ - return true; \ - } \ -} - SEARCH_ITEM_RAWS(armor, MakeArmor, ARMOR); - SEARCH_ITEM_RAWS(gloves, MakeGloves, GLOVES); - SEARCH_ITEM_RAWS(shoes, MakeShoes, SHOES); - SEARCH_ITEM_RAWS(helms, MakeHelm, HELM); - SEARCH_ITEM_RAWS(pants, MakePants, PANTS); - return false; + auto SEARCH_ITEM_RAWS = [&name](FT df::itemdef_handlerst:: * rawType, df::job_type job, df::item_type item) -> std::optional + { + auto& itemdefs = world->raws.itemdefs.*rawType; + auto it = std::find_if(itemdefs.begin(), itemdefs.end(), [&name] (auto& itemdef) { + string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; + return fullName == name; + }); + if (it != itemdefs.end()) + { + auto& itemdef = *it; + return ClothingRequirement{.jobType = job, .itemType = item, .item_subtype = itemdef->subtype}; + } + return std::nullopt; + }; + + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::armor, df::job_type::MakeArmor, df::item_type::ARMOR)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::gloves, df::job_type::MakeGloves, df::item_type::GLOVES)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::shoes, df::job_type::MakeShoes, df::item_type::SHOES)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::helms, df::job_type::MakeHelm, df::item_type::HELM)) + return v; + if (auto v = SEARCH_ITEM_RAWS(&df::itemdef_handlerst::pants, df::job_type::MakePants, df::item_type::PANTS)) + return v; + return std::nullopt; } -static bool setItemFromToken(string token, ClothingRequirement *requirement) { +static std::optional setItemFromToken(string token) { ItemTypeInfo itemInfo; if (!itemInfo.find(token)) - return false; + return std::nullopt; switch (itemInfo.type) { case item_type::ARMOR: - requirement->jobType = job_type::MakeArmor; - break; + return ClothingRequirement{.jobType = job_type::MakeArmor, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::GLOVES: - requirement->jobType = job_type::MakeGloves; - break; + return ClothingRequirement{.jobType = job_type::MakeGloves, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::SHOES: - requirement->jobType = job_type::MakeShoes; - break; + return ClothingRequirement{.jobType = job_type::MakeShoes, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::HELM: - requirement->jobType = job_type::MakeHelm; - break; + return ClothingRequirement{.jobType = job_type::MakeHelm, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; case item_type::PANTS: - requirement->jobType = job_type::MakePants; - break; + return ClothingRequirement{.jobType = job_type::MakePants, .itemType = itemInfo.type, .item_subtype = itemInfo.subtype}; default: - return false; + return std::nullopt; } - requirement->itemType = itemInfo.type; - requirement->item_subtype = itemInfo.subtype; - return true; } -static bool setItem(string name, ClothingRequirement *requirement) { - return setItemFromName(name, requirement) || setItemFromToken(name, requirement); +static std::optional setItem(string name) +{ + if (auto v = setItemFromName(name)) return v; + return setItemFromToken(name); } -static bool armorFlagsMatch(BitArray *flags, df::job_material_category *category) { - if (flags->is_set(df::armor_general_flags::SOFT) && - (category->bits.cloth || category->bits.yarn || category->bits.silk) +static bool armorFlagsMatch(BitArray& flags, df::job_material_category& category) { + if (flags.is_set(df::armor_general_flags::SOFT) && + (category.bits.cloth || category.bits.yarn || category.bits.silk) ) return true; - else if (flags->is_set(df::armor_general_flags::BARRED) && category->bits.bone) + else if (flags.is_set(df::armor_general_flags::BARRED) && category.bits.bone) return true; - else if (flags->is_set(df::armor_general_flags::SCALED) && category->bits.shell) + else if (flags.is_set(df::armor_general_flags::SCALED) && category.bits.shell) return true; - return flags->is_set(df::armor_general_flags::LEATHER) && category->bits.leather; + return flags.is_set(df::armor_general_flags::LEATHER) && category.bits.leather; } -static bool validateMaterialCategory(ClothingRequirement *requirement) { - auto itemDef = Items::getSubtypeDef(requirement->itemType, requirement->item_subtype); - switch (requirement->itemType) +static bool validateMaterialCategory(ClothingRequirement& requirement) { + auto itemDef = Items::getSubtypeDef(requirement.itemType, requirement.item_subtype); + switch (requirement.itemType) { case item_type::ARMOR: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_armorst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::GLOVES: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_glovesst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::SHOES: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_shoesst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::HELM: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_helmst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; case item_type::PANTS: if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_pantsst, itemDef)) - return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + return armorFlagsMatch(armor->props.flags, requirement.material_category); break; default: break; @@ -397,7 +424,7 @@ static bool validateMaterialCategory(ClothingRequirement *requirement) { command_result autoclothing(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -435,42 +462,56 @@ command_result autoclothing(color_ostream &out, vector ¶meters) } // Create a new requirement from the available parameters. - ClothingRequirement newRequirement; - if (!newRequirement.SetFromParameters(out, parameters)) + auto newRequirementOpt = ClothingRequirement::createFromParameters(out, parameters); + if (!newRequirementOpt) return CR_WRONG_USAGE; + + auto& newRequirement = *newRequirementOpt; + // All checks are passed. Now we either show or set the amount. bool settingSize = false; - bool matchedExisting = false; + if (parameters.size() > 2) { - try { - newRequirement.needed_per_citizen = std::stoi(parameters[2]); + if (parameters[0] == "clear") { + newRequirement.needed_per_citizen = 0; + settingSize = true; } - catch (const std::exception&) { - out << parameters[2] << " is not a valid number." << endl; - return CR_WRONG_USAGE; + else { + try { + newRequirement.needed_per_citizen = std::stoi(parameters[2]); + } + catch (const std::exception&) { + out << parameters[2] << " is not a valid number." << endl; + return CR_WRONG_USAGE; + } + settingSize = true; } - settingSize = true; } - for (size_t i = clothingOrders.size(); i-- > 0;) { - if (!clothingOrders[i].matches(&newRequirement)) - continue; - matchedExisting = true; - if (settingSize) { - if (newRequirement.needed_per_citizen == 0) { - clothingOrders.erase(clothingOrders.begin() + i); - out << "Unset " << parameters[0] << " " << parameters[1] << endl; + auto it = std::find(clothingOrders.begin(), clothingOrders.end(), newRequirement); + if (it != clothingOrders.end()) + { + if (settingSize) + { + if (newRequirement.needed_per_citizen == 0) + { + clothingOrders.erase(it); + if (parameters[0] == "clear") + out << "Unset " << parameters[1] << " " << parameters[2] << endl; + else + out << "Unset " << parameters[0] << " " << parameters[1] << endl; } - else { - clothingOrders[i] = newRequirement; + else + { + *it = newRequirement; out << "Set " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl; } } else - out << parameters[0] << " " << parameters[1] << " is set to " << clothingOrders[i].needed_per_citizen << endl; - break; + out << parameters[0] << " " << parameters[1] << " is set to " << it->needed_per_citizen << endl; } - if (!matchedExisting) { + else + { if (settingSize) { if (newRequirement.needed_per_citizen == 0) out << parameters[0] << " " << parameters[1] << " already unset." << endl; @@ -505,7 +546,7 @@ static void find_needed_clothing_items() { { auto item = Items::findItemByID(ownedItem); if (!item) { - DEBUG(cycle).print("autoclothing: Invalid inventory item ID: %d\n", ownedItem); + DEBUG(cycle).print("autoclothing: Invalid inventory item ID: {}\n", ownedItem); continue; } @@ -591,6 +632,7 @@ static void add_clothing_orders() { newOrder->material_category = clothingOrder.material_category; newOrder->amount_left = amount; newOrder->amount_total = amount; + newOrder->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(newOrder); } } @@ -661,7 +703,7 @@ static void generate_control(color_ostream &out) { { auto item = Items::findItemByID(itemId); if (!item) { - DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: %d\n", itemId); + DEBUG(cycle, out).print("autoclothing: Invalid inventory item ID: {}\n", itemId); continue; } else if (item->getWear() >= 1) @@ -764,7 +806,7 @@ static void generate_control(color_ostream &out) { } map availableGloves; - for (auto glove : world->items.other.HELM) { + for (auto glove : world->items.other.GLOVES) { if (!isAvailableItem(glove)) continue; availableGloves[glove->maker_race]++; @@ -775,7 +817,7 @@ static void generate_control(color_ostream &out) { } map availablePants; - for (auto pants : world->items.other.HELM) { + for (auto pants : world->items.other.PANTS) { if (!isAvailableItem(pants)) continue; availablePants[pants->maker_race]++; diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index fbf3ff8a82..994b86b02d 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -72,9 +72,43 @@ static constexpr uint32_t proj_flags = ( df::projectile_flags::mask_no_collide ); +// Original flags of items marked for destruction, so the marking can be +// reverted while the game is still paused (once a frame passes, the garbage +// collector may have already deleted the items) +static map pending_destroy; +static int last_frame = 0; + +static void reset_pending_destroy() +{ + if (world->frame_counter != last_frame) + { + last_frame = world->frame_counter; + pending_destroy.clear(); + } +} + +// mark an item for destruction, along with anything it contains, so that +// contained items are not left pointing at a deleted container +static void mark_for_destroy(df::item *itm) +{ + if (!pending_destroy.contains(itm->id)) + pending_destroy[itm->id] = itm->flags; + + itm->flags.bits.garbage_collect = true; + // Cosmetic changes: make them disappear from view instantly. + itm->flags.bits.forbid = true; + itm->flags.bits.hidden = true; + + vector contained_items; + Items::getContainedItems(itm, &contained_items); + for (auto child : contained_items) + mark_for_destroy(child); +} + static command_result autodump_main(color_ostream &out, vector ¶meters) { // Command line options. bool destroy = false; + bool undestroy = false; bool here = false; bool need_visible = false; bool need_hidden = false; @@ -86,6 +120,8 @@ static command_result autodump_main(color_ostream &out, vector ¶mete destroy = true; else if (p == "destroy-here") destroy = here = true; + else if (p == "undestroy") + undestroy = true; else if (p == "visible") need_visible = true; else if (p == "hidden") @@ -105,6 +141,29 @@ static command_result autodump_main(color_ostream &out, vector ¶mete return CR_FAILURE; } + reset_pending_destroy(); + + if (undestroy) + { // Revert items marked for destruction while the game is still paused. + int restored_total = 0; + for (auto &entry : pending_destroy) + { + // Items already garbage collected or deleted by another plugin + // are no longer in items.all, so find() returns NULL for them. + df::item *itm = df::item::find(entry.first); + if (!itm) + continue; + itm->flags.bits.garbage_collect = entry.second.bits.garbage_collect; + itm->flags.bits.hidden = entry.second.bits.hidden; + itm->flags.bits.dump = entry.second.bits.dump; + itm->flags.bits.forbid = entry.second.bits.forbid; + restored_total++; + } + pending_destroy.clear(); + out.print("Done. {} items unmarked for destruction.\n", restored_total); + return CR_OK; + } + int dumped_total = 0; bool make_projectile = false; @@ -170,21 +229,29 @@ static command_result autodump_main(color_ostream &out, vector ¶mete } } else - out.print("Could not move item: %s\n", Items::getDescription(itm, 0, true).c_str()); + out.print("Could not move item: {}\n", Items::getDescription(itm, 0, true)); } } else { // Destroy if (here && itm->pos != pos_cursor) continue; - itm->flags.bits.garbage_collect = true; - // Cosmetic changes: make them disappear from view instantly. - itm->flags.bits.forbid = true; - itm->flags.bits.hidden = true; + // Don't destroy items in unit inventories (including items inside + // carried containers); deleting them leaves unit equipment refs + // dangling. Matches the check in autodump-destroy-item. + bool held_by_unit = itm->flags.bits.in_inventory; + for (auto ref : itm->general_refs) + { + if (ref->getType() == general_ref_type::UNIT_HOLDER) + held_by_unit = true; + } + if (held_by_unit) + continue; + mark_for_destroy(itm); } dumped_total++; } - out.print("Done. %d items %s.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); + out.print("Done. {} items {}.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); return CR_OK; } @@ -202,9 +269,6 @@ command_result df_autodump_destroy_here(color_ostream &out, vector ¶ return autodump_main(out, args); } -static map pending_destroy; -static int last_frame = 0; - command_result df_autodump_destroy_item(color_ostream &out, vector ¶meters) { if (!parameters.empty()) @@ -215,18 +279,14 @@ command_result df_autodump_destroy_item(color_ostream &out, vector ¶ return CR_FAILURE; // Allow undoing the destroy. - if (world->frame_counter != last_frame) - { - last_frame = world->frame_counter; - pending_destroy.clear(); - } + reset_pending_destroy(); - if (pending_destroy.count(item->id)) + if (pending_destroy.contains(item->id)) { df::item_flags old_flags = pending_destroy[item->id]; pending_destroy.erase(item->id); - item->flags.bits.garbage_collect = false; + item->flags.bits.garbage_collect = old_flags.bits.garbage_collect; item->flags.bits.hidden = old_flags.bits.hidden; item->flags.bits.dump = old_flags.bits.dump; item->flags.bits.forbid = old_flags.bits.forbid; diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 571cd13608..2881bfcc96 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -1,8 +1,12 @@ #include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" +#include "PluginLua.h" +#include "modules/Buildings.h" #include "modules/Items.h" #include "modules/Maps.h" +#include "modules/Materials.h" #include "modules/World.h" #include "df/biome_type.h" @@ -16,13 +20,17 @@ #include "df/item_plant_growthst.h" #include "df/item_seedsst.h" #include "df/items_other_id.h" +#include "df/material.h" +#include "df/material_flags.h" +#include "df/plant_growth.h" #include "df/plant_raw.h" #include "df/plant_raw_flags.h" #include "df/plotinfost.h" #include "df/unit.h" #include "df/world.h" -#include +#include +#include using namespace DFHack; using namespace df::enums; @@ -50,7 +58,15 @@ class AutoFarm { std::map thresholds; int defaultThreshold = 50; + // thresholds for specific growth products of a plant, keyed by + // (plant index, growth id). growth items whose growth has no explicit + // threshold count toward the plant's pooled threshold instead + std::map, int> growth_thresholds; + std::map lastCounts; + // stockpile growth item counts, keyed by (plant index, growth id). + // populated for reporting; untracked growths also count toward lastCounts + std::map, int> lastGrowthCounts; public: void initialize() @@ -58,7 +74,10 @@ class AutoFarm { thresholds.clear(); defaultThreshold = 50; + growth_thresholds.clear(); + lastCounts.clear(); + lastGrowthCounts.clear(); } void setThreshold(int id, int val) @@ -71,6 +90,46 @@ class AutoFarm { return (thresholds.count(id) > 0) ? thresholds[id] : defaultThreshold; } + void removeThreshold(int id) + { + thresholds.erase(id); + } + + void setGrowthThreshold(int plant_id, const std::string& growth_id, int val) + { + growth_thresholds[std::make_pair(plant_id, growth_id)] = val; + } + + void removeGrowthThreshold(int plant_id, const std::string& growth_id) + { + growth_thresholds.erase(std::make_pair(plant_id, growth_id)); + } + + const std::map, int>& getGrowthThresholds() + { + return growth_thresholds; + } + + const std::map, int>& getLastGrowthCounts() + { + return lastGrowthCounts; + } + + int getDefault() + { + return defaultThreshold; + } + + const std::map& getThresholds() + { + return thresholds; + } + + const std::map& getLastCounts() + { + return lastCounts; + } + void setDefault(int val) { defaultThreshold = val; @@ -193,66 +252,216 @@ class AutoFarm { return raw ? raw->name : "NONE"; } + // whether the growth's material produces stockpilable growth items; + // only these are ever produced by processing jobs and gathering, so + // they are the only growths that can be meaningfully tracked + static bool is_stockpile_growth(df::plant_growth* growth) + { + MaterialInfo mat(growth->mat_type, growth->mat_index); + return mat.isValid() && + mat.material->flags.is_set(df::material_flags::STOCKPILE_PLANT_GROWTH); + } + + // the id of the stockpile growth at the given index, or "" if the + // growth doesn't exist or isn't stockpilable + static std::string stockpile_growth_id(df::plant_raw* plant, int32_t subtype) + { + if (subtype >= 0 && (size_t)subtype < plant->growths.size() && + is_stockpile_growth(plant->growths[subtype])) + return plant->growths[subtype]->id; + return ""; + } + + static bool has_stockpile_growth(df::plant_raw* plant, const std::string& growth_id) + { + for (auto g : plant->growths) + if (g->id == growth_id && is_stockpile_growth(g)) + return true; + return false; + } + + // gathers the set of biomes covered by the farm's tiles, since a farm may + // span multiple biomes and DF allows planting any crop that can grow in + // any of them (in the tiles of the respective biome) + std::set get_farm_biomes(df::building_farmplotst* farm) + { + std::set biomes; + for (int32_t x = farm->x1; x <= farm->x2; x++) + { + for (int32_t y = farm->y1; y <= farm->y2; y++) + { + if (!Buildings::containsTile(farm, df::coord2d(x, y))) + continue; + auto des = Maps::getTileDesignation(x, y, farm->z); + if (!des) + continue; + if (des->bits.subterranean) + { + // any subterranean tile can grow any crop that grows + // underground + biomes.insert(df::biome_type::SUBTERRANEAN_WATER); + biomes.insert(df::biome_type::SUBTERRANEAN_CHASM); + biomes.insert(df::biome_type::SUBTERRANEAN_LAVA); + } + else if (Maps::getRegionBiome(Maps::getTileBiomeRgn(df::coord(x, y, farm->z)))) + { + df::coord2d region(Maps::getTileBiomeRgn(df::coord(x, y, farm->z))); + biomes.insert(Maps::getBiomeType(region.x, region.y)); + } + } + } + return biomes; + } + +public: + bool is_farm_excluded(int32_t farm_id) + { + auto item = World::GetPersistentSiteData("autofarm/excluded_farm/" + std::to_string(farm_id)); + return item.isValid() && item.ival(0) != 0; + } + + void set_farm_excluded(int32_t farm_id, bool excluded) + { + const std::string key = "autofarm/excluded_farm/" + std::to_string(farm_id); + auto item = World::GetPersistentSiteData(key); + if (excluded) + { + if (!item.isValid()) + item = World::AddPersistentSiteData(key); + if (item.isValid()) + item.ival(0) = 1; + } + else if (item.isValid()) + { + World::DeletePersistentData(item); + } + } + + std::vector get_excluded_farms() + { + std::vector items; + std::vector ret; + World::GetPersistentSiteData(&items, "autofarm/excluded_farm/", true); + for (auto& i : items) + { + if (!i.isValid() || i.ival(0) == 0) + continue; + int32_t farm_id = atoi(i.key().substr(strlen("autofarm/excluded_farm/")).c_str()); + auto bld = df::building::find(farm_id); + // ignore entries left behind by destroyed farm plots + if (bld && bld->getType() == df::building_type::FarmPlot) + ret.push_back(farm_id); + } + return ret; + } + +private: void set_farm(color_ostream& out, int new_plant_id, df::building_farmplotst* farm, int season) { int old_plant_id = farm->plant_id[season]; if (old_plant_id != new_plant_id) { farm->plant_id[season] = new_plant_id; - INFO(cycle, out).print("autofarm: changing farm #%d from %s to %s\n", farm->id, - get_plant_name(old_plant_id).c_str(), - get_plant_name(new_plant_id).c_str()); + INFO(cycle, out).print("autofarm: changing farm #{} from {} to {}\n", farm->id, + get_plant_name(old_plant_id), + get_plant_name(new_plant_id)); } } - void set_farms(color_ostream& out, const std::set& plants, const std::vector& farms) +public: + void set_farms(color_ostream& out, const std::map>& plants, + const std::vector>>& farms) { - // this algorithm attempts to change as few farms as possible, while ensuring that - // the number of farms planting each eligible plant is "as equal as possible" + // this algorithm attempts to change as few farms as possible, while + // ensuring that the number of farms planting each plant is "as equal + // as possible". unlike previously, each farm may have a different set + // of plants that it can grow, since a farm can span multiple biomes int season = *df::global::cur_season; - if (farms.empty() || plants.empty()) + // the plants that each farm is eligible to grow, and the list of + // farms assigned to each plant + std::map> eligible; + std::map> assigned; + std::map counters; + std::vector unassigned; + + for (auto& [farm, biomes] : farms) { - // if no more plants were requested, fallow all farms - // if there were no farms, do nothing - for (auto farm : farms) + for (auto biome : biomes) { - set_farm(out, -1, farm, season); + auto it = plants.find(biome); + if (it != plants.end()) + eligible[farm].insert(it->second.begin(), it->second.end()); } - return; + for (auto plant : eligible[farm]) + assigned[plant]; // ensure every eligible plant can receive farms } - int min = farms.size() / plants.size(); // the number of farms that should plant each eligible plant, rounded down - int extra = farms.size() - min * plants.size(); // the remainder that cannot be evenly divided + // keep the current assignment for farms whose crop is still eligible + for (auto& [farm, biomes] : farms) + { + int cur = farm->plant_id[season]; + if (cur >= 0 && eligible[farm].count(cur)) + { + assigned[cur].push_back(farm); + counters[cur]++; + } + else + { + unassigned.push_back(farm); + } + } - std::map counters; - std::queue toChange; + // assign farms with the fewest options first so that constrained + // farms don't get crowded out by more flexible ones + std::sort(unassigned.begin(), unassigned.end(), + [&](df::building_farmplotst* a, df::building_farmplotst* b) + { + return eligible[a].size() < eligible[b].size(); + }); - for (auto farm : farms) + for (auto farm : unassigned) { - int o = farm->plant_id[season]; - if (plants.count(o) == 0 || counters[o] > min || (counters[o] == min && extra == 0)) - toChange.push(farm); // this farm is an excess instance for the plant it is currently planting - else + if (eligible[farm].empty()) { - if (counters[o] == min) - extra--; // allocate off one of the remainder farms - counters[o]++; + set_farm(out, -1, farm, season); + continue; } + int best = *std::min_element(eligible[farm].begin(), eligible[farm].end(), + [&](int a, int b) { return counters[a] < counters[b]; }); + set_farm(out, best, farm, season); + assigned[best].push_back(farm); + counters[best]++; } - for (auto n : plants) + // rebalance: move farms from overrepresented plants to + // underrepresented ones while their counts differ by more than one. + // each move strictly decreases the variance, so this terminates + for (bool moved = true; moved;) { - int c = counters[n]; - while (toChange.size() > 0 && (c < min || (c == min && extra > 0))) + moved = false; + for (auto& [src, src_farms] : assigned) { - // pick one of the excess farms and change it to plant this plant - df::building_farmplotst* farm = toChange.front(); - set_farm(out, n, farm, season); - toChange.pop(); - if (c++ == min) - extra--; + for (auto& [dst, dst_farms] : assigned) + { + if (counters[src] <= counters[dst] + 1) + continue; + auto it = std::find_if(src_farms.begin(), src_farms.end(), + [&](df::building_farmplotst* f) { return eligible[f].count(dst); }); + if (it == src_farms.end()) + continue; + auto farm = *it; + set_farm(out, dst, farm, season); + src_farms.erase(it); + dst_farms.push_back(farm); + counters[src]--; + counters[dst]++; + moved = true; + break; + } + if (moved) + break; } } } @@ -276,7 +485,9 @@ class AutoFarm { // have to scan both items[PLANT] and items[PLANT_GROWTH] because agricultural products can be either - auto count = [&, this](df::item* i) { + lastGrowthCounts.clear(); + + auto count_plant = [&, this](df::item* i) { auto mat = i->getMaterialIndex(); if ((i->flags.whole & bad_flags) == 0 && plantable_plants.count(mat) > 0) @@ -285,45 +496,73 @@ class AutoFarm { } }; + auto count_growth = [&, this](df::item* i) { + auto g = virtual_cast(i); + auto mat = i->getMaterialIndex(); + if (!g || (i->flags.whole & bad_flags) != 0 || + plantable_plants.count(mat) == 0) + return; + df::plant_raw* plant = world->raws.plants.all[mat]; + const std::string gid = stockpile_growth_id(plant, g->subtype); + const auto key = std::make_pair(mat, gid); + if (!gid.empty() && growth_thresholds.count(key)) + { + // tracked growths count only toward their own threshold + lastGrowthCounts[key] += i->getStackSize(); + } + else + { + if (!gid.empty()) + // unconfigured stockpile growths are tracked + // separately for reporting, but still count toward + // the plant's pooled threshold + lastGrowthCounts[key] += i->getStackSize(); + lastCounts[mat] += i->getStackSize(); + } + }; + for (auto i : world->items.other[df::items_other_id::PLANT]) - count(i); + count_plant(i); for (auto i : world->items.other[df::items_other_id::PLANT_GROWTH]) - count(i); + count_growth(i); std::map> plants; for (auto& plantable : plantable_plants) { df::plant_raw* plant = world->raws.plants.all[plantable.first]; - if (lastCounts[plant->index] < getThreshold(plant->index)) + // a plant is in demand when its pooled stock is under threshold + // or any explicitly tracked growth of it is under threshold + bool under = lastCounts[plant->index] < getThreshold(plant->index); + if (!under) + for (auto& gt : growth_thresholds) + { + if (gt.first.first == plant->index && + lastGrowthCounts[gt.first] < gt.second) + { + under = true; + break; + } + } + if (under) for (auto biome : plantable.second) { plants[biome].insert(plant->index); } } - std::map> farms; + std::vector>> farms; for (auto& bb : world->buildings.other[df::buildings_other_id::FARM_PLOT]) { auto farm = virtual_cast(bb); - if (farm->flags.bits.exists) + if (farm->flags.bits.exists && !is_farm_excluded(farm->id)) { - df::biome_type biome; - if (Maps::getTileDesignation(bb->centerx, bb->centery, bb->z)->bits.subterranean) - biome = biome_type::SUBTERRANEAN_WATER; - else { - df::coord2d region(Maps::getTileBiomeRgn(df::coord(bb->centerx, bb->centery, bb->z))); - biome = Maps::getBiomeType(region.x, region.y); - } - farms[biome].push_back(farm); + farms.emplace_back(farm, get_farm_biomes(farm)); } } - for (auto& ff : farms) - { - set_farms(out, plants[ff.first], ff.second); - } + set_farms(out, plants, farms); out << std::flush; } @@ -345,6 +584,14 @@ class AutoFarm { auto plant = world->raws.plants.all[th.first]; out << plant->id << " limit " << getThreshold(th.first) << " current 0" << '\n'; } + + for (auto& gt : growth_thresholds) + { + auto plant = world->raws.plants.all[gt.first.first]; + out << plant->id << ':' << gt.first.second << " limit " << gt.second + << " current " << lastGrowthCounts[gt.first] << '\n'; + } + out << "Default: " << defaultThreshold << '\n'; out << std::flush; @@ -385,15 +632,32 @@ class AutoFarm { const auto allPlants = world->raws.plants.all; const std::string id = i.val(); const int val = i.ival(0); - const auto plant = std::find_if(std::begin(allPlants), std::end(allPlants), [id](df::plant_raw* p) { return p->id == id; }); - if (plant != std::end(allPlants)) + const auto colon = id.find(':'); + const std::string plant_id = id.substr(0, colon); + const auto plant = std::find_if(std::begin(allPlants), std::end(allPlants), [plant_id](df::plant_raw* p) { return p->id == plant_id; }); + if (plant == std::end(allPlants)) + { + WARN(control, out).print("threshold for unknown plant {} in saved configuration ignored\n", id); + } + else if (colon == std::string::npos) { setThreshold((*plant)->index, val); - INFO(control, out).print("threshold of %d for plant %s in saved configuration loaded\n", val, id.c_str()); + INFO(control, out).print("threshold of {} for plant {} in saved configuration loaded\n", val, id); } else { - WARN(control, out).print("threshold for unknown plant %s in saved configuration ignored\n", id.c_str()); + // "PLANT:GROWTH" entries hold thresholds for specific + // stockpile growth products of the plant + const std::string growth_id = id.substr(colon + 1); + if (has_stockpile_growth(*plant, growth_id)) + { + setGrowthThreshold((*plant)->index, growth_id, val); + INFO(control, out).print("threshold of {} for growth {} in saved configuration loaded\n", val, id); + } + else + { + WARN(control, out).print("threshold for unknown growth {} in saved configuration ignored\n", id); + } } } } @@ -418,6 +682,15 @@ class AutoFarm { cfgThreshold.val() = plantID; cfgThreshold.ival(0) = t.second; } + + for (const auto& t : growth_thresholds) + { + const std::string productID = world->raws.plants.all[t.first.first]->id + ":" + t.first.second; + const std::string keyName = "autofarm/threshold/" + productID; + PersistentDataItem cfgThreshold = World::AddPersistentSiteData(keyName); + cfgThreshold.val() = productID; + cfgThreshold.ival(0) = t.second; + } } }; @@ -453,7 +726,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream& out) DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -489,19 +762,29 @@ static command_result setThresholds(color_ostream& out, std::vector std::string id = parameters[i]; std::transform(id.begin(), id.end(), id.begin(), ::toupper); + // a threshold may apply to a plant ("PLANT") or to a specific + // stockpile growth product of a plant ("PLANT:GROWTH") + const auto colon = id.find(':'); + const std::string plant_id = id.substr(0, colon); + const std::string growth_id = colon == std::string::npos ? "" : id.substr(colon + 1); + bool ok = false; for (auto plant : world->raws.plants.all) { - if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) - { + if (!plant->flags.is_set(df::plant_raw_flags::SEED) || (plant->id != plant_id)) + continue; + if (growth_id.empty()) autofarmInstance->setThreshold(plant->index, val); - ok = true; + else if (AutoFarm::has_stockpile_growth(plant, growth_id)) + autofarmInstance->setGrowthThreshold(plant->index, growth_id, val); + else break; - } + ok = true; + break; } if (!ok) { - out << "Cannot find plant with id " << id << '\n' << std::flush; + out << "Cannot find plant or growth with id " << id << '\n' << std::flush; return CR_WRONG_USAGE; } } @@ -512,7 +795,7 @@ static command_result setThresholds(color_ostream& out, std::vector static command_result autofarm(color_ostream& out, std::vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -544,3 +827,101 @@ static command_result autofarm(color_ostream& out, std::vector& par return CR_OK; } + +static void autofarm_setDefaultThreshold(color_ostream& out, int32_t val) +{ + autofarmInstance->setDefault(val); + autofarmInstance->save_state(out); +} + +static void autofarm_setThreshold(color_ostream& out, int32_t plant_index, int32_t val) +{ + autofarmInstance->setThreshold(plant_index, val); + autofarmInstance->save_state(out); +} + +static void autofarm_removeThreshold(color_ostream& out, int32_t plant_index) +{ + autofarmInstance->removeThreshold(plant_index); + autofarmInstance->save_state(out); +} + +static void autofarm_setGrowthThreshold(color_ostream& out, int32_t plant_index, std::string growth_id, int32_t val) +{ + autofarmInstance->setGrowthThreshold(plant_index, growth_id, val); + autofarmInstance->save_state(out); +} + +static void autofarm_removeGrowthThreshold(color_ostream& out, int32_t plant_index, std::string growth_id) +{ + autofarmInstance->removeGrowthThreshold(plant_index, growth_id); + autofarmInstance->save_state(out); +} + +static bool autofarm_isFarmExcluded(int32_t farm_id) +{ + return autofarmInstance->is_farm_excluded(farm_id); +} + +static void autofarm_setFarmExcluded(color_ostream& out, int32_t farm_id, bool excluded) +{ + autofarmInstance->set_farm_excluded(farm_id, excluded); +} + +// pushes a nested table of the form {plant_index = {growth_id = count}} for a +// map keyed by (plant_index, growth_id) +static void push_growth_map(lua_State* L, const std::map, int>& m) +{ + lua_newtable(L); + for (auto& [key, val] : m) + { + lua_pushinteger(L, key.first); + lua_gettable(L, -2); + if (lua_isnil(L, -1)) + { + lua_pop(L, 1); + lua_newtable(L); + lua_pushinteger(L, key.first); + lua_pushvalue(L, -2); + lua_settable(L, -4); + } + Lua::Push(L, key.second); + lua_pushinteger(L, val); + lua_settable(L, -3); + lua_pop(L, 1); + } +} + +static int autofarm_getData(lua_State* L) +{ + lua_newtable(L); + int ctable = lua_gettop(L); + Lua::SetField(L, autofarmInstance->getDefault(), ctable, "default_threshold"); + Lua::Push(L, autofarmInstance->getThresholds()); + lua_setfield(L, ctable, "thresholds"); + push_growth_map(L, autofarmInstance->getGrowthThresholds()); + lua_setfield(L, ctable, "growth_thresholds"); + Lua::Push(L, autofarmInstance->getLastCounts()); + lua_setfield(L, ctable, "last_counts"); + push_growth_map(L, autofarmInstance->getLastGrowthCounts()); + lua_setfield(L, ctable, "growth_counts"); + Lua::Push(L, autofarmInstance->get_excluded_farms()); + lua_setfield(L, ctable, "excluded_farms"); + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(autofarm_setDefaultThreshold), + DFHACK_LUA_FUNCTION(autofarm_setThreshold), + DFHACK_LUA_FUNCTION(autofarm_removeThreshold), + DFHACK_LUA_FUNCTION(autofarm_setGrowthThreshold), + DFHACK_LUA_FUNCTION(autofarm_removeGrowthThreshold), + DFHACK_LUA_FUNCTION(autofarm_isFarmExcluded), + DFHACK_LUA_FUNCTION(autofarm_setFarmExcluded), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(autofarm_getData), + DFHACK_LUA_END +}; diff --git a/plugins/autogems.cpp b/plugins/autogems.cpp index 593e64670a..249552a006 100644 --- a/plugins/autogems.cpp +++ b/plugins/autogems.cpp @@ -314,7 +314,7 @@ bool read_config(color_ostream &out) { } } catch (Json::Exception &e) { - out.printerr("autogems: failed to read autogems.json: %s\n", e.what()); + out.printerr("autogems: failed to read autogems.json: {}\n", e.what()); return false; } @@ -326,7 +326,7 @@ bool read_config(color_ostream &out) { blacklist.insert(mat_index(item.asInt())); } else { - out.printerr("autogems: illegal item at position %i in blacklist\n", i); + out.printerr("autogems: illegal item at position {} in blacklist\n", i); } } } @@ -355,7 +355,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { if (enable != enabled) { if (!INTERPOSE_HOOK(autogem_hook, feed).apply(enable) || !INTERPOSE_HOOK(autogem_hook, render).apply(enable)) { - out.printerr("Could not %s autogem hooks!\n", enable? "insert": "remove"); + out.printerr("Could not {} autogem hooks!\n", enable? "insert": "remove"); return CR_FAILURE; } diff --git a/plugins/autolabor/CMakeLists.txt b/plugins/autolabor/CMakeLists.txt index 721654d202..1f3ce779a4 100644 --- a/plugins/autolabor/CMakeLists.txt +++ b/plugins/autolabor/CMakeLists.txt @@ -1,15 +1,26 @@ project(autolabor) # A list of source files set(COMMON_SRCS + joblabormapper.cpp + laborcommon.cpp + workdetails.cpp + legacyengine.cpp + modernengine.cpp ) # A list of headers -set(COMMON_HDRS laborstatemap.h +set(COMMON_HDRS + laborstatemap.h + joblabormapper.h + laborcommon.h + workdetails.h + engines.h + legacyengine.h + modernengine.h + monitorengine.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) # mash them together (headers are marked as headers and nothing will try to compile them) list(APPEND COMMON_SRCS ${COMMON_HDRS}) -#dfhack_plugin(labormanager labormanager.cpp joblabormapper.cpp ${COMMON_SRCS}) - dfhack_plugin(autolabor autolabor.cpp ${COMMON_SRCS} LINK_LIBRARIES lua) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d755202de6..12d7cf1c84 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,1242 +1,428 @@ -#include "laborstatemap.h" +/* + * autolabor plugin: unified labor management for Dwarf Fortress. + * + * One plugin binary exposes two commands: + * + * autolabor legacy mode: controls the labor matrix directly, bypassing + * the work detail system (the pre-v50 behavior). + * labormanager modern mode: expresses labor policy through DF's work + * details so the game's job auction does the assignment. + * + * A third mode, monitor, only watches the job board for starving postings + * and performs no labor management. + * + * The active mode is persisted per fort and can be switched from either + * command (" mode legacy|modern"), from the work details overlay, or + * implicitly by enabling through one of the commands. + */ -#include "Debug.h" +#include "engines.h" +#include "joblabormapper.h" +#include "laborcommon.h" +#include "legacyengine.h" +#include "modernengine.h" +#include "monitorengine.h" +#include "workdetails.h" + +#include "Core.h" +#include "LuaTools.h" +#include "PluginLua.h" #include "PluginManager.h" -#include "modules/Units.h" #include "modules/World.h" -#include "modules/MapCache.h" -#include "modules/Items.h" -#include "modules/Units.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include using namespace DFHack; using namespace df::enums; +using namespace autolabor; DFHACK_PLUGIN("autolabor"); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(game); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) +command_result autolabor_cmd(color_ostream &out, std::vector ¶ms); +command_result labormanager_cmd(color_ostream &out, std::vector ¶ms); -/* - * Autolabor module for dfhack - * - * The idea behind this module is to constantly adjust labors so that the right dwarves - * are assigned to new tasks. The key is that, for almost all labors, once a dwarf begins - * a job it will finish that job even if the associated labor is removed. Thus the - * strategy is to frequently decide, for each labor, which dwarves should possibly take - * a new job for that labor if it comes in and which shouldn't, and then set the labors - * appropriately. The updating should happen as often as can be reasonably done without - * causing lag. - * - * The obvious thing to do is to just set each labor on a single idle dwarf who is best - * suited to doing new jobs of that labor. This works in a way, but it leads to a lot - * of idle dwarves since only one dwarf will be dispatched for each labor in an update - * cycle, and dwarves that finish tasks will wait for the next update before being - * dispatched. An improvement is to also set some labors on dwarves that are currently - * doing a job, so that they will immediately take a new job when they finish. The - * details of which dwarves should have labors set is mostly a heuristic. - * - * A complication to the above simple scheme is labors that have associated equipment. - * Enabling/disabling these labors causes dwarves to change equipment, and disabling - * them in the middle of a job may cause the job to be abandoned. Those labors - * (mining, hunting, and woodcutting) need to be handled carefully to minimize churn. - */ +static WorkDetailManager wdm; +static LegacyEngine legacy_engine(&wdm); +static ModernEngine modern_engine(&wdm); +static MonitorEngine monitor_engine; +static LaborEngine *active_engine = NULL; -DFHACK_PLUGIN_IS_ENABLED(enable_autolabor); - -namespace DFHack { - DBG_DECLARE(autolabor, cycle, DebugCategory::LINFO); -} - -static std::vector state_count(NUM_STATE); - -static PersistentDataItem config; - -command_result autolabor (color_ostream &out, std::vector & parameters); - -static bool isOptionEnabled(unsigned flag) +static LaborEngine *mode_engine(int mode) { - return config.isValid() && (config.ival(0) & flag) != 0; + if (mode == MODE_MODERN) + return &modern_engine; + if (mode == MODE_MONITOR) + return &monitor_engine; + return &legacy_engine; } -enum ConfigFlags { - CF_ENABLED = 1, -}; - -static void setOptionEnabled(ConfigFlags flag, bool on) +// switch the running engine to the given mode; safe when disabled +static void activate_mode(color_ostream &out, int mode) { - if (!config.isValid()) + store_engine_mode(mode); + LaborEngine *want = mode_engine(mode); + if (active_engine == want) return; - - if (on) - config.ival(0) |= flag; - else - config.ival(0) &= ~flag; -} - - -static void generate_labor_to_skill_map(); - -enum labor_mode { - DISABLE, - HAULERS, - AUTOMATIC, -}; - -struct labor_info -{ - PersistentDataItem config; - - bool is_exclusive; - int active_dwarfs; - - labor_mode mode() { return (labor_mode) config.ival(0); } - - void set_mode(labor_mode mode) { config.ival(0) = mode; } - - int minimum_dwarfs() { return config.ival(1); } - void set_minimum_dwarfs(int minimum_dwarfs) { config.ival(1) = minimum_dwarfs; } - - int maximum_dwarfs() { return config.ival(2); } - void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } - - int talent_pool() { return config.ival(3); } - void set_talent_pool(int talent_pool) { config.ival(3) = talent_pool; } -}; - -struct labor_default -{ - labor_mode mode; - bool is_exclusive; - int minimum_dwarfs; - int maximum_dwarfs; - int active_dwarfs; -}; - -// The percentage of the dwarves assigned as haulers at any one time. -static int hauler_pct = 33; - -// The maximum percentage of dwarves who will be allowed to be idle. -// Decreasing this will encourage autolabor to keep dwarves busy, -// at the expense of making it harder for dwarves to specialize in -// specific skills. -static int idler_pct = 10; - -static std::vector labor_infos; - -static const struct labor_default default_labor_infos[] = { - /* MINE */ {AUTOMATIC, true, 2, 200, 0}, - /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, - /* CLEAN */ {HAULERS, false, 1, 200, 0}, - /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, - /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, - /* STONECUTTER */ {AUTOMATIC, false, 1, 200, 0}, - /* STONE_CARVER */ {AUTOMATIC, false, 1, 200, 0}, - /* MASON */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, - /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, - /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, - /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, - /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, - /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, - /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, - /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, - /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, - /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, - /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, - /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, - /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, - /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, - /* MILK */ {AUTOMATIC, false, 1, 200, 0}, - /* COOK */ {AUTOMATIC, false, 1, 200, 0}, - /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, - /* FISH */ {AUTOMATIC, false, 1, 1, 0}, - /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, - /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, - /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, - /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, - /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, - /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, - /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* DYER */ {AUTOMATIC, false, 1, 200, 0}, - /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, - /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, - /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, - /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, - /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, - /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, - /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, - /* BEEKEEPING */ {AUTOMATIC, false, 1, 200, 0}, - /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, - /* HANDLE_VEHICLES */ {HAULERS, false, 1, 200, 0}, - /* HAUL_TRADE */ {HAULERS, false, 1, 200, 0}, - /* PULL_LEVER */ {HAULERS, false, 1, 200, 0}, - /* UNUSED_13 */ {DISABLE, false, 1, 0, 0}, - /* HAUL_WATER */ {HAULERS, false, 1, 200, 0}, - /* GELD */ {AUTOMATIC, false, 1, 200, 0}, - /* BUILD_ROAD */ {AUTOMATIC, false, 1, 200, 0}, - /* BUILD_CONSTRUCTION */ {AUTOMATIC, false, 1, 200, 0}, - /* PAPERMAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* BOOKBINDING */ {AUTOMATIC, false, 1, 200, 0}, - /* UNUSED_20 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_21 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_22 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_23 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_24 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_25 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_26 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_27 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_28 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_29 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_30 */ {DISABLE, false, 0, 0, 0}, -}; - -static const int responsibility_penalties[] = { - 0, /* LAW_MAKING */ - 0, /* LAW_ENFORCEMENT */ - 3000, /* RECEIVE_DIPLOMATS */ - 0, /* MEET_WORKERS */ - 1000, /* MANAGE_PRODUCTION */ - 3000, /* TRADE */ - 1000, /* ACCOUNTING */ - 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ - 0, /* MAKE_INTRODUCTIONS */ - 0, /* MAKE_PEACE_AGREEMENTS */ - 0, /* MAKE_TOPIC_AGREEMENTS */ - 0, /* COLLECT_TAXES */ - 0, /* ESCORT_TAX_COLLECTOR */ - 0, /* EXECUTIONS */ - 0, /* TAME_EXOTICS */ - 0, /* RELIGION */ - 0, /* ATTACK_ENEMIES */ - 0, /* PATROL_TERRITORY */ - 0, /* MILITARY_GOALS */ - 0, /* MILITARY_STRATEGY */ - 0, /* UPGRADE_SQUAD_EQUIPMENT */ - 0, /* EQUIPMENT_MANIFESTS */ - 0, /* SORT_AMMUNITION */ - 0, /* BUILD_MORALE */ - 5000 /* HEALTH_MANAGEMENT */ -}; - -struct dwarf_info_t -{ - int highest_skill; - int total_skill; - int mastery_penalty; - int assigned_jobs; - dwarf_state state; - bool has_exclusive_labor; - int noble_penalty; // penalty for assignment due to noble status - bool medical; // this dwarf has medical responsibility - bool trader; // this dwarf has trade responsibility - bool diplomacy; // this dwarf meets with diplomats -}; - -static void cleanup_state() -{ - enable_autolabor = false; - labor_infos.clear(); - - // reinstate DF's work detail system - game->external_flag.bits.automatic_professions_disabled = false; + if (active_engine) + active_engine->disable(out); + active_engine = want; + if (is_enabled) + active_engine->enable(out); } -static void reset_labor(df::unit_labor labor) +static bool can_run(color_ostream &out) { - labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs); - labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); - labor_infos[labor].set_talent_pool(200); - labor_infos[labor].set_mode(default_labor_infos[labor].mode); -} - -static void init_state() -{ - config = World::GetPersistentSiteData("autolabor/config"); - if (config.isValid() && config.ival(0) == -1) - config.ival(0) = 0; - - enable_autolabor = isOptionEnabled(CF_ENABLED); - - if (!enable_autolabor) - return; - - // bypass DF's work detail system - game->external_flag.bits.automatic_professions_disabled = true; - - auto cfg_haulpct = World::GetPersistentSiteData("autolabor/haulpct"); - if (cfg_haulpct.isValid()) - { - hauler_pct = cfg_haulpct.ival(0); - } - else + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - hauler_pct = 33; - } - - // Load labors from save - labor_infos.resize(ARRAY_COUNT(default_labor_infos)); - - std::vector items; - World::GetPersistentSiteData(&items, "autolabor/labors/", true); - - for (auto& p : items) - { - std::string key = p.key(); - df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); - if (labor >= 0 && size_t(labor) < labor_infos.size()) - { - labor_infos[labor].config = p; - labor_infos[labor].is_exclusive = default_labor_infos[labor].is_exclusive; - labor_infos[labor].active_dwarfs = 0; - } - } - - // Add default labors for those not in save - for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { - if (labor_infos[i].config.isValid()) - continue; - - std::stringstream name; - name << "autolabor/labors/" << i; - - labor_infos[i].config = World::AddPersistentSiteData(name.str()); - - labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; - labor_infos[i].active_dwarfs = 0; - reset_labor((df::unit_labor) i); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); + return false; } - - generate_labor_to_skill_map(); - + return true; } -static df::job_skill labor_to_skill[ENUM_LAST_ITEM(unit_labor) + 1]; - -static void generate_labor_to_skill_map() +DFhackCExport command_result plugin_init(color_ostream &out, + std::vector &commands) { - // Generate labor -> skill mapping - - for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++) - labor_to_skill[i] = job_skill::NONE; - - FOR_ENUM_ITEMS(job_skill, skill) - { - int labor = ENUM_ATTR(job_skill, labor, skill); - if (labor != unit_labor::NONE) - { - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_to_skill)); - */ - - labor_to_skill[labor] = skill; - } - } + commands.push_back(PluginCommand( + "autolabor", + "Automatically manage dwarf labors (legacy labor matrix mode).", + autolabor_cmd)); + commands.push_back(PluginCommand( + "labormanager", + "Automatically manage dwarf labors (work detail mode).", + labormanager_cmd)); + return CR_OK; } - -static void enable_plugin(color_ostream &out) +DFhackCExport command_result plugin_shutdown(color_ostream &out) { - if (!config.isValid()) - { - config = World::AddPersistentSiteData("autolabor/config"); - config.ival(0) = 0; - } - - setOptionEnabled(CF_ENABLED, true); - enable_autolabor = true; - out << "Enabling autolabor." << std::endl; - - cleanup_state(); - init_state(); + if (active_engine && is_enabled && Core::getInstance().isMapLoaded()) + active_engine->disable(out); + active_engine = NULL; + return CR_OK; } -static void disable_plugin(color_ostream& out) +DFhackCExport command_result plugin_load_site_data(color_ostream &out) { - if (config.isValid()) - setOptionEnabled(CF_ENABLED, false); + load_plugin_config(); + wdm.reset(); + init_labor_to_skill(); - enable_autolabor = false; - out << "Disabling autolabor." << std::endl; + is_enabled = config_flag(CF_ENABLED); + active_engine = NULL; + if (is_enabled) + activate_mode(out, engine_mode()); - cleanup_state(); + return CR_OK; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_onstatechange(color_ostream &out, + state_change_event event) { - // initialize labor infos table from default table - if(ARRAY_COUNT(default_labor_infos) != ENUM_LAST_ITEM(unit_labor) + 1) + if (event == SC_MAP_UNLOADED) { - out.printerr("autolabor: labor size mismatch\n"); - return CR_FAILURE; + legacy_engine.map_unload(); + modern_engine.map_unload(); + monitor_engine.map_unload(); + active_engine = NULL; + // is_enabled stays set; CF_ENABLED persists the user's intent and + // plugin_load_site_data will re-enable the engine on the next map } - - commands.push_back(PluginCommand( - "autolabor", - "Automatically manage dwarf labors.", - autolabor)); - - init_state(); - return CR_OK; } -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +DFhackCExport command_result plugin_onupdate(color_ostream &out) { - cleanup_state(); - + if (is_enabled && active_engine) + active_engine->update(out); return CR_OK; } -// sorting objects -struct dwarfinfo_sorter +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - dwarfinfo_sorter(std::vector & info):dwarf_info(info){}; - bool operator() (int i,int j) - { - if (dwarf_info[i].state == IDLE && dwarf_info[j].state != IDLE) - return true; - if (dwarf_info[i].state != IDLE && dwarf_info[j].state == IDLE) - return false; - return dwarf_info[i].mastery_penalty > dwarf_info[j].mastery_penalty; - }; - std::vector & dwarf_info; -}; -struct laborinfo_sorter -{ - bool operator() (int i,int j) - { - if (labor_infos[i].mode() != labor_infos[j].mode()) - return labor_infos[i].mode() < labor_infos[j].mode(); - if (labor_infos[i].is_exclusive != labor_infos[j].is_exclusive) - return labor_infos[i].is_exclusive; - if (labor_infos[i].maximum_dwarfs() != labor_infos[j].maximum_dwarfs()) - return labor_infos[i].maximum_dwarfs() < labor_infos[j].maximum_dwarfs(); - return false; - }; -}; + if (!can_run(out)) + return CR_FAILURE; -struct values_sorter -{ - values_sorter(std::vector & values):values(values){}; - bool operator() (int i,int j) + if (enable && !is_enabled) { - return values[i] > values[j]; - }; - std::vector & values; -}; - - -static void assign_labor(unit_labor::unit_labor labor, - int n_dwarfs, - std::vector& dwarf_info, - bool trader_requested, - std::vector& dwarfs, - bool has_butchers, - bool has_fishery, - color_ostream& out) -{ - df::job_skill skill = labor_to_skill[labor]; - - if (labor_infos[labor].mode() != AUTOMATIC) - return; - - int best_skill = 0; - - std::vector values(n_dwarfs); - std::vector candidates; - std::map dwarf_skill; - std::map dwarf_skillxp; - std::vector previously_enabled(n_dwarfs); - - // Find candidate dwarfs, and calculate a preference value for each dwarf - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) - { - if (dwarf_info[dwarf].state == CHILD) - continue; - if (dwarf_info[dwarf].state == MILITARY) - continue; - if (dwarf_info[dwarf].trader && trader_requested) - continue; - if (dwarf_info[dwarf].diplomacy) - continue; - - if (labor_infos[labor].is_exclusive && dwarf_info[dwarf].has_exclusive_labor) - continue; - - int value = dwarf_info[dwarf].mastery_penalty; - - if (skill != job_skill::NONE) - { - int skill_level = 0; - int skill_experience = 0; - - for (auto s : dwarfs[dwarf]->status.souls[0]->skills) - { - if (s->id == skill) - { - skill_level = s->rating; - skill_experience = s->experience; - break; - } - } - - dwarf_skill[dwarf] = skill_level; - dwarf_skillxp[dwarf] = skill_experience; - - if (best_skill < skill_level) - best_skill = skill_level; - - value += skill_level * 100; - value += skill_experience / 20; - if (skill_level > 0 || skill_experience > 0) - value += 200; - if (skill_level >= 15) - value += 1000 * (skill_level - 14); - } - else - { - dwarf_skill[dwarf] = 0; - } - - if (dwarfs[dwarf]->status.labors[labor]) - { - value += 5; - if (labor_infos[labor].is_exclusive) - value += 350; - } - - if (dwarf_info[dwarf].has_exclusive_labor) - value -= 500; - - // bias by happiness - - //value += dwarfs[dwarf]->status.happiness; - - values[dwarf] = value; - - candidates.push_back(dwarf); - - } - - int pool = labor_infos[labor].talent_pool(); - if (pool < 200 && candidates.size() > 1 && size_t(abs(pool)) < candidates.size()) - { - // Sort in descending order - std::sort(candidates.begin(), candidates.end(), [&](const int lhs, const int rhs) -> bool { - if (dwarf_skill[lhs] == dwarf_skill[rhs]) - if (pool > 0) - return dwarf_skillxp[lhs] > dwarf_skillxp[rhs]; - else - return dwarf_skillxp[lhs] < dwarf_skillxp[rhs]; - else - if (pool > 0) - return dwarf_skill[lhs] > dwarf_skill[rhs]; - else - return dwarf_skill[lhs] < dwarf_skill[rhs]; - }); - - // Check if all dwarves have equivalent skills, usually zero - int first_dwarf = candidates[0]; - int last_dwarf = candidates[candidates.size() - 1]; - if (dwarf_skill[first_dwarf] == dwarf_skill[last_dwarf] && - dwarf_skillxp[first_dwarf] == dwarf_skillxp[last_dwarf]) - { - // There's no difference in skill, so change nothing - } - else - { - // Trim down to our top (or not) talents - candidates.resize(abs(pool)); - } - } - - // Sort candidates by preference value - values_sorter ivs(values); - std::sort(candidates.begin(), candidates.end(), ivs); - - // Disable the labor on everyone - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) - { - if (dwarf_info[dwarf].state == CHILD) - continue; - - previously_enabled[dwarf] = dwarfs[dwarf]->status.labors[labor]; - dwarfs[dwarf]->status.labors[labor] = false; - } - - int min_dwarfs = labor_infos[labor].minimum_dwarfs(); - int max_dwarfs = labor_infos[labor].maximum_dwarfs(); - - // Special - don't assign hunt without a butchers, or fish without a fishery - if (unit_labor::HUNT == labor && !has_butchers) - min_dwarfs = max_dwarfs = 0; - if (unit_labor::FISH == labor && !has_fishery) - min_dwarfs = max_dwarfs = 0; - - // If there are enough idle dwarves to choose from, enter an aggressive assignment - // mode. "Enough" idle dwarves is defined as 2 or 10% of the total number of dwarves, - // whichever is higher. - // - // In aggressive mode, we will always pick at least one idle dwarf for each skill, - // in order to try to get the idle dwarves to start doing something. We also pick - // any dwarf more preferable to the idle dwarf, since we'd rather have a more - // preferable dwarf do a new job if one becomes available (probably because that - // dwarf just finished a job). - // - // In non-aggressive mode, only dwarves that are good at a labor will be assigned - // to it. Dwarves good at nothing, or nothing that needs doing, will tend to get - // assigned to hauling by the hauler code. If there are no hauling jobs to do, - // they will sit around idle and when enough build up they will trigger aggressive - // mode again. - bool aggressive_mode = state_count[IDLE] >= 2 && state_count[IDLE] >= n_dwarfs * idler_pct / 100; - - /* - * Assign dwarfs to this labor. We assign at least the minimum number of dwarfs, in - * order of preference, and then assign additional dwarfs that meet any of these conditions: - * - We are in aggressive mode and have not yet assigned an idle dwarf - * - The dwarf is good at this skill - * - The labor is mining, hunting, or woodcutting and the dwarf currently has it enabled. - * We stop assigning dwarfs when we reach the maximum allowed. - * Note that only idle and busy dwarfs count towards the number of dwarfs. "Other" dwarfs - * (sleeping, eating, on break, etc.) will have labors assigned, but will not be counted. - * Military and children/nobles will not have labors assigned. - * Dwarfs with the "health management" responsibility are always assigned DIAGNOSIS. - */ - for (size_t i = 0; i < candidates.size() && labor_infos[labor].active_dwarfs < max_dwarfs; i++) - { - int dwarf = candidates[i]; - - if (dwarf_info[dwarf].trader && trader_requested) - continue; - if (dwarf_info[dwarf].diplomacy) - continue; - - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); - - bool preferred_dwarf = false; - if (dwarf_skillxp[dwarf] > 0 && dwarf_skill[dwarf] >= best_skill / 2) - preferred_dwarf = true; - if (previously_enabled[dwarf] && labor_infos[labor].is_exclusive && dwarf_info[dwarf].state == EXCLUSIVE) - preferred_dwarf = true; - if (dwarf_info[dwarf].medical && labor == df::unit_labor::DIAGNOSE) - preferred_dwarf = true; - - if (labor_infos[labor].active_dwarfs >= min_dwarfs && !preferred_dwarf && !aggressive_mode) - continue; - - if (!dwarfs[dwarf]->status.labors[labor]) - dwarf_info[dwarf].assigned_jobs++; - - if (Units::isValidLabor(dwarfs[dwarf], labor)) - dwarfs[dwarf]->status.labors[labor] = true; - - if (labor_infos[labor].is_exclusive) - { - dwarf_info[dwarf].has_exclusive_labor = true; - // all the exclusive labors require equipment so this should force the dorf to reequip if needed - dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; - } - - TRACE(cycle, out).print("Dwarf % i \"%s\" assigned %s: value %i %s %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str(), values[dwarf], - dwarf_info[dwarf].trader ? "(trader)" : "", - dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - labor_infos[labor].active_dwarfs++; - - if (dwarf_info[dwarf].state == IDLE) - aggressive_mode = false; - } -} - -static const int32_t CYCLE_TICKS = 61; -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - cycle_timestamp = 0; - cleanup_state(); - init_state(); - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_UNLOADED: - cleanup_state(); - break; - default: - break; + set_config_flag(CF_ENABLED, true); + is_enabled = true; + activate_mode(out, engine_mode()); + } + else if (!enable && is_enabled) + { + set_config_flag(CF_ENABLED, false); + is_enabled = false; + if (active_engine) + active_engine->disable(out); + active_engine = NULL; } return CR_OK; } -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +// handle " mode" / " mode legacy|modern"; returns true if handled +static bool mode_command(color_ostream &out, std::vector ¶meters) { - if(!world || !world->map.block_index || !enable_autolabor) - { - return CR_OK; - } - - if (world->frame_counter - cycle_timestamp <= CYCLE_TICKS) - return CR_OK; - - cycle_timestamp = world->frame_counter; - - std::vector dwarfs; - - bool has_butchers = false; - bool has_fishery = false; - bool trader_requested = false; - - for (auto& build : world->buildings.all) - { - auto type = build->getType(); - if (building_type::Workshop == type) - { - df::workshop_type subType = (df::workshop_type)build->getSubtype(); - if (workshop_type::Butchers == subType) - has_butchers = true; - if (workshop_type::Fishery == subType) - has_fishery = true; - } - else if (building_type::TradeDepot == type) - { - df::building_tradedepotst* depot = (df::building_tradedepotst*) build; - trader_requested = trader_requested || depot->trade_flags.bits.trader_requested; - TRACE(cycle,out).print(trader_requested - ? "Trade depot found and trader requested, trader will be excluded from all labors.\n" - : "Trade depot found but trader is not requested.\n" - ); - } - } + if (parameters.empty() || parameters[0] != "mode") + return false; - for (auto& cre : world->units.active) + if (parameters.size() == 1) { - if (Units::isCitizen(cre)) - { - if (cre->burrows.size() > 0) - continue; // dwarfs assigned to active burrows are skipped entirely - dwarfs.push_back(cre); - } + out << "Mode: " + << (engine_mode() == MODE_MODERN ? "modern (labormanager)" : + engine_mode() == MODE_MONITOR ? "monitor (starvation warnings)" : + "legacy (autolabor)") + << std::endl; + return true; } - int n_dwarfs = dwarfs.size(); - - if (n_dwarfs == 0) - return CR_OK; - - std::vector dwarf_info(n_dwarfs); - - // Find total skill and highest skill for each dwarf. More skilled dwarves shouldn't be used for minor tasks. - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + int want = -1; + if (parameters.size() == 2) { - if (dwarfs[dwarf]->status.souls.size() <= 0) - continue; - - // compute noble penalty - - int noble_penalty = 0; - - df::historical_figure* hf = df::historical_figure::find(dwarfs[dwarf]->hist_figure_id); - if(hf!=NULL) //can be NULL. E.g. script created citizens - for (auto& hfelink : hf->entity_links) - { - if (hfelink->getType() == df::histfig_entity_link_type::POSITION) - { - df::histfig_entity_link_positionst *epos = - (df::histfig_entity_link_positionst*) hfelink; - df::historical_entity* entity = df::historical_entity::find(epos->entity_id); - if (!entity) - continue; - df::entity_position_assignment* assignment = binsearch_in_vector(entity->positions.assignments, epos->assignment_id); - if (!assignment) - continue; - df::entity_position* position = binsearch_in_vector(entity->positions.own, assignment->position_id); - if (!position) - continue; - - for (int n = 0; n < 25; n++) - if (position->responsibilities[n]) - noble_penalty += responsibility_penalties[n]; - - if (position->responsibilities[df::entity_position_responsibility::HEALTH_MANAGEMENT]) - dwarf_info[dwarf].medical = true; - - if (position->responsibilities[df::entity_position_responsibility::TRADE]) - dwarf_info[dwarf].trader = true; - - } - - } - - dwarf_info[dwarf].noble_penalty = noble_penalty; - - // identify dwarfs who are needed for meetings and mark them for exclusion - - for (auto& act : plotinfo->activities) - { - if (!act) continue; - bool p1 = act->unit_actor == dwarfs[dwarf]->id; - bool p2 = act->unit_noble == dwarfs[dwarf]->id; - - if (p1 || p2) - { - dwarf_info[dwarf].diplomacy = true; - DEBUG(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str()); - break; - } - } - - for (auto& skill : dwarfs[dwarf]->status.souls[0]->skills) - { - df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); - - int skill_level = skill->rating; - - // Track total & highest skill among normal/medical skills. (We don't care about personal or social skills.) - - if (skill_class != job_skill_class::Normal && skill_class != job_skill_class::Medical) - continue; - - if (dwarf_info[dwarf].highest_skill < skill_level) - dwarf_info[dwarf].highest_skill = skill_level; - dwarf_info[dwarf].total_skill += skill_level; - } + if (parameters[1] == "legacy") + want = MODE_LEGACY; + else if (parameters[1] == "modern") + want = MODE_MODERN; + else if (parameters[1] == "monitor") + want = MODE_MONITOR; } - // Calculate a base penalty for using each dwarf for a task he isn't good at. - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + if (want == -1) { - dwarf_info[dwarf].mastery_penalty -= 40 * dwarf_info[dwarf].highest_skill; - dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill; - dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty; - - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - - if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor]) - dwarf_info[dwarf].mastery_penalty -= 100; - } + out.printerr("Syntax: mode [legacy|modern|monitor]\n"); + return true; } - // Find the activity state for each dwarf. It's important to get this right - a dwarf who we think is IDLE but - // can't work will gum everything up. In the future I might add code to auto-detect slacker dwarves. - - state_count.clear(); - state_count.resize(NUM_STATE); - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) - { - if (Units::isBaby(dwarfs[dwarf]) || - Units::isChild(dwarfs[dwarf]) || - dwarfs[dwarf]->profession == profession::DRUNK) - { - dwarf_info[dwarf].state = CHILD; - } - else if (ENUM_ATTR(profession, military, dwarfs[dwarf]->profession)) - dwarf_info[dwarf].state = MILITARY; - else if (dwarfs[dwarf]->job.current_job == NULL) - { - if (Units::getMiscTrait(dwarfs[dwarf], misc_trait_type::Migrant)) - dwarf_info[dwarf].state = OTHER; - else if (dwarfs[dwarf]->specific_refs.size() > 0) - dwarf_info[dwarf].state = OTHER; - else - dwarf_info[dwarf].state = IDLE; - } - else - { - int job = dwarfs[dwarf]->job.current_job->job_type; - if (job >= 0 && size_t(job) < ARRAY_COUNT(dwarf_states)) - dwarf_info[dwarf].state = dwarf_states[job]; - else - { - WARN(cycle, out).print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job); - dwarf_info[dwarf].state = OTHER; - } - } + if (!can_run(out)) + return true; - state_count[dwarf_info[dwarf].state]++; + activate_mode(out, want); + out << "Mode: " << parameters[1] << std::endl; + return true; +} - TRACE(cycle, out).print("Dwarf %i \"%s\": penalty %i, state %s\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); - } +command_result autolabor_cmd(color_ostream &out, std::vector ¶meters) +{ + if (!can_run(out)) + return CR_FAILURE; - std::vector labors; + if (mode_command(out, parameters)) + return CR_OK; - FOR_ENUM_ITEMS(unit_labor, labor) + if (parameters.size() == 1 && parameters[0] == "dump") { - if (labor == unit_labor::NONE) - continue; - - labor_infos[labor].active_dwarfs = 0; - - labors.push_back(labor); + out.print("=== autolabor state dump ===\n"); + out.print("enabled={} mode={}\n", (int)is_enabled, engine_mode()); + autolabor::dump_engine_state(out); + return CR_OK; } - laborinfo_sorter lasorter; - std::sort(labors.begin(), labors.end(), lasorter); - // Handle DISABLED skills (just bookkeeping). - // Note that autolabor should *NEVER* enable or disable a skill that has been marked as DISABLED, for any reason. - // The user has told us that they want manage this skill manually, and we must respect that. - for (auto& labor: labors) + if (parameters.size() == 1 && + (parameters[0] == "0" || parameters[0] == "enable" || + parameters[0] == "1" || parameters[0] == "disable")) { - if (labor_infos[labor].mode() != DISABLE) - continue; - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + bool enable = parameters[0] == "1" || parameters[0] == "enable"; + if (enable) { - if (dwarfs[dwarf]->status.labors[labor]) - { - if (labor_infos[labor].is_exclusive) - dwarf_info[dwarf].has_exclusive_labor = true; - - dwarf_info[dwarf].assigned_jobs++; - } + store_engine_mode(MODE_LEGACY); + if (is_enabled) + activate_mode(out, MODE_LEGACY); } + return plugin_enable(out, enable); } - // Handle all skills except those marked HAULERS - - for (auto& labor : labors) - { - assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out); - } + if (legacy_engine.command(out, parameters)) + return CR_OK; - // Set about 1/3 of the dwarfs as haulers. The haulers have all HAULER labors enabled. Having a lot of haulers helps - // make sure that hauling jobs are handled quickly rather than building up. + out.print("Automatically assigns labors to dwarves by writing the labor\n" + "matrix directly (legacy mode). Activate with 'autolabor enable',\n" + "deactivate with 'autolabor disable'.\n" + "Current state: {}, mode: {}.\n", (int)is_enabled, + engine_mode() == MODE_MODERN ? "modern" : "legacy"); + return CR_OK; +} - int num_haulers = state_count[IDLE] + (state_count[BUSY] + state_count[EXCLUSIVE]) * hauler_pct / 100; +command_result labormanager_cmd(color_ostream &out, std::vector ¶meters) +{ + if (!can_run(out)) + return CR_FAILURE; - if (num_haulers < 1) - num_haulers = 1; + if (mode_command(out, parameters)) + return CR_OK; - std::vector hauler_ids; - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + if (parameters.size() == 1 && + (parameters[0] == "0" || parameters[0] == "enable" || + parameters[0] == "1" || parameters[0] == "disable")) { - if ((dwarf_info[dwarf].trader && trader_requested) || - dwarf_info[dwarf].diplomacy) + bool enable = parameters[0] == "1" || parameters[0] == "enable"; + if (enable) { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - if (labor_infos[labor].mode() != HAULERS) - continue; - dwarfs[dwarf]->status.labors[labor] = false; - } - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - { - num_haulers--; - } - continue; + store_engine_mode(MODE_MODERN); + if (is_enabled) + activate_mode(out, MODE_MODERN); } - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - hauler_ids.push_back(dwarf); - } - dwarfinfo_sorter sorter(dwarf_info); - // Idle dwarves come first, then we sort from least-skilled to most-skilled. - std::sort(hauler_ids.begin(), hauler_ids.end(), sorter); - - // don't set any haulers if everyone is off drinking or something - if (hauler_ids.size() == 0) { - num_haulers = 0; + return plugin_enable(out, enable); } - FOR_ENUM_ITEMS(unit_labor, labor) + if (parameters.size() == 1 && parameters[0] == "dump") { - if (labor == unit_labor::NONE) - continue; - - if (labor_infos[labor].mode() != HAULERS) - continue; - - for (int i = 0; i < num_haulers; i++) - { - assert(size_t(i) < hauler_ids.size()); - - int dwarf = hauler_ids[i]; - - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); - dwarfs[dwarf]->status.labors[labor] = true; - dwarf_info[dwarf].assigned_jobs++; - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - labor_infos[labor].active_dwarfs++; - - TRACE(cycle, out).print("Dwarf %i \"%s\" assigned %s: hauler\n", - dwarf, dwarfs[dwarf]->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, labor).c_str()); - } + out.print("=== autolabor state dump ===\n"); + out.print("enabled={} mode={}\n", (int)is_enabled, engine_mode()); + autolabor::dump_engine_state(out); + return CR_OK; + } - for (size_t i = num_haulers; i < hauler_ids.size(); i++) - { - assert(i < hauler_ids.size()); + // in monitor mode the monitor engine handles status/list; everything + // else (balance, labor config, ...) falls through to the modern dialect + if (engine_mode() == MODE_MONITOR && monitor_engine.command(out, parameters)) + return CR_OK; - int dwarf = hauler_ids[i]; + if (modern_engine.command(out, parameters)) + return CR_OK; - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); + out.print("Automatically assigns labors to dwarves through the work detail\n" + "system (modern mode). Activate with 'labormanager enable',\n" + "deactivate with 'labormanager disable'.\n" + "Current state: {}, mode: {}.\n", (int)is_enabled, + engine_mode() == MODE_MODERN ? "modern" : "legacy"); + return CR_OK; +} - dwarfs[dwarf]->status.labors[labor] = false; - } - } +// --------------------------------------------------------------------------- +// Lua API for the work details overlay (plugins.autolabor.*) +// --------------------------------------------------------------------------- - return CR_OK; +static int autolabor_getMode(lua_State *L) +{ + Lua::Push(L, engine_mode()); + return 1; } -void print_labor (df::unit_labor labor, color_ostream &out) +static int autolabor_getStatus(lua_State *L) { - std::string labor_name = ENUM_KEY_STR(unit_labor, labor); - out << labor_name << ": "; - for (int i = 0; i < 20 - (int)labor_name.length(); i++) - out << ' '; - if (labor_infos[labor].mode() == DISABLE) - out << "disabled" << std::endl; - else - { - if (labor_infos[labor].mode() == HAULERS) - out << "haulers"; - else - out << "minimum " << labor_infos[labor].minimum_dwarfs() << ", maximum " << labor_infos[labor].maximum_dwarfs() - << ", pool " << labor_infos[labor].talent_pool(); - out << ", currently " << labor_infos[labor].active_dwarfs << " dwarfs" << std::endl; - } + std::string status; + if (is_enabled && active_engine == &modern_engine) + status = modern_engine.status_line(); + Lua::Push(L, status); + return 1; } -DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) +static int autolabor_getBalanceStops(lua_State *L) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } + for (int i = 0; i < NUM_BALANCE_STOPS; i++) + Lua::Push(L, balance_stop_name(i)); + return NUM_BALANCE_STOPS; +} - if (enable && !enable_autolabor) - { - enable_plugin(out); - } - else if(!enable && enable_autolabor) - { - disable_plugin(out); - } +static int autolabor_getBalance(lua_State *L) +{ + Lua::Push(L, balance()); + return 1; +} - return CR_OK; +static void autolabor_setBalance(color_ostream &out, int32_t stop) +{ + if (can_run(out)) + store_balance(stop); } -command_result autolabor (color_ostream &out, std::vector & parameters) +static int autolabor_getIdleReserve(lua_State *L) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } + Lua::Push(L, idle_reserve()); + return 1; +} - if (parameters.size() == 1 && - (parameters[0] == "0" || parameters[0] == "enable" || - parameters[0] == "1" || parameters[0] == "disable")) +// starving job postings on the board; only tracked while the modern or +// monitor engine is running. returns the count plus, when nonzero, the +// caption, age (ticks), and map position of the longest-starving posting +static int autolabor_getStarvingJobs(lua_State *L) +{ + int count = 0; + std::string name; + int32_t wait = 0; + df::coord pos; + if (is_enabled && active_engine == &modern_engine) { - bool enable = (parameters[0] == "1" || parameters[0] == "enable"); - - return plugin_enable(out, enable); + count = modern_engine.starving_jobs(); + name = modern_engine.oldest_starving_name(); + wait = modern_engine.oldest_starving_wait(); + pos = modern_engine.oldest_starving_pos(); } - else if (parameters.size() == 2 && parameters[0] == "haulpct") + else if (is_enabled && active_engine == &monitor_engine) { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } - - int pct = atoi (parameters[1].c_str()); - hauler_pct = pct; - return CR_OK; + count = monitor_engine.starving_jobs(); + name = monitor_engine.oldest_starving_name(); + wait = monitor_engine.oldest_starving_wait(); + pos = monitor_engine.oldest_starving_pos(); } - else if (parameters.size() >= 2 && parameters.size() <= 4) + Lua::Push(L, count); + if (count > 0) { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } - - df::unit_labor labor = unit_labor::NONE; - - FOR_ENUM_ITEMS(unit_labor, test_labor) - { - if (parameters[0] == ENUM_KEY_STR(unit_labor, test_labor)) - labor = test_labor; - } - - if (labor == unit_labor::NONE) - { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); - return CR_WRONG_USAGE; - } - - if (parameters[1] == "haulers") - { - labor_infos[labor].set_mode(HAULERS); - print_labor(labor, out); - return CR_OK; - } - if (parameters[1] == "disable") - { - labor_infos[labor].set_mode(DISABLE); - print_labor(labor, out); - return CR_OK; - } - if (parameters[1] == "reset") - { - reset_labor(labor); - print_labor(labor, out); - return CR_OK; - } - - int minimum = atoi (parameters[1].c_str()); - int maximum = 200; - int pool = 200; - - if (parameters.size() >= 3) - maximum = atoi (parameters[2].c_str()); - if (parameters.size() == 4) - pool = std::stoi(parameters[3]); - - if (maximum < minimum || maximum < 0 || minimum < 0) - { - out.printerr("Syntax: autolabor [] [], %d > %d\n", maximum, minimum); - return CR_WRONG_USAGE; - } - - labor_infos[labor].set_minimum_dwarfs(minimum); - labor_infos[labor].set_maximum_dwarfs(maximum); - labor_infos[labor].set_talent_pool(pool); - labor_infos[labor].set_mode(AUTOMATIC); - print_labor(labor, out); - - return CR_OK; + Lua::Push(L, name); + Lua::Push(L, (int32_t)wait); + Lua::Push(L, (int32_t)pos.x); + Lua::Push(L, (int32_t)pos.y); + Lua::Push(L, (int32_t)pos.z); + return 6; } - else if (parameters.size() == 1 && parameters[0] == "reset-all") - { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } + return 1; +} - for (size_t i = 0; i < labor_infos.size(); i++) - { - reset_labor((df::unit_labor) i); - } - out << "All labors reset." << std::endl; - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) - { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } +static void autolabor_setIdleReserve(color_ostream &out, int32_t pct) +{ + if (can_run(out)) + store_idle_reserve(pct); +} - bool need_comma = 0; - for (int i = 0; i < NUM_STATE; i++) - { - if (state_count[i] == 0) - continue; - if (need_comma) - out << ", "; - out << state_count[i] << ' ' << state_names[i]; - need_comma = 1; - } - out << std::endl; +// returns a list of unit_labor enum names that the engines' static labor +// data doesn't cover; used by the labor coverage test in scripts/test +static int autolabor_checkLaborCoverage(lua_State *L) +{ + init_labor_to_skill(); + std::vector missing; + legacy_labor_coverage(missing); + modern_labor_coverage(missing); + Lua::Push(L, missing); + return 1; +} - if (parameters[0] == "list") - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; +// returns a list of job_type enum names with no entry in the job to labor +// table; used by the job coverage test in scripts/test +static int autolabor_checkJobCoverage(lua_State *L) +{ + std::vector missing; + JobLaborMapper mapper; + mapper.job_coverage(missing); + Lua::Push(L, missing); + return 1; +} - print_labor(labor, out); - } - } +static void autolabor_setMode(color_ostream &out, int32_t mode) +{ + if (!can_run(out)) + return; + if (mode != MODE_LEGACY && mode != MODE_MODERN && mode != MODE_MONITOR) + return; + activate_mode(out, mode); +} - return CR_OK; - } - else - { - out.print("Automatically assigns labors to dwarves.\n" - "Activate with 'enable autolabor', deactivate with 'disable autolabor'.\n" - "Current state: %d.\n", enable_autolabor); +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(autolabor_setMode), + DFHACK_LUA_FUNCTION(autolabor_setBalance), + DFHACK_LUA_FUNCTION(autolabor_setIdleReserve), + DFHACK_LUA_END +}; - return CR_OK; - } -} +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(autolabor_getMode), + DFHACK_LUA_COMMAND(autolabor_getStatus), + DFHACK_LUA_COMMAND(autolabor_getBalance), + DFHACK_LUA_COMMAND(autolabor_getBalanceStops), + DFHACK_LUA_COMMAND(autolabor_getIdleReserve), + DFHACK_LUA_COMMAND(autolabor_getStarvingJobs), + DFHACK_LUA_COMMAND(autolabor_checkLaborCoverage), + DFHACK_LUA_COMMAND(autolabor_checkJobCoverage), + DFHACK_LUA_END +}; diff --git a/plugins/autolabor/engines.h b/plugins/autolabor/engines.h new file mode 100644 index 0000000000..b236f34f89 --- /dev/null +++ b/plugins/autolabor/engines.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "ColorText.h" +#include "PluginManager.h" + +using namespace DFHack; + +// One labor-management strategy. Exactly one engine is active at a time, +// selected by the plugin's persisted mode. Engines are stateless between +// update() calls except for persisted config and small persistent caches. +class LaborEngine { +public: + virtual ~LaborEngine() {} + + // Called when the plugin is enabled in this engine's mode (including on + // map load). Must leave the fort's labor state consistent. + virtual void enable(color_ostream &out) = 0; + + // Called when the plugin is disabled or the mode is switched away. + // Must undo any labor-affecting state so vanilla behaves normally. + virtual void disable(color_ostream &out) = 0; + + // Called every frame while active; engines throttle internally. + virtual void update(color_ostream &out) = 0; + + // Handle the engine's command dialect. Returns false if the command + // isn't recognized so the caller can try shared handling. + virtual bool command(color_ostream &out, std::vector ¶meters) = 0; +}; diff --git a/plugins/autolabor/joblabormapper.cpp b/plugins/autolabor/joblabormapper.cpp index c1d773e046..95942d20d8 100644 --- a/plugins/autolabor/joblabormapper.cpp +++ b/plugins/autolabor/joblabormapper.cpp @@ -15,8 +15,18 @@ */ +#include "joblabormapper.h" + +#include +#include +#include +#include + +#include "Core.h" #include "DataDefs.h" +#include "Debug.h" #include "MiscUtils.h" + #include "modules/Materials.h" #include @@ -26,31 +36,23 @@ #include #include #include - +#include #include - #include #include #include - +#include #include #include - #include #include #include - +#include #include - #include - #include - #include -#include -#include - using namespace std; using std::string; using std::endl; @@ -59,8 +61,19 @@ using namespace df::enums; using df::global::plotinfo; using df::global::world; -#include "labormanager.h" -#include "joblabormapper.h" +namespace DFHack { + DBG_DECLARE(autolabor, mapper, DebugCategory::LWARNING); +} + +// stand-ins for the old labormanager debug hooks; these only fire on +// failure-to-map paths that indicate a gap in the labor tables +template +static void jlm_debug(fmt::format_string fmt, Args&&... args) +{ + WARN(mapper, Core::getInstance().getConsole()).print("{}", + fmt::format(fmt, std::forward(args)...)); +} +static void jlm_debug_pause() {} static df::unit_labor hauling_labor_map[] = { @@ -302,8 +315,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Workshop: { df::building_workshopst* ws = (df::building_workshopst*) bld; - if (ws->design && !ws->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; if (ws->type == df::workshop_type::Custom) { df::building_def* def = df::building_def::find(ws->custom_type); @@ -316,8 +327,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Furnace: { df::building_furnacest* frn = (df::building_furnacest*) bld; - if (frn->design && !frn->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; if (frn->type == df::furnace_type::Custom) { df::building_def* def = df::building_def::find(frn->custom_type); @@ -343,8 +352,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Windmill: { df::building_actual* b = (df::building_actual*) bld; - if (b->design && !b->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; return construction_build_labor(b); } break; @@ -397,9 +404,9 @@ class jlfunc_construct_bld : public jlfunc return df::unit_labor::BUILD_ROAD; } - debug("LABORMANAGER: Cannot deduce labor for construct building job of type %s\n", - ENUM_KEY_STR(building_type, bld->getType()).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for construct building job of type {}\n", + ENUM_KEY_STR(building_type, bld->getType())); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -512,9 +519,9 @@ class jlfunc_destroy_bld : public jlfunc return df::unit_labor::SIEGECRAFT; } - debug("LABORMANAGER: Cannot deduce labor for destroy building job of type %s\n", - ENUM_KEY_STR(building_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for destroy building job of type {}\n", + ENUM_KEY_STR(building_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -536,6 +543,10 @@ class jlfunc_make : public jlfunc { case df::workshop_type::Craftsdwarfs: { + // v50: stone blocks are stonecutting work, even when they are + // made at a craftsdwarf's workshop + if (j->job_type == df::job_type::ConstructBlocks) + return df::unit_labor::STONECUTTER; df::item_type jobitem = j->job_items.elements[0]->item_type; switch (jobitem) { @@ -550,8 +561,8 @@ class jlfunc_make : public jlfunc return df::unit_labor::BONE_CARVE; else { - debug("LABORMANAGER: Cannot deduce labor for make crafts job (not bone)\n"); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make crafts job (not bone)\n"); + jlm_debug_pause(); return df::unit_labor::NONE; } case df::item_type::WOOD: @@ -561,14 +572,20 @@ class jlfunc_make : public jlfunc case df::item_type::SKIN_TANNED: return df::unit_labor::LEATHER; default: - debug("LABORMANAGER: Cannot deduce labor for make crafts job, item type %s\n", - ENUM_KEY_STR(item_type, jobitem).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make crafts job, item type {}\n", + ENUM_KEY_STR(item_type, jobitem)); + jlm_debug_pause(); return df::unit_labor::NONE; } } case df::workshop_type::Masons: - return df::unit_labor::MASON; + // v50: the stoneworker's workshop no longer uses the masonry + // labor; stonecutters cut blocks there and stone carvers make + // everything else (masonry is now only for constructing + // buildings out of stone) + if (j->job_type == df::job_type::ConstructBlocks) + return df::unit_labor::STONECUTTER; + return df::unit_labor::STONE_CARVER; case df::workshop_type::Carpenters: return df::unit_labor::CARPENTER; case df::workshop_type::Leatherworks: @@ -581,9 +598,9 @@ class jlfunc_make : public jlfunc case df::workshop_type::MetalsmithsForge: return metaltype; default: - debug("LABORMANAGER: Cannot deduce labor for make job, workshop type %s\n", - ENUM_KEY_STR(workshop_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, workshop type {}\n", + ENUM_KEY_STR(workshop_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } } @@ -596,16 +613,16 @@ class jlfunc_make : public jlfunc case df::furnace_type::GlassFurnace: return df::unit_labor::GLASSMAKER; default: - debug("LABORMANAGER: Cannot deduce labor for make job, furnace type %s\n", - ENUM_KEY_STR(furnace_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, furnace type {}\n", + ENUM_KEY_STR(furnace_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } } - debug("LABORMANAGER: Cannot deduce labor for make job, building type %s\n", - ENUM_KEY_STR(building_type, bld->getType()).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, building type {}\n", + ENUM_KEY_STR(building_type, bld->getType())); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -685,9 +702,9 @@ JobLaborMapper::JobLaborMapper() jlfunc* jlf_no_labor = jlf_const(df::unit_labor::NONE); - job_to_labor_table[df::job_type::CarveFortification] = jlf_const(df::unit_labor::DETAIL); - job_to_labor_table[df::job_type::DetailWall] = jlf_const(df::unit_labor::DETAIL); - job_to_labor_table[df::job_type::DetailFloor] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::CarveFortification] = jlf_const(df::unit_labor::STONECUTTER); + job_to_labor_table[df::job_type::DetailWall] = jlf_const(df::unit_labor::ENGRAVER); + job_to_labor_table[df::job_type::DetailFloor] = jlf_const(df::unit_labor::ENGRAVER); job_to_labor_table[df::job_type::Dig] = jlf_const(df::unit_labor::MINE); job_to_labor_table[df::job_type::CarveUpwardStaircase] = jlf_const(df::unit_labor::MINE); job_to_labor_table[df::job_type::CarveDownwardStaircase] = jlf_const(df::unit_labor::MINE); @@ -719,19 +736,16 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::CatchLiveLandAnimal] = jlf_const(df::unit_labor::HUNT); job_to_labor_table[df::job_type::CatchLiveFish] = jlf_const(df::unit_labor::FISH); job_to_labor_table[df::job_type::ReturnKill] = jlf_no_labor; - job_to_labor_table[df::job_type::CheckChest] = jlf_no_labor; job_to_labor_table[df::job_type::StoreOwnedItem] = jlf_no_labor; job_to_labor_table[df::job_type::PlaceItemInTomb] = jlf_const(df::unit_labor::HAUL_BODY); job_to_labor_table[df::job_type::StoreItemInStockpile] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBag] = jlf_hauling; - job_to_labor_table[df::job_type::StoreItemInHospital] = jlf_hauling; job_to_labor_table[df::job_type::StoreWeapon] = jlf_hauling; job_to_labor_table[df::job_type::StoreArmor] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBarrel] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBin] = jlf_hauling; job_to_labor_table[df::job_type::SeekArtifact] = jlf_no_labor; job_to_labor_table[df::job_type::SeekInfant] = jlf_no_labor; - job_to_labor_table[df::job_type::AttendParty] = jlf_no_labor; job_to_labor_table[df::job_type::GoShopping] = jlf_no_labor; job_to_labor_table[df::job_type::GoShoppingSpecific] = jlf_no_labor; job_to_labor_table[df::job_type::Clean] = jlf_const(df::unit_labor::CLEAN); @@ -828,7 +842,6 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::LoadStoneTrap] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::LoadWeaponTrap] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::CleanTrap] = jlf_const(df::unit_labor::MECHANIC); - job_to_labor_table[df::job_type::CastSpell] = jlf_no_labor; job_to_labor_table[df::job_type::LinkBuildingToTrigger] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::PullLever] = jlf_const(df::unit_labor::PULL_LEVER); job_to_labor_table[df::job_type::ExtractFromPlants] = jlf_const(df::unit_labor::HERBALIST); @@ -893,7 +906,7 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::ApplyCast] = jlf_const(df::unit_labor::BONE_SETTING); job_to_labor_table[df::job_type::CustomReaction] = new jlfunc_custom(); job_to_labor_table[df::job_type::ConstructSlab] = jlf_make_furniture; - job_to_labor_table[df::job_type::EngraveSlab] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::EngraveSlab] = jlf_const(df::unit_labor::ENGRAVER); job_to_labor_table[df::job_type::ShearCreature] = jlf_const(df::unit_labor::SHEARER); job_to_labor_table[df::job_type::SpinThread] = jlf_const(df::unit_labor::SPINNER); job_to_labor_table[df::job_type::PenLargeAnimal] = jlf_const(df::unit_labor::HAUL_ANIMALS); @@ -907,7 +920,7 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::ReportCrime] = jlf_no_labor; job_to_labor_table[df::job_type::ExecuteCriminal] = jlf_no_labor; job_to_labor_table[df::job_type::TrainAnimal] = jlf_const(df::unit_labor::ANIMALTRAIN); - job_to_labor_table[df::job_type::CarveTrack] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::CarveTrack] = jlf_const(df::unit_labor::STONECUTTER); job_to_labor_table[df::job_type::PushTrackVehicle] = jlf_const(df::unit_labor::HANDLE_VEHICLES); job_to_labor_table[df::job_type::PlaceTrackVehicle] = jlf_const(df::unit_labor::HANDLE_VEHICLES); job_to_labor_table[df::job_type::StoreItemInVehicle] = jlf_hauling; @@ -927,8 +940,45 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::HeistItem] = jlf_no_labor; // added for 47.04 - see #1561 job_to_labor_table[df::job_type::InterrogateSubject] = jlf_no_labor; // added for 47.04 - see #1561 job_to_labor_table[df::job_type::AcceptHeistItem] = jlf_no_labor; // added for 47.04 - see #1561 + + // v50 additions + job_to_labor_table[df::job_type::SmoothWall] = jlf_const(df::unit_labor::STONECUTTER); + job_to_labor_table[df::job_type::SmoothFloor] = jlf_const(df::unit_labor::STONECUTTER); + job_to_labor_table[df::job_type::PolishStones] = jlf_const(df::unit_labor::STONE_CRAFT); + job_to_labor_table[df::job_type::ConstructBag] = jlf_const(df::unit_labor::LEATHER); + job_to_labor_table[df::job_type::EncrustWithStones] = jlf_const(df::unit_labor::ENCRUST_GEM); + job_to_labor_table[df::job_type::StoreSquadEquipmentItem] = jlf_no_labor; + job_to_labor_table[df::job_type::MixDye] = jlf_const(df::unit_labor::DYER); + job_to_labor_table[df::job_type::DyeLeather] = jlf_const(df::unit_labor::DYER); + job_to_labor_table[df::job_type::ConstructBoltThrowerParts] = jlf_const(df::unit_labor::SIEGECRAFT); + job_to_labor_table[df::job_type::LoadBoltThrower] = jlf_const(df::unit_labor::SIEGEOPERATE); + job_to_labor_table[df::job_type::FireBoltThrower] = jlf_const(df::unit_labor::SIEGEOPERATE); + job_to_labor_table[df::job_type::UNUSED_31] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_32] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_33] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_34] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_35] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_36] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_37] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_38] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_39] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_40] = jlf_no_labor; }; +void JobLaborMapper::job_coverage(std::vector &missing) +{ + FOR_ENUM_ITEMS(job_type, jt) + { + if (jt < 0) + continue; + // a job type with no table entry is invisible to labor management; + // DF releases that add job types must add a mapping (or jlf_no_labor) + if (!job_to_labor_table.count(jt)) + missing.push_back(ENUM_KEY_STR(job_type, jt) + + ": no entry in job to labor table"); + } +} + df::unit_labor JobLaborMapper::find_job_labor(df::job* j) { if (j->job_type == df::job_type::CustomReaction) @@ -948,8 +998,8 @@ df::unit_labor JobLaborMapper::find_job_labor(df::job* j) df::unit_labor labor; if (job_to_labor_table.count(j->job_type) == 0) { - debug("LABORMANAGER: job has no job to labor table entry: %s (%d)\n", ENUM_KEY_STR(job_type, j->job_type).c_str(), j->job_type); - debug_pause(); + jlm_debug("LABORMANAGER: job has no job to labor table entry: {} ({})\n", ENUM_KEY_STR(job_type, j->job_type), int(j->job_type)); + jlm_debug_pause(); labor = df::unit_labor::NONE; } else { diff --git a/plugins/autolabor/joblabormapper.h b/plugins/autolabor/joblabormapper.h index 03e06a67f4..a117de384c 100644 --- a/plugins/autolabor/joblabormapper.h +++ b/plugins/autolabor/joblabormapper.h @@ -1,15 +1,17 @@ #pragma once #include +#include +#include -using namespace DFHack; -using namespace df::enums; - #include "df/job.h" #include "df/job_type.h" #include "df/unit_labor.h" +using namespace DFHack; +using namespace df::enums; + class jlfunc; @@ -27,5 +29,9 @@ class JobLaborMapper { df::unit_labor find_job_labor(df::job* j); + // appends names of job_type enum items with no entry in the labor + // table; used by the job coverage self-test + void job_coverage(std::vector &missing); + }; diff --git a/plugins/autolabor/laborcommon.cpp b/plugins/autolabor/laborcommon.cpp new file mode 100644 index 0000000000..7c4a5ef1bc --- /dev/null +++ b/plugins/autolabor/laborcommon.cpp @@ -0,0 +1,451 @@ +#include "laborcommon.h" + +#include +#include +#include + +#include "joblabormapper.h" + +#include "Debug.h" + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; + +namespace DFHack { + DBG_DECLARE(autolabor, labor_probe, DebugCategory::LINFO); +} + +namespace autolabor { + +PersistentDataItem plugin_config; + +void load_plugin_config() +{ + plugin_config = World::GetPersistentSiteData("autolabor/config", true); + if (plugin_config.isValid() && plugin_config.ival(0) == -1) + plugin_config.ival(0) = 0; +} + +bool config_flag(unsigned flag) +{ + return plugin_config.isValid() && (plugin_config.ival(0) & flag) != 0; +} + +void set_config_flag(unsigned flag, bool on) +{ + if (!plugin_config.isValid()) + return; + if (on) + plugin_config.ival(0) |= flag; + else + plugin_config.ival(0) &= ~flag; +} + +int engine_mode() +{ + if (!plugin_config.isValid()) + return MODE_LEGACY; + int m = plugin_config.get_int(1); + return (m >= MODE_LEGACY && m <= MODE_MONITOR) ? m : MODE_LEGACY; +} + +void store_engine_mode(int mode) +{ + if (plugin_config.isValid()) + plugin_config.ival(1) = mode; +} + +int balance() +{ + if (!plugin_config.isValid()) + return NUM_BALANCE_STOPS / 2; + int v = plugin_config.get_int(2); + if (v < 0 || v >= NUM_BALANCE_STOPS) + v = NUM_BALANCE_STOPS / 2; // default: balanced + return v; +} + +void store_balance(int v) +{ + if (plugin_config.isValid() && v >= 0 && v < NUM_BALANCE_STOPS) + plugin_config.ival(2) = v; +} + +static const char * const balance_names[] = { + "staffing", "lean-staffing", "balanced", "lean-skills", "skills" +}; + +const char *balance_stop_name(int i) +{ + if (i < 0 || i >= NUM_BALANCE_STOPS) + return NULL; + return balance_names[i]; +} + +int idle_reserve() +{ + if (!plugin_config.isValid()) + return 30; + int v = plugin_config.get_int(3); + return (v < 0 || v > 100) ? 30 : v; +} + +void store_idle_reserve(int v) +{ + if (plugin_config.isValid() && v >= 0 && v <= 100) + plugin_config.ival(3) = v; +} + +df::job_skill labor_to_skill[NUM_LABORS]; + +void init_labor_to_skill() +{ + for (int i = 0; i < NUM_LABORS; i++) + labor_to_skill[i] = job_skill::NONE; + + FOR_ENUM_ITEMS(job_skill, skill) + { + int labor = ENUM_ATTR(job_skill, labor, skill); + if (labor >= 0 && labor < NUM_LABORS) + labor_to_skill[labor] = skill; + } +} + +bool is_exclusive_labor(df::unit_labor l) +{ + // labors that carry tools; toggling them causes equipment churn + switch (l) { + case unit_labor::MINE: + case unit_labor::CUTWOOD: + case unit_labor::HUNT: + return true; + default: + return false; + } +} + +BuildingScan scan_buildings() +{ + BuildingScan scan; + + for (auto build : world->buildings.all) + { + auto type = build->getType(); + if (building_type::Workshop == type) + { + df::workshop_type subType = (df::workshop_type)build->getSubtype(); + if (workshop_type::Butchers == subType) + scan.has_butchers = true; + if (workshop_type::Fishery == subType) + scan.has_fishery = true; + } + else if (building_type::TradeDepot == type) + { + df::building_tradedepotst* depot = + strict_virtual_cast(build); + if (depot) + scan.trader_requested = + scan.trader_requested || depot->trade_flags.bits.trader_requested; + } + } + + return scan; +} + +static const int responsibility_penalties[] = { + 0, /* LAW_MAKING */ + 0, /* LAW_ENFORCEMENT */ + 3000, /* RECEIVE_DIPLOMATS */ + 0, /* MEET_WORKERS */ + 1000, /* MANAGE_PRODUCTION */ + 3000, /* TRADE */ + 1000, /* ACCOUNTING */ + 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ + 0, /* MAKE_INTRODUCTIONS */ + 0, /* MAKE_PEACE_AGREEMENTS */ + 0, /* MAKE_TOPIC_AGREEMENTS */ + 0, /* COLLECT_TAXES */ + 0, /* ESCORT_TAX_COLLECTOR */ + 0, /* EXECUTIONS */ + 0, /* TAME_EXOTICS */ + 0, /* RELIGION */ + 0, /* ATTACK_ENEMIES */ + 0, /* PATROL_TERRITORY */ + 0, /* MILITARY_GOALS */ + 0, /* MILITARY_STRATEGY */ + 0, /* UPGRADE_SQUAD_EQUIPMENT */ + 0, /* EQUIPMENT_MANIFESTS */ + 0, /* SORT_AMMUNITION */ + 0, /* BUILD_MORALE */ + 5000 /* HEALTH_MANAGEMENT */ +}; + +PositionInfo unit_position_info(df::unit *u) +{ + PositionInfo info; + + df::historical_figure* hf = df::historical_figure::find(u->hist_figure_id); + if (!hf) // can be NULL, e.g. script-created citizens + return info; + + for (auto hfelink : hf->entity_links) + { + if (hfelink->getType() != df::histfig_entity_link_type::POSITION) + continue; + + df::histfig_entity_link_positionst *epos = + (df::histfig_entity_link_positionst*) hfelink; + df::historical_entity* entity = df::historical_entity::find(epos->entity_id); + if (!entity) + continue; + df::entity_position_assignment* assignment = + binsearch_in_vector(entity->positions.assignments, epos->assignment_id); + if (!assignment) + continue; + df::entity_position* position = + binsearch_in_vector(entity->positions.own, assignment->position_id); + if (!position) + continue; + + for (int n = 0; n < 25; n++) + if (position->responsibilities[n]) + info.noble_penalty += responsibility_penalties[n]; + + if (position->responsibilities[df::entity_position_responsibility::HEALTH_MANAGEMENT]) + info.medical = true; + + if (position->responsibilities[df::entity_position_responsibility::TRADE]) + info.trader = true; + } + + return info; +} + +bool in_diplomacy_meeting(df::unit *u) +{ + for (auto act : plotinfo->activities) + { + if (!act) continue; + if (act->unit_actor == u->id || act->unit_noble == u->id) + return true; + } + return false; +} + +bool is_assignable(df::unit *u) +{ + return Units::isOwnCiv(u) && + Units::isOwnGroup(u) && + Units::isActive(u) && + !u->flags2.bits.visitor && + !u->flags3.bits.ghostly && + ENUM_ATTR(profession, can_assign_labor, u->profession); +} + +dwarf_state get_dwarf_state(df::unit *u) +{ + if (Units::isBaby(u) || Units::isChild(u) || + u->profession == profession::DRUNK) + return dwarf_state::CHILD; + + if (ENUM_ATTR(profession, military, u->profession)) + return dwarf_state::MILITARY; + + if (!u->job.current_job) + { + if (Units::getMiscTrait(u, misc_trait_type::Migrant) || + u->specific_refs.size() > 0) + return dwarf_state::OTHER; + return dwarf_state::IDLE; + } + + int job = u->job.current_job->job_type; + if (job >= 0 && size_t(job) < dwarf_state_count) + return dwarf_states[job]; + + return dwarf_state::OTHER; +} + +int top_skill(df::unit *u, df::job_skill *skill_out) +{ + int best = 0; + df::job_skill best_skill = job_skill::NONE; + + if (u->status.souls.size() > 0) + { + for (auto skill : u->status.souls[0]->skills) + { + df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); + if (skill_class != job_skill_class::Normal && + skill_class != job_skill_class::Medical) + continue; + + if (best < skill->rating) + { + best = skill->rating; + best_skill = skill->id; + } + } + } + + if (skill_out) + *skill_out = best_skill; + return best; +} + +int total_skill(df::unit *u) +{ + int total = 0; + if (u->status.souls.size() > 0) + { + for (auto skill : u->status.souls[0]->skills) + { + df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); + if (skill_class != job_skill_class::Normal && + skill_class != job_skill_class::Medical) + continue; + total += skill->rating; + } + } + return total; +} + +// --------------------------------------------------------------------------- +// labor probe +// +// The v50 stoneworker labor split (stonecutter / stone carver / mason / +// engraver / stonecrafter) was re-derived from documentation rather than +// verified against the game. The game only lets a unit claim a job if it +// has the job's true gating labor enabled, so logging which of the disputed +// labors are enabled on each observed worker lets A/B testing identify the +// real mapping (e.g. a unit making rock blocks with STONECUTTER on and +// MASON off confirms the mapping; an enabled mask that consistently +// disagrees with `mapped` flags a wrong guess). +// --------------------------------------------------------------------------- + +const std::vector disputed_labors = { + df::unit_labor::STONECUTTER, + df::unit_labor::STONE_CARVER, + df::unit_labor::MASON, + df::unit_labor::ENGRAVER, + df::unit_labor::STONE_CRAFT, +}; + +static JobLaborMapper *probe_mapper = nullptr; +// (job id, unit id) -> enabled-labor bitmask; only new or changed +// combinations are logged so the channel doesn't spam every cycle +static std::map, int> probe_seen; +// (unit id, job type) pairs already warned about for a mapped-labor +// mismatch; each unit/job-type combination warns once +static std::set> probe_warned; + +void probe_labor_observation(df::unit *u) +{ + df::job *j = u->job.current_job; + if (!j) + return; + if (!probe_mapper) + probe_mapper = new JobLaborMapper(); + + df::unit_labor mapped = probe_mapper->find_job_labor(j); + + // strong mismatch signal independent of the disputed set: the unit is + // doing the job without the labor we mapped it to. the game gates job + // claiming on the real labor, so either our mapping is wrong or the job + // accepts multiple labors and we picked the wrong one -- either way the + // unit's enabled labors hold the better candidate. (a claimed job can + // also outlive a labor removal, so this is only a *possible* mismatch) + if (mapped > unit_labor::NONE && mapped < NUM_LABORS && + !u->status.labors[mapped]) + { + auto wkey = std::make_pair(u->id, (int32_t)j->job_type); + if (!probe_warned.count(wkey)) + { + probe_warned.insert(wkey); + std::stringstream es; + bool comma = false; + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE || !u->status.labors[l]) + continue; + if (comma) + es << ','; + es << ENUM_KEY_STR(unit_labor, l); + comma = true; + } + INFO(labor_probe).print( + "possible labor mapping mismatch: unit {} ({}) is doing job " + "{} {} (mapped {}) without that labor enabled; enabled " + "labors: {{{}}}\n", + u->id, Units::getReadableName(u), j->id, + ENUM_KEY_STR(job_type, j->job_type), + ENUM_KEY_STR(unit_labor, mapped), + comma ? es.str() : std::string("NONE")); + } + } + + const int n_disputed = disputed_labors.size(); + int mask = 0; + bool in_cluster = false; + for (int i = 0; i < n_disputed; i++) + { + if (disputed_labors[i] == mapped) + in_cluster = true; + if (u->status.labors[disputed_labors[i]]) + mask |= 1 << i; + } + if (!in_cluster) + return; + + auto key = std::make_pair(j->id, u->id); + auto seen = probe_seen.find(key); + if (seen != probe_seen.end() && seen->second == mask) + return; + probe_seen[key] = mask; + + std::stringstream ss; + bool comma = false; + for (int i = 0; i < n_disputed; i++) + { + if (!(mask & (1 << i))) + continue; + if (comma) + ss << ','; + ss << ENUM_KEY_STR(unit_labor, disputed_labors[i]); + comma = true; + } + if (!comma) + ss << "NONE"; + + TRACE(labor_probe).print( + "unit {} ({}) job {} {}: mapped={} enabled={{{}}}\n", + u->id, Units::getReadableName(u), j->id, + ENUM_KEY_STR(job_type, j->job_type), + ENUM_KEY_STR(unit_labor, mapped), ss.str()); +} + +} diff --git a/plugins/autolabor/laborcommon.h b/plugins/autolabor/laborcommon.h new file mode 100644 index 0000000000..56a823c538 --- /dev/null +++ b/plugins/autolabor/laborcommon.h @@ -0,0 +1,124 @@ +#pragma once + +#include "laborstatemap.h" + +#include +#include + +#include "modules/Persistence.h" + +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +// Shared infrastructure for the autolabor plugin's two engines. + +namespace autolabor { + +// --------------------------------------------------------------------------- +// plugin-global persisted config ("autolabor/config" site data) +// ival(0): flags ival(1): mode ival(2): balance stop (0-4) +// ival(3): idle reserve pct +// --------------------------------------------------------------------------- + +extern PersistentDataItem plugin_config; +void load_plugin_config(); + +enum ConfigFlags { + CF_ENABLED = 1, + CF_ALLOW_FISHING = 2, + CF_ALLOW_HUNTING = 4, +}; + +enum EngineMode { + MODE_LEGACY = 0, // classic autolabor: writes unit->status.labors directly + MODE_MODERN = 1, // labormanager: works through the work detail system + MODE_MONITOR = 2, // starvation warnings only; no labor management +}; + +bool config_flag(unsigned flag); +void set_config_flag(unsigned flag, bool on); + +int engine_mode(); +void store_engine_mode(int mode); + +int balance(); // 0..4 slider stop; 0 = max staffing +void store_balance(int v); +int idle_reserve(); // percent idle time reserved for guilded specialists +void store_idle_reserve(int v); + +const int NUM_BALANCE_STOPS = 5; +// CLI/overlay label for a balance stop; NULL if out of range +const char *balance_stop_name(int i); + +// --------------------------------------------------------------------------- + +const int NUM_LABORS = ENUM_LAST_ITEM(unit_labor) + 1; + +// labor_to_skill[l] is the job_skill trained by labor l, or job_skill::NONE +// for labors that require no skill. +extern df::job_skill labor_to_skill[]; +void init_labor_to_skill(); + +inline bool is_unskilled(df::unit_labor l) { + return l >= 0 && l < NUM_LABORS && labor_to_skill[l] == job_skill::NONE; +} + +// does the labor require a tool (and thus churn equipment when toggled)? +bool is_exclusive_labor(df::unit_labor l); + +struct BuildingScan { + bool has_butchers = false; + bool has_fishery = false; + bool trader_requested = false; +}; +BuildingScan scan_buildings(); + +struct PositionInfo { + int noble_penalty = 0; + bool medical = false; + bool trader = false; +}; +// noble status, medical/trade responsibilities from entity positions +PositionInfo unit_position_info(df::unit *u); + +// is the unit currently involved in a diplomatic/noble meeting? +bool in_diplomacy_meeting(df::unit *u); + +// --------------------------------------------------------------------------- +// labor coverage self-checks +// --------------------------------------------------------------------------- + +// Appends names of unit_labor enum items that the engine's static labor +// data doesn't cover (e.g. a labor added by a new DF release that has no +// entry in a fixed-size table). Used by the autolabor_checkLaborCoverage +// lua function; an empty result means every labor is handled. +void legacy_labor_coverage(std::vector &missing); +void modern_labor_coverage(std::vector &missing); + +// can the unit take labors at all (own civ/group, active, not visitor/ghost)? +bool is_assignable(df::unit *u); + +// classify what the unit is currently doing +dwarf_state get_dwarf_state(df::unit *u); + +// highest rating in a Normal or Medical skill; also reports the skill id +int top_skill(df::unit *u, df::job_skill *skill_out = nullptr); +int total_skill(df::unit *u); + +// labors whose v50 job->labor mappings were re-derived rather than verified +// against the game (the stoneworker split); the labor probe and the modern +// engine's mismatch escalation both work from this list +extern const std::vector disputed_labors; + +// labor-probe instrumentation: if the unit is doing a job that maps to a +// labor whose job->labor mapping is disputed, log which of the disputed +// labors are enabled on the unit (via the "labor_probe" debug channel). +// the true gating labor must be enabled on the worker, so comparing +// observations across units identifies the labor the game actually uses. +void probe_labor_observation(df::unit *u); + +} diff --git a/plugins/autolabor/labormanager.cpp b/plugins/autolabor/labormanager.cpp deleted file mode 100644 index f72be79913..0000000000 --- a/plugins/autolabor/labormanager.cpp +++ /dev/null @@ -1,2145 +0,0 @@ -/* -* Labor manager (formerly Autolabor 2) module for dfhack -* -* */ - - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "modules/Units.h" -#include "modules/World.h" -#include "modules/Maps.h" -#include "modules/MapCache.h" -#include "modules/Items.h" - -// DF data structure definition headers -#include "DataDefs.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "labormanager.h" -#include "joblabormapper.h" - -#include "laborstatemap.h" - -using namespace std; -using std::string; -using std::endl; -using namespace DFHack; -using namespace df::enums; -using df::global::plotinfo; -using df::global::world; - -#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) - -DFHACK_PLUGIN_IS_ENABLED(enable_labormanager); - -static bool print_debug = 0; -static bool pause_on_error = 1; - -static std::vector state_count(5); - -static PersistentDataItem config; - -enum ConfigFlags { - CF_ENABLED = 1, - CF_ALLOW_FISHING = 2, - CF_ALLOW_HUNTING = 4, -}; - -// Value of 0 for max dwarfs means uncapped. -const int MAX_DWARFS_NONE = 0; -// Value < 0 for max dwarfs means don't manager the labor. -const int MAX_DWARFS_UNMANAGED = -1; - - -// Here go all the command declarations... -// mostly to allow having the mandatory stuff on top of the file and commands on the bottom -command_result labormanager(color_ostream &out, std::vector & parameters); - -// A plugin must be able to return its name and version. -// The name string provided must correspond to the filename - labormanager.plug.so or labormanager.plug.dll in this case -DFHACK_PLUGIN("labormanager"); - -static void generate_labor_to_skill_map(); - -struct labor_info -{ - PersistentDataItem config; - - int active_dwarfs; - int idle_dwarfs; - int busy_dwarfs; - - int priority() const { return config.ival(1); } - void set_priority(int priority) { config.ival(1) = priority; } - - bool is_unmanaged() const { return maximum_dwarfs() == MAX_DWARFS_UNMANAGED; } - int maximum_dwarfs() const { return config.ival(2); } - void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } - - int time_since_last_assigned() const - { - return (*df::global::cur_year - config.ival(3)) * 403200 + *df::global::cur_year_tick - config.ival(4); - } - - void mark_assigned() { - config.ival(3) = (*df::global::cur_year); - config.ival(4) = (*df::global::cur_year_tick); - } - -}; - -enum tools_enum { - TOOL_NONE, TOOL_PICK, TOOL_AXE, TOOL_CROSSBOW, - TOOLS_MAX -}; - -struct labor_default -{ - int priority; - int maximum_dwarfs; - tools_enum tool; -}; - -static std::vector labor_infos; - -static const struct labor_default default_labor_infos[] = { - /* MINE */ {100, 0, TOOL_PICK}, - /* HAUL_STONE */ {100, 0, TOOL_NONE}, - /* HAUL_WOOD */ {100, 0, TOOL_NONE}, - /* HAUL_BODY */ {1000, 0, TOOL_NONE}, - /* HAUL_FOOD */ {500, 0, TOOL_NONE}, - /* HAUL_REFUSE */ {200, 0, TOOL_NONE}, - /* HAUL_ITEM */ {100, 0, TOOL_NONE}, - /* HAUL_FURNITURE */ {100, 0, TOOL_NONE}, - /* HAUL_ANIMAL */ {100, 0, TOOL_NONE}, - /* CLEAN */ {100, 0, TOOL_NONE}, - /* CUTWOOD */ {100, 0, TOOL_AXE}, - /* CARPENTER */ {100, 0, TOOL_NONE}, - /* DETAIL */ {100, 0, TOOL_NONE}, - /* MASON */ {100, 0, TOOL_NONE}, - /* ARCHITECT */ {100, 0, TOOL_NONE}, - /* ANIMALTRAIN */ {100, 0, TOOL_NONE}, - /* ANIMALCARE */ {100, 0, TOOL_NONE}, - /* DIAGNOSE */ {1000, 0, TOOL_NONE}, - /* SURGERY */ {1000, 0, TOOL_NONE}, - /* BONE_SETTING */ {1000, 0, TOOL_NONE}, - /* SUTURING */ {1000, 0, TOOL_NONE}, - /* DRESSING_WOUNDS */ {1000, 0, TOOL_NONE}, - /* FEED_WATER_CIVILIANS */ {1000, 0, TOOL_NONE}, - /* RECOVER_WOUNDED */ {200, 0, TOOL_NONE}, - /* BUTCHER */ {500, 0, TOOL_NONE}, - /* TRAPPER */ {100, 0, TOOL_NONE}, - /* DISSECT_VERMIN */ {100, 0, TOOL_NONE}, - /* LEATHER */ {100, 0, TOOL_NONE}, - /* TANNER */ {100, 0, TOOL_NONE}, - /* BREWER */ {100, 0, TOOL_NONE}, - /* ALCHEMIST */ {100, 0, TOOL_NONE}, - /* SOAP_MAKER */ {100, 0, TOOL_NONE}, - /* WEAVER */ {100, 0, TOOL_NONE}, - /* CLOTHESMAKER */ {100, 0, TOOL_NONE}, - /* MILLER */ {100, 0, TOOL_NONE}, - /* PROCESS_PLANT */ {100, 0, TOOL_NONE}, - /* MAKE_CHEESE */ {100, 0, TOOL_NONE}, - /* MILK */ {100, 0, TOOL_NONE}, - /* COOK */ {100, 0, TOOL_NONE}, - /* PLANT */ {100, 0, TOOL_NONE}, - /* HERBALIST */ {100, 0, TOOL_NONE}, - /* FISH */ {100, 0, TOOL_NONE}, - /* CLEAN_FISH */ {100, 0, TOOL_NONE}, - /* DISSECT_FISH */ {100, 0, TOOL_NONE}, - /* HUNT */ {100, 0, TOOL_CROSSBOW}, - /* SMELT */ {100, 0, TOOL_NONE}, - /* FORGE_WEAPON */ {100, 0, TOOL_NONE}, - /* FORGE_ARMOR */ {100, 0, TOOL_NONE}, - /* FORGE_FURNITURE */ {100, 0, TOOL_NONE}, - /* METAL_CRAFT */ {100, 0, TOOL_NONE}, - /* CUT_GEM */ {100, 0, TOOL_NONE}, - /* ENCRUST_GEM */ {100, 0, TOOL_NONE}, - /* WOOD_CRAFT */ {100, 0, TOOL_NONE}, - /* STONE_CRAFT */ {100, 0, TOOL_NONE}, - /* BONE_CARVE */ {100, 0, TOOL_NONE}, - /* GLASSMAKER */ {100, 0, TOOL_NONE}, - /* EXTRACT_STRAND */ {100, 0, TOOL_NONE}, - /* SIEGECRAFT */ {100, 0, TOOL_NONE}, - /* SIEGEOPERATE */ {100, 0, TOOL_NONE}, - /* BOWYER */ {100, 0, TOOL_NONE}, - /* MECHANIC */ {100, 0, TOOL_NONE}, - /* POTASH_MAKING */ {100, 0, TOOL_NONE}, - /* LYE_MAKING */ {100, 0, TOOL_NONE}, - /* DYER */ {100, 0, TOOL_NONE}, - /* BURN_WOOD */ {100, 0, TOOL_NONE}, - /* OPERATE_PUMP */ {100, 0, TOOL_NONE}, - /* SHEARER */ {100, 0, TOOL_NONE}, - /* SPINNER */ {100, 0, TOOL_NONE}, - /* POTTERY */ {100, 0, TOOL_NONE}, - /* GLAZING */ {100, 0, TOOL_NONE}, - /* PRESSING */ {100, 0, TOOL_NONE}, - /* BEEKEEPING */ {100, 0, TOOL_NONE}, - /* WAX_WORKING */ {100, 0, TOOL_NONE}, - /* PUSH_HAUL_VEHICLES */ {100, 0, TOOL_NONE}, - /* HAUL_TRADE */ {1000, 0, TOOL_NONE}, - /* PULL_LEVER */ {1000, 0, TOOL_NONE}, - /* UNUSED_13 */ {0, 0, TOOL_NONE}, - /* HAUL_WATER */ {100, 0, TOOL_NONE}, - /* GELD */ {100, 0, TOOL_NONE}, - /* BUILD_ROAD */ {100, 0, TOOL_NONE}, - /* BUILD_CONSTRUCTION */ {100, 0, TOOL_NONE}, - /* PAPERMAKING */ {100, 0, TOOL_NONE}, - /* BOOKBINDING */ {100, 0, TOOL_NONE} -}; - - - -struct dwarf_info_t -{ - df::unit* dwarf; - dwarf_state state; - - bool clear_all; - - bool has_tool[TOOLS_MAX]; - - int high_skill; - - bool has_children; - bool armed; - - int unmanaged_labors_assigned; - - df::unit_labor using_labor; - - dwarf_info_t(df::unit* dw) : dwarf(dw), state(OTHER), - clear_all(false), high_skill(0), has_children(false), armed(false), - unmanaged_labors_assigned(0), using_labor(df::unit_labor::NONE) - { - for (int e = TOOL_NONE; e < TOOLS_MAX; e++) - has_tool[e] = false; - } - - ~dwarf_info_t() - { - } - -}; - -color_ostream* debug_stream; - -void debug(const char* fmt, ...) -{ - if (debug_stream) - { - va_list args; - va_start(args, fmt); - debug_stream->vprint(fmt, args); - va_end(args); - } -} - -void debug_pause() -{ - if (pause_on_error) - { - debug("LABORMANAGER: Game paused so you can investigate the above message.\nUse 'labormanager pause-on-error no' to disable autopausing.\n"); - *df::global::pause_state = true; - } -} - -static JobLaborMapper* labor_mapper = 0; - -static bool initialized = false; - -static bool isOptionEnabled(unsigned flag) -{ - return config.isValid() && (config.ival(0) & flag) != 0; -} - -static void setOptionEnabled(ConfigFlags flag, bool on) -{ - if (!config.isValid()) - return; - - if (on) - config.ival(0) |= flag; - else - config.ival(0) &= ~flag; -} - -static void cleanup_state() -{ - enable_labormanager = false; - labor_infos.clear(); - initialized = false; -} - -static void reset_labor(df::unit_labor labor) -{ - labor_infos[labor].set_priority(default_labor_infos[labor].priority); - labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); -} - -static void init_state() -{ - config = World::GetPersistentData("labormanager/2.0/config"); - if (config.isValid() && config.ival(0) == -1) - config.ival(0) = 0; - - enable_labormanager = isOptionEnabled(CF_ENABLED); - - if (!enable_labormanager) - return; - - // Load labors from save - labor_infos.resize(ARRAY_COUNT(default_labor_infos)); - - std::vector items; - World::GetPersistentData(&items, "labormanager/2.0/labors/", true); - - for (auto p = items.begin(); p != items.end(); p++) - { - string key = p->key(); - df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("labormanager/2.0/labors/")).c_str()); - if (labor >= 0 && size_t(labor) < labor_infos.size()) - { - labor_infos[labor].config = *p; - labor_infos[labor].active_dwarfs = 0; - } - } - - // Add default labors for those not in save - for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { - if (labor_infos[i].config.isValid()) - continue; - - std::stringstream name; - name << "labormanager/2.0/labors/" << i; - - labor_infos[i].config = World::AddPersistentData(name.str()); - labor_infos[i].mark_assigned(); - labor_infos[i].active_dwarfs = 0; - reset_labor((df::unit_labor) i); - } - - initialized = true; - -} - -static df::job_skill labor_to_skill[ENUM_LAST_ITEM(unit_labor) + 1]; - -static void generate_labor_to_skill_map() -{ - // Generate labor -> skill mapping - - for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++) - labor_to_skill[i] = job_skill::NONE; - - FOR_ENUM_ITEMS(job_skill, skill) - { - int labor = ENUM_ATTR(job_skill, labor, skill); - if (labor != unit_labor::NONE) - { - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_to_skill)); - */ - - labor_to_skill[labor] = skill; - } - } -} - -struct skill_attr_weight { - int phys_attr_weights[6]; - int mental_attr_weights[13]; -}; - -static struct skill_attr_weight skill_attr_weights[ENUM_LAST_ITEM(job_skill) + 1] = -{ - // S A T E R D AA F W C I P M LA SS M KS E SA - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MINING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WOODCUTTING */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CARPENTRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ENGRAVE_STONE */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MASONRY */, - { { 0, 1, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0 } } /* ANIMALTRAIN */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 } } /* ANIMALCARE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISSECT_FISH */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISSECT_VERMIN */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROCESSFISH */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BUTCHER */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* TRAPPING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* TANNER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WEAVING */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BREWING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ALCHEMY */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CLOTHESMAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILLING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROCESSPLANTS */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CHEESEMAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILK */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* COOK */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLANT */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* HERBALISM */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 } } /* FISH */, - { { 1, 0, 1, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SMELT */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* EXTRACT_STRAND */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_WEAPON */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_ARMOR */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_FURNITURE */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CUTGEM */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ENCRUSTGEM */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WOODCRAFT */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* STONECRAFT */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* METALCRAFT */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GLASSMAKER */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* LEATHERWORK */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BONECARVE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* AXE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SWORD */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DAGGER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MACE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* HAMMER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SPEAR */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CROSSBOW */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SHIELD */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* ARMOR */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SIEGECRAFT */, - { { 1, 0, 1, 1, 0, 0 }, { 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SIEGEOPERATE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BOWYER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* PIKE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WHIP */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BOW */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BLOWGUN */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* THROW */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MECHANICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MAGIC_NATURE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SNEAK */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DESIGNBUILDING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0 } } /* DRESS_WOUNDS */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0 } } /* DIAGNOSE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SURGERY */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SET_BONE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SUTURE */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CRUTCH_WALK */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* WOOD_BURNING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* LYE_MAKING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SOAP_MAKING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* POTASH_MAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DYER */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* OPERATE_PUMP */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SWIMMING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* PERSUASION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* NEGOTIATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1 } } /* JUDGING_INTENT */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0 } } /* APPRAISAL */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 } } /* ORGANIZATION */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* RECORD_KEEPING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 } } /* LYING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 } } /* INTIMIDATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* CONVERSATION */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0 } } /* COMEDY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* FLATTERY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0 } } /* CONSOLE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* PACIFY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* TRACKING */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* KNOWLEDGE_ACQUISITION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 } } /* CONCENTRATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISCIPLINE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SITUATIONAL_AWARENESS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* WRITING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROSE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* POETRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* READING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SPEAKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* COORDINATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BALANCE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* LEADERSHIP */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* TEACHING */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MELEE_COMBAT */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* RANGED_COMBAT */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WRESTLING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BITE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GRASP_STRIKE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* STANCE_STRIKE */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DODGING */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MISC_WEAPON */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* KNAPPING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILITARY_TACTICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SHEARING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SPINNING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* POTTERY */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GLAZING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PRESSING */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BEEKEEPING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WAX_WORKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CLIMBING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* GELD */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DANCE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MAKE_MUSIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SING_MUSIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_KEYBOARD_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_STRINGED_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_WIND_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_PERCUSSION_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CRITICAL_THINKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* LOGIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MATHEMATICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ASTRONOMY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CHEMISTRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* GEOGRAPHY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* OPTICS_ENGINEER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FLUID_ENGINEER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PAPERMAKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BOOKBINDING */ -}; - -static void enable_plugin(color_ostream &out) -{ - if (!config.isValid()) - { - config = World::AddPersistentData("labormanager/2.0/config"); - config.ival(0) = 0; - } - - setOptionEnabled(CF_ENABLED, true); - enable_labormanager = true; - out << "Enabling the plugin." << endl; - - cleanup_state(); - init_state(); -} - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) -{ - // initialize labor infos table from default table - if (ARRAY_COUNT(default_labor_infos) != ENUM_LAST_ITEM(unit_labor) + 1) - return CR_FAILURE; - - // Fill the command list with your commands. - commands.push_back(PluginCommand( - "labormanager", - "Automatically manage dwarf labors.", - labormanager)); - - generate_labor_to_skill_map(); - - labor_mapper = new JobLaborMapper(); - - init_state(); - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream &out) -{ - cleanup_state(); - - delete labor_mapper; - - return CR_OK; -} - -class AutoLaborManager { - color_ostream& out; - -public: - AutoLaborManager(color_ostream& o) : out(o) - { - dwarf_info.clear(); - } - - ~AutoLaborManager() - { - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - delete (*d); - } - } - - dwarf_info_t* add_dwarf(df::unit* u) - { - dwarf_info_t* dwarf = new dwarf_info_t(u); - dwarf_info.push_back(dwarf); - return dwarf; - } - - -private: - bool has_butchers; - bool has_fishery; - bool trader_requested; - - int dig_count; - int tree_count; - int plant_count; - int detail_count; - - bool labors_changed; - - int tool_count[TOOLS_MAX]; - int tool_in_use[TOOLS_MAX]; - - int cnt_recover_wounded; - int cnt_diagnosis; - int cnt_immobilize; - int cnt_dressing; - int cnt_cleaning; - int cnt_surgery; - int cnt_suture; - int cnt_setting; - int cnt_traction; - int cnt_crutch; - - int need_food_water; - - int priority_food; - - std::map labor_needed; - std::map labor_in_use; - std::map labor_outside; - std::vector dwarf_info; - std::list available_dwarfs; - std::list busy_dwarfs; - -private: - void set_labor(dwarf_info_t* dwarf, df::unit_labor labor, bool value) - { - if (labor >= 0 && labor <= ENUM_LAST_ITEM(unit_labor) && !labor_infos[labor].is_unmanaged()) - { - if (!Units::isValidLabor(dwarf->dwarf, labor)) - { - debug("WARN(labormanager): Attempted to %s dwarf %s with ineligible labor %s\n", - value ? "set" : "unset", - dwarf->dwarf->name.first_name.c_str(), - ENUM_KEY_STR(unit_labor, labor).c_str()); - return; - } - bool old = dwarf->dwarf->status.labors[labor]; - dwarf->dwarf->status.labors[labor] = value; - if (old != value) - { - labors_changed = true; - - tools_enum tool = default_labor_infos[labor].tool; - if (tool != TOOL_NONE) - tool_in_use[tool] += value ? 1 : -1; - } - } - } - - void process_job(df::job* j) - { - if (j->flags.bits.suspend || j->flags.bits.item_lost) - return; - - int worker = -1; - int bld = -1; - - for (auto ref : j->general_refs) - { - if (ref->getType() == df::general_ref_type::UNIT_WORKER) - worker = ((df::general_ref_unit_workerst *)ref)->unit_id; - if (ref->getType() == df::general_ref_type::BUILDING_HOLDER) - bld = ((df::general_ref_building_holderst *)ref)->building_id; - } - - if (bld != -1) - { - df::building* b = binsearch_in_vector(world->buildings.all, bld); - - // check if this job is the first nonsuspended job on this building; if not, ignore it - // (except for farms and trade depots) - - if (b->getType() != df::building_type::FarmPlot && - b->getType() != df::building_type::TradeDepot) - { - int fjid = -1; - for (size_t jn = 0; jn < b->jobs.size(); jn++) - { - if (b->jobs[jn]->flags.bits.suspend) - continue; - fjid = b->jobs[jn]->id; - break; - } - if (fjid != j->id) - return; - } - } - - df::unit_labor labor = labor_mapper->find_job_labor(j); - - if (labor != df::unit_labor::NONE && !labor_infos[labor].is_unmanaged()) - { - labor_needed[labor]++; - if (worker == -1) - { - if (j->pos.isValid()) - { - df::tile_designation* d = Maps::getTileDesignation(j->pos); - if (d->bits.outside) - labor_outside[labor] = true; - } - } - else { - labor_infos[labor].mark_assigned(); - labor_in_use[labor]++; - } - - } - } - - void scan_buildings() - { - has_butchers = false; - has_fishery = false; - trader_requested = false; - labors_changed = false; - - for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++) - { - df::building *build = *b; - auto type = build->getType(); - if (building_type::Workshop == type) - { - df::workshop_type subType = (df::workshop_type)build->getSubtype(); - if (workshop_type::Butchers == subType) - has_butchers = true; - if (workshop_type::Fishery == subType) - has_fishery = true; - } - else if (building_type::TradeDepot == type) - { - df::building_tradedepotst* depot = (df::building_tradedepotst*) build; - trader_requested = depot->trade_flags.bits.trader_requested; - - if (print_debug) - { - if (trader_requested) - out.print("Trade depot found and trader requested, trader will be excluded from all labors.\n"); - else - out.print("Trade depot found but trader is not requested.\n"); - } - - } - } - } - - void count_map_designations() - { - dig_count = 0; - tree_count = 0; - plant_count = 0; - detail_count = 0; - - for (size_t i = 0; i < world->map.map_blocks.size(); ++i) - { - df::map_block* bl = world->map.map_blocks[i]; - - if (!bl->flags.bits.designated) - continue; - - for (int x = 0; x < 16; x++) - for (int y = 0; y < 16; y++) - { - if (bl->designation[x][y].bits.hidden) - { - df::coord p = bl->map_pos; - if (! Maps::isTileVisible(p.x, p.y, p.z-1)) - continue; - } - - df::tile_dig_designation dig = bl->designation[x][y].bits.dig; - if (dig != df::enums::tile_dig_designation::No) - { - df::tiletype tt = bl->tiletype[x][y]; - df::tiletype_material ttm = ENUM_ATTR(tiletype, material, tt); - df::tiletype_shape tts = ENUM_ATTR(tiletype, shape, tt); - if (ttm == df::enums::tiletype_material::TREE) - tree_count++; - else if (tts == df::enums::tiletype_shape::SHRUB) - plant_count++; - else - dig_count++; - } - if (bl->designation[x][y].bits.smooth != 0) - detail_count++; - } - } - - if (print_debug) - out.print("Dig count = %d, Cut tree count = %d, gather plant count = %d, detail count = %d\n", dig_count, tree_count, plant_count, detail_count); - - } - - void count_tools() - { - for (int e = TOOL_NONE; e < TOOLS_MAX; e++) - { - tool_count[e] = 0; - tool_in_use[e] = 0; - } - - priority_food = 0; - - df::item_flags bad_flags; - bad_flags.whole = 0; - -#define F(x) bad_flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); -#undef F - - for (auto item : world->items.other.IN_PLAY) { - if (item->flags.bits.dump && !labor_infos[df::unit_labor::HAUL_REFUSE].is_unmanaged()) - labor_needed[df::unit_labor::HAUL_REFUSE]++; - - if (item->flags.whole & bad_flags.whole) - continue; - - df::item_type t = item->getType(); - - if (item->materialRots() && t != df::item_type::CORPSEPIECE && t != df::item_type::CORPSE && item->getRotTimer() > 1) - priority_food++; - - if (!item->isWeapon()) - continue; - - df::itemdef_weaponst* weapondef = ((df::item_weaponst*)item)->subtype; - df::job_skill weaponsk = (df::job_skill) weapondef->skill_melee; - df::job_skill weaponsk2 = (df::job_skill) weapondef->skill_ranged; - if (weaponsk == df::job_skill::AXE) - tool_count[TOOL_AXE]++; - else if (weaponsk == df::job_skill::MINING) - tool_count[TOOL_PICK]++; - else if (weaponsk2 == df::job_skill::CROSSBOW) - tool_count[TOOL_CROSSBOW]++; - } - - } - - void collect_job_list() - { - for (df::job_list_link* jll = world->jobs.list.next; jll; jll = jll->next) - { - df::job* j = jll->item; - if (!j) - continue; - process_job(j); - } - - for (auto jp = world->jobs.postings.begin(); jp != world->jobs.postings.end(); jp++) - { - if ((*jp)->flags.bits.dead) - continue; - - process_job((*jp)->job); - } - - } - - void collect_dwarf_list() - { - state_count.clear(); - state_count.resize(NUM_STATE); - - for (auto u = world->units.active.begin(); u != world->units.active.end(); ++u) - { - df::unit* cre = *u; - - // following tests shamelessly stolen from Dwarf Manipulator plugin - - bool isAssignable = - (Units::isOwnCiv(cre)) && - (Units::isOwnGroup(cre)) && - (Units::isActive(cre)) && - (!cre->flags2.bits.visitor) && - (!cre->flags3.bits.ghostly) && - (ENUM_ATTR(profession, can_assign_labor, cre->profession)); - - if (isAssignable) - { - dwarf_info_t* dwarf = add_dwarf(cre); - - df::historical_figure* hf = df::historical_figure::find(dwarf->dwarf->hist_figure_id); - for (size_t i = 0; i < hf->entity_links.size(); i++) - { - df::histfig_entity_link* hfelink = hf->entity_links.at(i); - if (hfelink->getType() == df::histfig_entity_link_type::POSITION) - { - df::histfig_entity_link_positionst *epos = - (df::histfig_entity_link_positionst*) hfelink; - df::historical_entity* entity = df::historical_entity::find(epos->entity_id); - if (!entity) - continue; - df::entity_position_assignment* assignment = binsearch_in_vector(entity->positions.assignments, epos->assignment_id); - if (!assignment) - continue; - df::entity_position* position = binsearch_in_vector(entity->positions.own, assignment->position_id); - if (!position) - continue; - - if (position->responsibilities[df::entity_position_responsibility::TRADE]) - if (trader_requested) - dwarf->clear_all = true; - } - - } - - // identify dwarfs who are needed for meetings and mark them for exclusion - - for (size_t i = 0; i < plotinfo->activities.size(); ++i) - { - df::activity_info *act = plotinfo->activities[i]; - if (!act) continue; - - bool p1 = act->unit_actor == dwarf->dwarf; - bool p2 = act->unit_noble == dwarf->dwarf; - - if (p1 || p2) - { - df::unit* other = p1 ? act->unit_noble : act->unit_actor; - if (other && !(!Units::isActive(other) || - (other->job.current_job && - (other->job.current_job->job_type == df::job_type::Sleep || - other->job.current_job->job_type == df::job_type::Rest)) || - ENUM_ATTR(profession, military, other->profession))) { - dwarf->clear_all = true; - if (print_debug) - out.print("Dwarf \"%s\" has a meeting, will be cleared of all labors\n", dwarf->dwarf->name.first_name.c_str()); - break; - } - else - { - if (print_debug) - out.print("Dwarf \"%s\" has a meeting, but with someone who can't make the meeting.\n", dwarf->dwarf->name.first_name.c_str()); - } - } - } - - // check to see if dwarf has minor children - - for (auto u2 = world->units.active.begin(); u2 != world->units.active.end(); ++u2) - { - if ((*u2)->relationship_ids[df::unit_relationship_type::Mother] == dwarf->dwarf->id && - Units::isActive(*u2) && - ((*u2)->profession == df::profession::CHILD || (*u2)->profession == df::profession::BABY)) - { - dwarf->has_children = true; - if (print_debug) - out.print("Dwarf %s has minor children\n", dwarf->dwarf->name.first_name.c_str()); - break; - } - } - - // check if dwarf has an axe, pick, or crossbow - - for (size_t j = 0; j < dwarf->dwarf->inventory.size(); j++) - { - df::unit_inventory_item* plotinfo = dwarf->dwarf->inventory[j]; - if (plotinfo->mode == df::unit_inventory_item::Weapon && plotinfo->item->isWeapon()) - { - dwarf->armed = true; - df::itemdef_weaponst* weapondef = ((df::item_weaponst*)(plotinfo->item))->subtype; - df::job_skill weaponsk = (df::job_skill) weapondef->skill_melee; - df::job_skill rangesk = (df::job_skill) weapondef->skill_ranged; - if (weaponsk == df::job_skill::AXE) - { - dwarf->has_tool[TOOL_AXE] = true; - } - else if (weaponsk == df::job_skill::MINING) - { - dwarf->has_tool[TOOL_PICK] = true; - } - else if (rangesk == df::job_skill::CROSSBOW) - { - dwarf->has_tool[TOOL_CROSSBOW] = true; - } - } - } - - // Find the activity state for each dwarf - - bool is_migrant = false; - dwarf_state state = OTHER; - - for (auto p = dwarf->dwarf->status.misc_traits.begin(); p < dwarf->dwarf->status.misc_traits.end(); p++) - { - if ((*p)->id == misc_trait_type::Migrant) - is_migrant = true; - } - - if (dwarf->dwarf->social_activities.size() > 0) - { - if (print_debug) - out.print("Dwarf %s is engaged in a social activity. Info only.\n", dwarf->dwarf->name.first_name.c_str()); - } - - if (dwarf->dwarf->profession == profession::BABY || - dwarf->dwarf->profession == profession::CHILD || - dwarf->dwarf->profession == profession::DRUNK) - { - state = CHILD; - } - - else if (ENUM_ATTR(profession, military, dwarf->dwarf->profession)) - state = MILITARY; - - else if (dwarf->dwarf->burrows.size() > 0) - state = OTHER; // dwarfs assigned to burrows are treated as if permanently busy - - else if (dwarf->dwarf->job.current_job == NULL) - { - if (is_migrant || dwarf->dwarf->flags1.bits.chained || dwarf->dwarf->flags1.bits.caged) - { - state = OTHER; - dwarf->clear_all = true; - } - else if (dwarf->dwarf->status2.limbs_grasp_count == 0) - { - state = OTHER; // dwarfs unable to grasp are incapable of nearly all labors - dwarf->clear_all = true; - if (print_debug) - out.print("Dwarf %s is disabled, will not be assigned labors\n", dwarf->dwarf->name.first_name.c_str()); - } - else - { - state = IDLE; - } - } - else - { - df::job_type job = dwarf->dwarf->job.current_job->job_type; - if (job >= 0 && size_t(job) < ARRAY_COUNT(dwarf_states)) - state = dwarf_states[job]; - else - { - out.print("Dwarf \"%s\" has unknown job %i\n", dwarf->dwarf->name.first_name.c_str(), job); - debug_pause(); - state = OTHER; - } - if (state == BUSY) - { - df::unit_labor labor = labor_mapper->find_job_labor(dwarf->dwarf->job.current_job); - - dwarf->using_labor = labor; - - if (labor != df::unit_labor::NONE) - { - labor_infos[labor].busy_dwarfs++; - if (default_labor_infos[labor].tool != TOOL_NONE) - { - tool_in_use[default_labor_infos[labor].tool]++; - } - } - } - } - - dwarf->state = state; - - dwarf->unmanaged_labors_assigned = 0; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if (dwarf->dwarf->status.labors[l]) - if (state == IDLE) - labor_infos[l].idle_dwarfs++; - if (labor_infos[l].is_unmanaged()) - dwarf->unmanaged_labors_assigned++; - } - - - if (print_debug) - out.print("Dwarf \"%s\": state %s %d\n", dwarf->dwarf->name.first_name.c_str(), state_names[dwarf->state], dwarf->clear_all); - - state_count[dwarf->state]++; - - // determine if dwarf has medical needs - if (dwarf->dwarf->health && !( - // on-duty military will not necessarily break to get minor injuries attended - ENUM_ATTR(profession, military, dwarf->dwarf->profession) || - // babies cannot currently receive health care even if they need it - dwarf->dwarf->profession == profession::BABY) - ) - { - if (dwarf->dwarf->health->flags.bits.should_not_move) - cnt_recover_wounded++; - if (dwarf->dwarf->health->flags.bits.rq_diagnosis) - cnt_diagnosis++; - if (dwarf->dwarf->health->flags.bits.rq_immobilize) - cnt_immobilize++; - if (dwarf->dwarf->health->flags.bits.rq_dressing) - cnt_dressing++; - if (dwarf->dwarf->health->flags.bits.rq_cleaning) - cnt_cleaning++; - if (dwarf->dwarf->health->flags.bits.rq_surgery) - cnt_surgery++; - if (dwarf->dwarf->health->flags.bits.rq_suture) - cnt_suture++; - if (dwarf->dwarf->health->flags.bits.rq_setting) - cnt_setting++; - if (dwarf->dwarf->health->flags.bits.rq_traction) - cnt_traction++; - if (dwarf->dwarf->health->flags.bits.rq_crutch) - cnt_crutch++; - } - - if (dwarf->dwarf->counters2.hunger_timer > 60000 || dwarf->dwarf->counters2.thirst_timer > 40000) - need_food_water++; - - // find dwarf's highest effective skill - - int high_skill = 0; - - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == df::unit_labor::NONE || labor_infos[labor].is_unmanaged()) - continue; - - df::job_skill skill = labor_to_skill[labor]; - if (skill != df::job_skill::NONE) - { - int skill_level = Units::getNominalSkill(dwarf->dwarf, skill, false); - high_skill = std::max(high_skill, skill_level); - } - } - - dwarf->high_skill = high_skill; - - - // clear labors of dwarfs with clear_all set - - if (dwarf->clear_all) - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE || labor_infos[labor].is_unmanaged()) - continue; - if (Units::isValidLabor(dwarf->dwarf, labor)) - set_labor(dwarf, labor, false); - } - } - else { - if (state == IDLE) - available_dwarfs.push_back(dwarf); - - if (state == BUSY) - busy_dwarfs.push_back(dwarf); - } - } - - } - } - - void release_dwarf_list() - { - while (!dwarf_info.empty()) { - auto d = dwarf_info.begin(); - delete *d; - dwarf_info.erase(d); - } - available_dwarfs.clear(); - busy_dwarfs.clear(); - } - - int score_labor(dwarf_info_t* d, df::unit_labor labor) - { - int skill_level = 0; - int xp = 0; - int attr_weight = 0; - - if (labor != df::unit_labor::NONE) - { - df::job_skill skill = labor_to_skill[labor]; - if (skill != df::job_skill::NONE) - { - skill_level = Units::getEffectiveSkill(d->dwarf, skill); - xp = Units::getExperience(d->dwarf, skill, false); - - for (int pa = 0; pa < 6; pa++) - attr_weight += (skill_attr_weights[skill].phys_attr_weights[pa]) * (d->dwarf->body.physical_attrs[pa].value - 1000); - - for (int ma = 0; ma < 13; ma++) - attr_weight += (skill_attr_weights[skill].mental_attr_weights[ma]) * (d->dwarf->status.current_soul->mental_attrs[ma].value - 1000); - } - - } - - int score = skill_level * 1000 - (d->high_skill - skill_level) * 2000 + (xp / (skill_level + 5) * 10) + attr_weight; - - if (labor != df::unit_labor::NONE) - { - if (d->dwarf->status.labors[labor]) - { - if (labor == df::unit_labor::OPERATE_PUMP) - score += 50000; - else - score += 25000; - } - if (default_labor_infos[labor].tool != TOOL_NONE && - d->has_tool[default_labor_infos[labor].tool]) - score += 10000000; - if (d->has_children && labor_outside[labor]) - score -= 15000; - if (d->armed && labor_outside[labor]) - score += 5000; - } - - // Favor/disfavor RECOVER_WOUNDED based on ALTRUISM personality facet - - if (labor == df::unit_labor::RECOVER_WOUNDED) - { - int altruism = d->dwarf->status.current_soul->personality.traits[df::personality_facet_type::ALTRUISM]; - if (altruism >= 61) - score += 5000; - else if (altruism <= 24) - score -= 50000; - } - - // Favor/disfavor BUTCHER (covers slaughtering), HAUL_ANIMALS (covers caging), and CUTWOOD based on NATURE value - - if (labor == df::unit_labor::BUTCHER || labor == df::unit_labor::HAUL_ANIMALS || labor == df::unit_labor::CUTWOOD) - { - int nature = 0; - for (auto i = d->dwarf->status.current_soul->personality.values.begin(); - i != d->dwarf->status.current_soul->personality.values.end(); - i++) - { - if ((*i)->type == df::value_type::NATURE) - nature = (*i)->strength; - } - - if (nature <= -11) - score += 5000; - else if (nature >= 26) - score -= 50000; - } - - // This should reweight assigning CUTWOOD jobs based on a citizen's ethic toward killing plants - - if (labor == df::unit_labor::CUTWOOD) - { - if (auto culture = df::cultural_identity::find(d->dwarf->cultural_identity)) - { - auto ethics = culture->ethic[df::ethic_type::KILL_PLANT]; - if (ethics != df::ethic_response::NOT_APPLICABLE && ethics != df::ethic_response::REQUIRED) - score += 10000 * (df::ethic_response::ACCEPTABLE - ethics); - } - } - - score -= Units::computeMovementSpeed(d->dwarf); - - // significantly disfavor dwarves who have unmanaged labors assigned - score -= 1000 * d->unmanaged_labors_assigned; - - return score; - } - -public: - void process() - { - if (*df::global::process_dig || *df::global::process_jobs) - return; - - release_dwarf_list(); - - dig_count = tree_count = plant_count = detail_count = 0; - cnt_recover_wounded = cnt_diagnosis = cnt_immobilize = cnt_dressing = cnt_cleaning = cnt_surgery = cnt_suture = - cnt_setting = cnt_traction = cnt_crutch = 0; - need_food_water = 0; - - labor_needed.clear(); - - for (int e = 0; e < TOOLS_MAX; e++) - tool_count[e] = 0; - - trader_requested = false; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - - labor_infos[l].active_dwarfs = labor_infos[l].busy_dwarfs = labor_infos[l].idle_dwarfs = 0; - } - - // scan for specific buildings of interest - - scan_buildings(); - - // count number of squares designated for dig, wood cutting, detailing, and plant harvesting - - count_map_designations(); - - // collect current job list - - collect_job_list(); - - // count number of picks and axes available for use - - count_tools(); - - // collect list of dwarfs - - collect_dwarf_list(); - - // add job entries for designation-related jobs - - labor_needed[df::unit_labor::MINE] += dig_count; - labor_needed[df::unit_labor::CUTWOOD] += tree_count; - labor_needed[df::unit_labor::DETAIL] += detail_count; - labor_needed[df::unit_labor::HERBALIST] += plant_count; - - // add job entries for health care - - labor_needed[df::unit_labor::RECOVER_WOUNDED] += cnt_recover_wounded; - labor_needed[df::unit_labor::DIAGNOSE] += cnt_diagnosis; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_immobilize; - labor_needed[df::unit_labor::DRESSING_WOUNDS] += cnt_dressing; - labor_needed[df::unit_labor::DRESSING_WOUNDS] += cnt_cleaning; - labor_needed[df::unit_labor::SURGERY] += cnt_surgery; - labor_needed[df::unit_labor::SUTURING] += cnt_suture; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_setting; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_traction; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_crutch; - - labor_needed[df::unit_labor::FEED_WATER_CIVILIANS] += need_food_water; - - // add entries for hauling jobs - - labor_needed[df::unit_labor::HAUL_STONE] += world->stockpile.num_jobs[1]; - labor_needed[df::unit_labor::HAUL_WOOD] += world->stockpile.num_jobs[2]; - labor_needed[df::unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[3]; - labor_needed[df::unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[4]; - labor_needed[df::unit_labor::HAUL_BODY] += world->stockpile.num_jobs[5]; - labor_needed[df::unit_labor::HAUL_FOOD] += world->stockpile.num_jobs[6]; - labor_needed[df::unit_labor::HAUL_REFUSE] += world->stockpile.num_jobs[7]; - labor_needed[df::unit_labor::HAUL_FURNITURE] += world->stockpile.num_jobs[8]; - labor_needed[df::unit_labor::HAUL_ANIMALS] += world->stockpile.num_jobs[9]; - - labor_needed[df::unit_labor::HAUL_STONE] += (world->stockpile.num_jobs[1] >= world->stockpile.num_haulers[1]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_WOOD] += (world->stockpile.num_jobs[2] >= world->stockpile.num_haulers[2]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_ITEM] += (world->stockpile.num_jobs[3] >= world->stockpile.num_haulers[3]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_BODY] += (world->stockpile.num_jobs[5] >= world->stockpile.num_haulers[5]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_FOOD] += (world->stockpile.num_jobs[6] >= world->stockpile.num_haulers[6]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_REFUSE] += (world->stockpile.num_jobs[7] >= world->stockpile.num_haulers[7]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_FURNITURE] += (world->stockpile.num_jobs[8] >= world->stockpile.num_haulers[8]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_ANIMALS] += (world->stockpile.num_jobs[9] >= world->stockpile.num_haulers[9]) ? 1 : 0; - - int binjobs = world->stockpile.num_jobs[4] + ((world->stockpile.num_jobs[4] >= world->stockpile.num_haulers[4]) ? 1 : 0); - - labor_needed[df::unit_labor::HAUL_ITEM] += binjobs; - labor_needed[df::unit_labor::HAUL_FOOD] += priority_food; - - // add entries for vehicle hauling - - for (auto v = world->vehicles.all.begin(); v != world->vehicles.all.end(); v++) - if ((*v)->route_id != -1) - labor_needed[df::unit_labor::HANDLE_VEHICLES]++; - - // add fishing & hunting - - labor_needed[df::unit_labor::FISH] = - (isOptionEnabled(CF_ALLOW_FISHING) && has_fishery) ? 1 : 0; - - labor_needed[df::unit_labor::HUNT] = - (isOptionEnabled(CF_ALLOW_HUNTING) && has_butchers) ? 1 : 0; - - /* add animal trainers */ - for (auto a = df::global::plotinfo->training.training_assignments.begin(); - a != df::global::plotinfo->training.training_assignments.end(); - a++) - { - labor_needed[df::unit_labor::ANIMALTRAIN]++; - // note: this doesn't test to see if the trainer is actually needed, and thus will overallocate trainers. bleah. - } - - /* set requirements to zero for labors with currently idle dwarfs, and remove from requirement dwarfs actually working */ - - FOR_ENUM_ITEMS(unit_labor, l) { - if (l == df::unit_labor::NONE) - continue; - - if (!labor_infos[l].is_unmanaged()) - { - int before = labor_needed[l]; - - labor_needed[l] = max(0, labor_needed[l] - labor_in_use[l]); - - if (default_labor_infos[l].tool != TOOL_NONE) - labor_needed[l] = std::min(labor_needed[l], tool_count[default_labor_infos[l].tool] - tool_in_use[default_labor_infos[l].tool]); - - if (print_debug && before != labor_needed[l]) - out.print("labor %s reduced from %d to %d\n", ENUM_KEY_STR(unit_labor, l).c_str(), before, labor_needed[l]); - } - else - { - labor_needed[l] = 0; - } - } - - /* assign food haulers for rotting food items */ - if (!labor_infos[df::unit_labor::HAUL_FOOD].is_unmanaged()) - { - if (priority_food > 0 && labor_infos[df::unit_labor::HAUL_FOOD].idle_dwarfs > 0) - priority_food = 1; - - if (print_debug) - out.print("priority food count = %d\n", priority_food); - - while (!available_dwarfs.empty() && priority_food > 0) - { - std::list::iterator bestdwarf = available_dwarfs.begin(); - - int best_score = INT_MIN; - - for (std::list::iterator k = available_dwarfs.begin(); k != available_dwarfs.end(); k++) - { - dwarf_info_t* d = (*k); - - if (Units::isValidLabor(d->dwarf, df::unit_labor::HAUL_FOOD)) - { - int score = score_labor(d, df::unit_labor::HAUL_FOOD); - - if (score > best_score) - { - bestdwarf = k; - best_score = score; - } - } - } - - if (best_score > INT_MIN) - { - if (print_debug) - out.print("LABORMANAGER: assign \"%s\" labor %s score=%d (priority food)\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, df::unit_labor::HAUL_FOOD).c_str(), best_score); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, l == df::unit_labor::HAUL_FOOD); - } - - available_dwarfs.erase(bestdwarf); - priority_food--; - } - else - break; - - } - } - else - { - priority_food = 0; - } - - if (print_debug) - { - for (auto i = labor_needed.begin(); i != labor_needed.end(); i++) - { - out.print("labor_needed [%s] = %d, busy = %d, outside = %d, idle = %d\n", ENUM_KEY_STR(unit_labor, i->first).c_str(), i->second, - labor_infos[i->first].busy_dwarfs, labor_outside[i->first], labor_infos[i->first].idle_dwarfs); - } - } - - std::map base_priority; - priority_queue> pq; - priority_queue> pq2; - - for (auto i = labor_needed.begin(); i != labor_needed.end(); i++) - { - df::unit_labor l = i->first; - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - const int user_specified_max_dwarfs = labor_infos[l].maximum_dwarfs(); - - if (user_specified_max_dwarfs != MAX_DWARFS_NONE && i->second > user_specified_max_dwarfs) - { - i->second = user_specified_max_dwarfs; - } - - int priority = labor_infos[l].priority(); - - priority += labor_infos[l].time_since_last_assigned() / 12; - priority -= labor_infos[l].busy_dwarfs; - - base_priority[l] = priority; - - if (i->second > 0) - { - pq.push(make_pair(priority, l)); - } - } - - if (print_debug) - out.print("available count = %zu, distinct labors needed = %zu\n", available_dwarfs.size(), pq.size()); - - std::map to_assign; - - to_assign.clear(); - - size_t av = available_dwarfs.size(); - - while (!pq.empty() && av > 0) - { - df::unit_labor labor = pq.top().second; - int priority = pq.top().first; - to_assign[labor]++; - pq.pop(); - av--; - - if (print_debug) - out.print("Will assign: %s priority %d (%d)\n", ENUM_KEY_STR(unit_labor, labor).c_str(), priority, to_assign[labor]); - - if (--labor_needed[labor] > 0) - { - priority -= 10; - pq2.push(make_pair(priority, labor)); - } - - if (pq.empty()) - while (!pq2.empty()) - { - pq.push(pq2.top()); - pq2.pop(); - } - } - - while (!pq2.empty()) - { - pq.push(pq2.top()); - pq2.pop(); - } - - int canary = (1 << df::unit_labor::HAUL_STONE) | - (1 << df::unit_labor::HAUL_WOOD) | - (1 << df::unit_labor::HAUL_BODY) | - (1 << df::unit_labor::HAUL_FOOD) | - (1 << df::unit_labor::HAUL_REFUSE) | - (1 << df::unit_labor::HAUL_ITEM) | - (1 << df::unit_labor::HAUL_FURNITURE) | - (1 << df::unit_labor::HAUL_ANIMALS); - - while (!available_dwarfs.empty()) - { - std::list::iterator bestdwarf = available_dwarfs.begin(); - - int best_score = INT_MIN; - df::unit_labor best_labor = df::unit_labor::NONE; - - for (auto j = to_assign.begin(); j != to_assign.end(); j++) - { - if (j->second <= 0) - continue; - - df::unit_labor labor = j->first; - - for (std::list::iterator k = available_dwarfs.begin(); k != available_dwarfs.end(); k++) - { - dwarf_info_t* d = (*k); - if (Units::isValidLabor(d->dwarf, labor)) - { - int score = score_labor(d, labor); - if (score > best_score) - { - bestdwarf = k; - best_score = score; - best_labor = labor; - } - } - } - } - - if (best_labor == df::unit_labor::NONE) - break; - - if (print_debug) - out.print("assign \"%s\" labor %s score=%d\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, best_labor).c_str(), best_score); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - - tools_enum t = default_labor_infos[l].tool; - - if (l == best_labor && - Units::isValidLabor((*bestdwarf)->dwarf, l) && - (t == TOOL_NONE || tool_in_use[t] < tool_count[t])) - { - set_labor(*bestdwarf, l, true); - if (t != TOOL_NONE && !((*bestdwarf)->has_tool[t])) - { - df::job_type j; - j = df::job_type::NONE; - - if ((*bestdwarf)->dwarf->job.current_job) - j = (*bestdwarf)->dwarf->job.current_job->job_type; - - if (print_debug) - out.print("LABORMANAGER: asking %s to pick up tools, current job %s\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(job_type, j).c_str()); - - (*bestdwarf)->dwarf->uniform.pickup_flags.bits.update = true; - labors_changed = true; - } - } - else if (l == df::unit_labor::CLEAN && best_score < 0) - { - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, true); - } - else if ((*bestdwarf)->state == IDLE) - { - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, false); - } - } - - if (best_labor == df::unit_labor::HAUL_FOOD && priority_food > 0) - priority_food--; - - if (best_labor >= df::unit_labor::HAUL_STONE && best_labor <= df::unit_labor::HAUL_ANIMALS) - canary &= ~(1 << best_labor); - - if (best_labor != df::unit_labor::NONE) - { - labor_infos[best_labor].active_dwarfs++; - to_assign[best_labor]--; - } - - busy_dwarfs.push_back(*bestdwarf); - available_dwarfs.erase(bestdwarf); - } - - for (auto d = busy_dwarfs.begin(); d != busy_dwarfs.end(); d++) - { - int current_score = score_labor(*d, (*d)->using_labor); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - if (l == (*d)->using_labor) - continue; - if (labor_needed[l] <= 0) - continue; - if (!Units::isValidLabor((*d)->dwarf, l)) - continue; - - int score = score_labor(*d, l); - - if (l == df::unit_labor::HAUL_FOOD && priority_food > 0) - score += 1000000; - - if (score > current_score) - { - tools_enum t = default_labor_infos[l].tool; - if (t == TOOL_NONE || (*d)->has_tool[t]) - { - set_labor(*d, l, true); - } - - if (score < 0) - set_labor(*d, df::unit_labor::CLEAN, true); - - if ((*d)->using_labor != df::unit_labor::NONE && - (score > current_score + 5000 || base_priority[(*d)->using_labor] < base_priority[l]) && - default_labor_infos[(*d)->using_labor].tool == TOOL_NONE) - set_labor(*d, (*d)->using_labor, false); - } - } - } - - - dwarf_info_t* canary_dwarf = 0; - - for (auto di = busy_dwarfs.begin(); di != busy_dwarfs.end(); di++) - if (!(*di)->clear_all) - { - canary_dwarf = *di; - break; - } - - if (canary_dwarf) - { - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l >= df::unit_labor::HAUL_STONE && l <= df::unit_labor::HAUL_ANIMALS && - canary & (1 << l) && - Units::isValidLabor(canary_dwarf->dwarf, l)) - set_labor(canary_dwarf, l, true); - } - - set_labor(canary_dwarf, df::unit_labor::CLEAN, true); - - /* Set HAUL_WATER so we can detect ponds that need to be filled ponds. */ - - if (!labor_infos[df::unit_labor::HAUL_WATER].is_unmanaged()) - { - set_labor(canary_dwarf, df::unit_labor::HAUL_WATER, true); - } - - if (print_debug) - out.print("Setting %s as the hauling canary\n", canary_dwarf->dwarf->name.first_name.c_str()); - } - else - { - if (print_debug) - out.print("No dwarf available to set as the hauling canary!\n"); - } - - /* Assign any leftover dwarfs to "standard" labors */ - - if (print_debug) - out.print("After assignment, %zu dwarfs left over\n", available_dwarfs.size()); - - for (auto d = available_dwarfs.begin(); d != available_dwarfs.end(); d++) - { - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - if (Units::isValidLabor((*d)->dwarf, l)) - set_labor(*d, l, - (l >= df::unit_labor::HAUL_STONE && l <= df::unit_labor::HAUL_ANIMALS) || - l == df::unit_labor::CLEAN || - l == df::unit_labor::HAUL_WATER || - l == df::unit_labor::PULL_LEVER || - l == df::unit_labor::HAUL_TRADE); - } - } - - /* check for dwarfs assigned no labors and assign them the bucket list if there are */ - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - if ((*d)->state == CHILD) - continue; - - bool any = false; - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if ((*d)->dwarf->status.labors[l]) - { - any = true; - break; - } - } - - if (!labor_infos[df::unit_labor::PULL_LEVER].is_unmanaged()) - { - set_labor(*d, df::unit_labor::PULL_LEVER, true); - } - - if (any) continue; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - if (to_assign[l] > 0 || l == df::unit_labor::CLEAN) - set_labor(*d, l, true); - } - } - - - /* set reequip on any dwarfs who are carrying tools needed by others */ - - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - if ((*d)->dwarf->job.current_job && (*d)->dwarf->job.current_job->job_type == df::job_type::PickupEquipment) - continue; - - if ((*d)->dwarf->uniform.pickup_flags.bits.update) - continue; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - tools_enum t = default_labor_infos[l].tool; - if (t == TOOL_NONE) - continue; - - bool has_tool = (*d)->has_tool[t]; - bool needs_tool = (*d)->dwarf->status.labors[l]; - - if ((needs_tool && !has_tool) || - (has_tool && !needs_tool && tool_in_use[t] >= tool_count[t])) - { - df::job_type j = df::job_type::NONE; - - if ((*d)->dwarf->job.current_job) - j = (*d)->dwarf->job.current_job->job_type; - - if (print_debug) - out.print("LABORMANAGER: asking %s to %s tools, current job %s, %d %d \n", (*d)->dwarf->name.first_name.c_str(), (has_tool) ? "drop" : "pick up", ENUM_KEY_STR(job_type, j).c_str(), has_tool, needs_tool); - - (*d)->dwarf->uniform.pickup_flags.bits.update = true; - labors_changed = true; - - if (needs_tool) - tool_in_use[t]++; - } - } - } - - release_dwarf_list(); - - if (labors_changed) - { - *df::global::process_dig = true; - *df::global::process_jobs = true; - } - - if (print_debug) { - *df::global::pause_state = true; - } - - print_debug = 0; - - } - -}; - - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - cleanup_state(); - init_state(); - break; - case SC_MAP_UNLOADED: - cleanup_state(); - break; - default: - break; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) -{ - // static int step_count = 0; - // check run conditions - if (!initialized || !world || !world->map.block_index || !enable_labormanager) - { - // give up if we shouldn't be running' - return CR_OK; - } - - // if (++step_count < 60) - // return CR_OK; - - if (*df::global::process_jobs) - return CR_OK; - - // step_count = 0; - - debug_stream = &out; - AutoLaborManager alm(out); - alm.process(); - - return CR_OK; -} - -void print_labor(df::unit_labor labor, color_ostream &out) -{ - string labor_name = ENUM_KEY_STR(unit_labor, labor); - out << labor_name << ": "; - for (int i = 0; i < 20 - (int)labor_name.length(); i++) - out << ' '; - const auto& labor_info = labor_infos[labor]; - if (labor_info.is_unmanaged()) - { - out << "UNMANAGED"; - } - else - { - out << "priority " << labor_info.priority(); - - if (labor_info.maximum_dwarfs() == MAX_DWARFS_NONE) - out << ", no maximum"; - else - out << ", maximum " << labor_info.maximum_dwarfs(); - } - out << ", currently " << labor_info.active_dwarfs << " dwarfs (" - << labor_info.busy_dwarfs << " busy, " - << labor_info.idle_dwarfs << " idle)" - << endl; -} - -df::unit_labor lookup_labor_by_name(std::string name) -{ - // We should accept incorrect casing, there is no ambiguity. - std::transform(name.begin(), name.end(), name.begin(), ::toupper); - - FOR_ENUM_ITEMS(unit_labor, test_labor) - { - if (name == ENUM_KEY_STR(unit_labor, test_labor)) - { - return test_labor; - } - } - - return df::unit_labor::NONE; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("World is not loaded: please load a fort first.\n"); - return CR_FAILURE; - } - - if (enable && !enable_labormanager) - { - enable_plugin(out); - } - else if (!enable && enable_labormanager) - { - enable_labormanager = false; - setOptionEnabled(CF_ENABLED, false); - - out << "LaborManager is disabled." << endl; - } - - return CR_OK; -} - -command_result labormanager(color_ostream &out, std::vector & parameters) -{ - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("World is not loaded: please load a game first.\n"); - return CR_FAILURE; - } - - if (parameters.size() == 1 && - (parameters[0] == "enable" || parameters[0] == "disable")) - { - bool enable = (parameters[0] == "enable"); - return plugin_enable(out, enable); - } - else if (parameters.size() == 3 && - (parameters[0] == "max" || parameters[0] == "priority")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - df::unit_labor labor = lookup_labor_by_name(parameters[1]); - - if (labor == df::unit_labor::NONE) - { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); - return CR_WRONG_USAGE; - } - - int v; - - if (parameters[2] == "none") - v = MAX_DWARFS_NONE; - else if (parameters[2] == "disable" || parameters[2] == "unmanaged") - v = MAX_DWARFS_UNMANAGED; - else - v = atoi(parameters[2].c_str()); - - if (parameters[0] == "max") - labor_infos[labor].set_maximum_dwarfs(v); - else if (parameters[0] == "priority") - labor_infos[labor].set_priority(v); - - print_labor(labor, out); - return CR_OK; - } - else if (parameters.size() == 2 && parameters[0] == "reset") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - df::unit_labor labor = lookup_labor_by_name(parameters[1]); - - if (labor == df::unit_labor::NONE) - { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); - return CR_WRONG_USAGE; - } - reset_labor(labor); - print_labor(labor, out); - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "allow-fishing" || parameters[0] == "forbid-fishing")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - setOptionEnabled(CF_ALLOW_FISHING, (parameters[0] == "allow-fishing")); - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "allow-hunting" || parameters[0] == "forbid-hunting")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - setOptionEnabled(CF_ALLOW_HUNTING, (parameters[0] == "allow-hunting")); - return CR_OK; - } - else if (parameters.size() == 1 && parameters[0] == "reset-all") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - for (size_t i = 0; i < labor_infos.size(); i++) - { - reset_labor((df::unit_labor) i); - } - out << "All labors reset." << endl; - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - bool need_comma = 0; - for (int i = 0; i < NUM_STATE; i++) - { - if (state_count[i] == 0) - continue; - if (need_comma) - out << ", "; - out << state_count[i] << ' ' << state_names[i]; - need_comma = 1; - } - out << endl; - - if (parameters[0] == "list") - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - - print_labor(labor, out); - } - } - - return CR_OK; - } - else if (parameters.size() == 2 && parameters[0] == "pause-on-error") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - pause_on_error = parameters[1] == "yes" || parameters[1] == "true"; - - return CR_OK; - } - else if (parameters.size() == 1 && parameters[0] == "debug") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - print_debug = 1; - - return CR_OK; - } - else - { - out.print("Automatically assigns labors to dwarves.\n" - "Activate with 'labormanager enable', deactivate with 'labormanager disable'.\n" - "Current state: %s.\n", enable_labormanager ? "enabled" : "disabled"); - - return CR_OK; - } -} diff --git a/plugins/autolabor/labormanager.h b/plugins/autolabor/labormanager.h deleted file mode 100644 index c958900b1a..0000000000 --- a/plugins/autolabor/labormanager.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -void debug(const char* fmt, ...); -void debug_pause(); diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index a9472bcc2e..e944c815a4 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -30,7 +30,7 @@ using namespace df::enums; const int NUM_STATE = 6; -char const* state_names[] { +static char const* const state_names[] { "IDLE", "BUSY", "EXCLUSIVE", @@ -282,6 +282,12 @@ const dwarf_state dwarf_states[] = { dwarf_state::OTHER /* HeistItem */, dwarf_state::OTHER /* InterrogateSubject */, dwarf_state::OTHER /* AcceptHeistItem */, + dwarf_state::BUSY /* StoreSquadEquipmentItem */, + dwarf_state::BUSY /* MixDye */, + dwarf_state::BUSY /* DyeLeather */, + dwarf_state::BUSY /* ConstructBoltThrowerParts */, + dwarf_state::BUSY /* LoadBoltThrower */, + dwarf_state::BUSY /* FireBoltThrower */, }; #define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) diff --git a/plugins/autolabor/legacyengine.cpp b/plugins/autolabor/legacyengine.cpp new file mode 100644 index 0000000000..5193411bef --- /dev/null +++ b/plugins/autolabor/legacyengine.cpp @@ -0,0 +1,951 @@ +/* + * Legacy engine for autolabor: direct labor matrix manipulation. + * + * This preserves the pre-v50 behavior of the autolabor plugin. It bypasses + * the work detail system by setting automatic_professions_disabled and + * writes unit->status.labors directly each cycle. + */ + +#include "laborcommon.h" +#include "legacyengine.h" + +#include +#include +#include +#include +#include + +#include "Debug.h" +#include "PluginManager.h" + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; +using df::global::game; + +#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) + +namespace DFHack { + DBG_DECLARE(autolabor, legacy_cycle, DebugCategory::LINFO); +} + +using autolabor::plugin_config; +using autolabor::config_flag; +using autolabor::labor_to_skill; + +static std::vector state_count(NUM_STATE); + +enum labor_mode { + DISABLE, + HAULERS, + AUTOMATIC, +}; + +struct labor_info +{ + PersistentDataItem config; + + bool is_exclusive; + int active_dwarfs; + + labor_mode mode() { return (labor_mode) config.ival(0); } + void set_mode(labor_mode mode) { config.ival(0) = mode; } + + int minimum_dwarfs() { return config.ival(1); } + void set_minimum_dwarfs(int minimum_dwarfs) { config.ival(1) = minimum_dwarfs; } + + int maximum_dwarfs() { return config.ival(2); } + void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } + + int talent_pool() { return config.ival(3); } + void set_talent_pool(int talent_pool) { config.ival(3) = talent_pool; } +}; + +struct labor_default +{ + labor_mode mode; + bool is_exclusive; + int minimum_dwarfs; + int maximum_dwarfs; + int active_dwarfs; +}; + +// The percentage of the dwarves assigned as haulers at any one time. +static int hauler_pct = 33; + +// The maximum percentage of dwarves who will be allowed to be idle. +// Decreasing this will encourage autolabor to keep dwarves busy, +// at the expense of making it harder for dwarves to specialize in +// specific skills. +static int idler_pct = 10; + +static std::vector labor_infos; + +static const struct labor_default default_labor_infos[] = { + /* MINE */ {AUTOMATIC, true, 2, 200, 0}, + /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, + /* CLEAN */ {HAULERS, false, 1, 200, 0}, + /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, + /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, + /* STONECUTTER */ {AUTOMATIC, false, 1, 200, 0}, + /* STONE_CARVER */ {AUTOMATIC, false, 1, 200, 0}, + /* MASON */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, + /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, + /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, + /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, + /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, + /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, + /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, + /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, + /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, + /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, + /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, + /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, + /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, + /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, + /* MILK */ {AUTOMATIC, false, 1, 200, 0}, + /* COOK */ {AUTOMATIC, false, 1, 200, 0}, + /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, + /* FISH */ {AUTOMATIC, false, 1, 1, 0}, + /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, + /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, + /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, + /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, + /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, + /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, + /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* DYER */ {AUTOMATIC, false, 1, 200, 0}, + /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, + /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, + /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, + /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, + /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, + /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, + /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, + /* BEEKEEPING */ {AUTOMATIC, false, 1, 200, 0}, + /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, + /* HANDLE_VEHICLES */ {HAULERS, false, 1, 200, 0}, + /* HAUL_TRADE */ {HAULERS, false, 1, 200, 0}, + /* PULL_LEVER */ {HAULERS, false, 1, 200, 0}, + /* UNUSED_13 */ {DISABLE, false, 1, 0, 0}, + /* HAUL_WATER */ {HAULERS, false, 1, 200, 0}, + /* GELD */ {AUTOMATIC, false, 1, 200, 0}, + /* BUILD_ROAD */ {AUTOMATIC, false, 1, 200, 0}, + /* BUILD_CONSTRUCTION */ {AUTOMATIC, false, 1, 200, 0}, + /* PAPERMAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* BOOKBINDING */ {AUTOMATIC, false, 1, 200, 0}, + /* UNUSED_20 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_21 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_22 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_23 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_24 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_25 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_26 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_27 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_28 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_29 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_30 */ {DISABLE, false, 0, 0, 0}, +}; + +// the table is indexed by labor enum value: one entry per item from 0 to +// the last labor (NONE is -1 and has no entry). a DF release that adds or +// removes labor types breaks the build here until the table is updated. +static_assert(ARRAY_COUNT(default_labor_infos) == + (int)ENUM_LAST_ITEM(unit_labor) + 1, + "default_labor_infos must have one entry per unit_labor enum item"); + +struct dwarf_info_t +{ + int highest_skill; + int total_skill; + int mastery_penalty; + int assigned_jobs; + dwarf_state state; + bool has_exclusive_labor; + int noble_penalty; // penalty for assignment due to noble status + bool medical; // this dwarf has medical responsibility + bool trader; // this dwarf has trade responsibility + bool diplomacy; // this dwarf meets with diplomats +}; + +static const int32_t CYCLE_TICKS = 61; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static void reset_labor(df::unit_labor labor) +{ + labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs); + labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); + labor_infos[labor].set_talent_pool(200); + labor_infos[labor].set_mode(default_labor_infos[labor].mode); +} + +namespace autolabor { + +void legacy_labor_coverage(std::vector &missing) +{ + int enum_count = 0; + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + enum_count++; + // default_labor_infos is indexed by labor enum value; a labor added + // by a new DF release has no default and would be invisible here + if ((size_t)l >= ARRAY_COUNT(default_labor_infos)) + missing.push_back(ENUM_KEY_STR(unit_labor, l) + + ": no entry in legacy labor defaults"); + } + // a labor removed from the enum leaves stale entries behind + if (enum_count != (int)ARRAY_COUNT(default_labor_infos)) + missing.push_back("labor defaults table has " + + std::to_string(ARRAY_COUNT(default_labor_infos)) + + " entries for " + std::to_string(enum_count) + + " labor types"); +} + +} // namespace autolabor + +void LegacyEngine::enable(color_ostream &out) +{ + // purge any state left behind by the modern engine + wdm->reset(); + wdm->shutdown(); + + auto cfg_haulpct = World::GetPersistentSiteData("autolabor/haulpct"); + hauler_pct = cfg_haulpct.isValid() ? cfg_haulpct.ival(0) : 33; + + // Load labors from save + labor_infos.resize(ARRAY_COUNT(default_labor_infos)); + + std::vector items; + World::GetPersistentSiteData(&items, "autolabor/labors/", true); + + for (auto& p : items) + { + std::string key = p.key(); + df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); + if (labor >= 0 && size_t(labor) < labor_infos.size()) + { + labor_infos[labor].config = p; + labor_infos[labor].is_exclusive = default_labor_infos[labor].is_exclusive; + labor_infos[labor].active_dwarfs = 0; + } + } + + // Add default labors for those not in save + for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { + if (labor_infos[i].config.isValid()) + continue; + + std::stringstream name; + name << "autolabor/labors/" << i; + + labor_infos[i].config = World::AddPersistentSiteData(name.str()); + + labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; + labor_infos[i].active_dwarfs = 0; + reset_labor((df::unit_labor) i); + } + + // bypass DF's work detail system + game->external_flag.bits.automatic_professions_disabled = true; + + cycle_timestamp = 0; + initialized = true; + out << "Enabling autolabor (legacy mode)." << std::endl; +} + +void LegacyEngine::disable(color_ostream &out) +{ + initialized = false; + labor_infos.clear(); + + // reinstate DF's work detail system and force a recompute so the + // labor matrix is rebuilt from the player's work details immediately + game->external_flag.bits.automatic_professions_disabled = false; + for (auto u : world->units.active) + if (Units::isCitizen(u)) + Units::setAutomaticProfessions(u); + + out << "Disabling autolabor." << std::endl; +} + +void LegacyEngine::map_unload() +{ + initialized = false; + labor_infos.clear(); + if (game) + game->external_flag.bits.automatic_professions_disabled = false; +} + +// sorting objects +struct dwarfinfo_sorter +{ + dwarfinfo_sorter(std::vector & info):dwarf_info(info){}; + bool operator() (int i,int j) + { + if (dwarf_info[i].state == IDLE && dwarf_info[j].state != IDLE) + return true; + if (dwarf_info[i].state != IDLE && dwarf_info[j].state == IDLE) + return false; + return dwarf_info[i].mastery_penalty > dwarf_info[j].mastery_penalty; + }; + std::vector & dwarf_info; +}; +struct laborinfo_sorter +{ + bool operator() (int i,int j) + { + if (labor_infos[i].mode() != labor_infos[j].mode()) + return labor_infos[i].mode() < labor_infos[j].mode(); + if (labor_infos[i].is_exclusive != labor_infos[j].is_exclusive) + return labor_infos[i].is_exclusive; + if (labor_infos[i].maximum_dwarfs() != labor_infos[j].maximum_dwarfs()) + return labor_infos[i].maximum_dwarfs() < labor_infos[j].maximum_dwarfs(); + return false; + }; +}; + +struct values_sorter +{ + values_sorter(std::vector & values):values(values){}; + bool operator() (int i,int j) + { + return values[i] > values[j]; + }; + std::vector & values; +}; + + +static void assign_labor(unit_labor::unit_labor labor, + int n_dwarfs, + std::vector& dwarf_info, + bool trader_requested, + std::vector& dwarfs, + bool has_butchers, + bool has_fishery, + color_ostream& out) +{ + df::job_skill skill = labor_to_skill[labor]; + + if (labor_infos[labor].mode() != AUTOMATIC) + return; + + int best_skill = 0; + + std::vector values(n_dwarfs); + std::vector candidates; + std::map dwarf_skill; + std::map dwarf_skillxp; + std::vector previously_enabled(n_dwarfs); + + // Find candidate dwarfs, and calculate a preference value for each dwarf + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarf_info[dwarf].state == CHILD) + continue; + if (dwarf_info[dwarf].state == MILITARY) + continue; + if (dwarf_info[dwarf].trader && trader_requested) + continue; + if (dwarf_info[dwarf].diplomacy) + continue; + + if (labor_infos[labor].is_exclusive && dwarf_info[dwarf].has_exclusive_labor) + continue; + + int value = dwarf_info[dwarf].mastery_penalty; + + if (skill != job_skill::NONE) + { + int skill_level = 0; + int skill_experience = 0; + + for (auto s : dwarfs[dwarf]->status.souls[0]->skills) + { + if (s->id == skill) + { + skill_level = s->rating; + skill_experience = s->experience; + break; + } + } + + dwarf_skill[dwarf] = skill_level; + dwarf_skillxp[dwarf] = skill_experience; + + if (best_skill < skill_level) + best_skill = skill_level; + + value += skill_level * 100; + value += skill_experience / 20; + if (skill_level > 0 || skill_experience > 0) + value += 200; + if (skill_level >= 15) + value += 1000 * (skill_level - 14); + } + else + { + dwarf_skill[dwarf] = 0; + } + + if (dwarfs[dwarf]->status.labors[labor]) + { + value += 5; + if (labor_infos[labor].is_exclusive) + value += 350; + } + + if (dwarf_info[dwarf].has_exclusive_labor) + value -= 500; + + // bias by happiness + + //value += dwarfs[dwarf]->status.happiness; + + values[dwarf] = value; + + candidates.push_back(dwarf); + + } + + int pool = labor_infos[labor].talent_pool(); + if (pool < 200 && candidates.size() > 1 && size_t(abs(pool)) < candidates.size()) + { + // Sort in descending order + std::sort(candidates.begin(), candidates.end(), [&](const int lhs, const int rhs) -> bool { + if (dwarf_skill[lhs] == dwarf_skill[rhs]) + if (pool > 0) + return dwarf_skillxp[lhs] > dwarf_skillxp[rhs]; + else + return dwarf_skillxp[lhs] < dwarf_skillxp[rhs]; + else + if (pool > 0) + return dwarf_skill[lhs] > dwarf_skill[rhs]; + else + return dwarf_skill[lhs] < dwarf_skill[rhs]; + }); + + // Check if all dwarves have equivalent skills, usually zero + int first_dwarf = candidates[0]; + int last_dwarf = candidates[candidates.size() - 1]; + if (dwarf_skill[first_dwarf] == dwarf_skill[last_dwarf] && + dwarf_skillxp[first_dwarf] == dwarf_skillxp[last_dwarf]) + { + // There's no difference in skill, so change nothing + } + else + { + // Trim down to our top (or not) talents + candidates.resize(abs(pool)); + } + } + + // Sort candidates by preference value + values_sorter ivs(values); + std::sort(candidates.begin(), candidates.end(), ivs); + + // Disable the labor on everyone + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarf_info[dwarf].state == CHILD) + continue; + + previously_enabled[dwarf] = dwarfs[dwarf]->status.labors[labor]; + dwarfs[dwarf]->status.labors[labor] = false; + } + + int min_dwarfs = labor_infos[labor].minimum_dwarfs(); + int max_dwarfs = labor_infos[labor].maximum_dwarfs(); + + // Special - don't assign hunt without a butchers, or fish without a fishery + if (unit_labor::HUNT == labor && !has_butchers) + min_dwarfs = max_dwarfs = 0; + if (unit_labor::FISH == labor && !has_fishery) + min_dwarfs = max_dwarfs = 0; + + // If there are enough idle dwarves to choose from, enter an aggressive assignment + // mode. "Enough" idle dwarves is defined as 2 or 10% of the total number of dwarves, + // whichever is higher. + // + // In aggressive mode, we will always pick at least one idle dwarf for each skill, + // in order to try to get the idle dwarves to start doing something. We also pick + // any dwarf more preferable to the idle dwarf, since we'd rather have a more + // preferable dwarf do a new job if one becomes available (probably because that + // dwarf just finished a job). + // + // In non-aggressive mode, only dwarves that are good at a labor will be assigned + // to it. Dwarves good at nothing, or nothing that needs doing, will tend to get + // assigned to hauling by the hauler code. If there are no hauling jobs to do, + // they will sit around idle and when enough build up they will trigger aggressive + // mode again. + bool aggressive_mode = state_count[IDLE] >= 2 && state_count[IDLE] >= n_dwarfs * idler_pct / 100; + + /* + * Assign dwarfs to this labor. We assign at least the minimum number of dwarfs, in + * order of preference, and then assign additional dwarfs that meet any of these conditions: + * - We are in aggressive mode and have not yet assigned an idle dwarf + * - The dwarf is good at this skill + * - The labor is mining, hunting, or woodcutting and the dwarf currently has it enabled. + * We stop assigning dwarfs when we reach the maximum allowed. + * Note that only idle and busy dwarfs count towards the number of dwarfs. "Other" dwarfs + * (sleeping, eating, on break, etc.) will have labors assigned, but will not be counted. + * Military and children/nobles will not have labors assigned. + * Dwarfs with the "health management" responsibility are always assigned DIAGNOSIS. + */ + for (size_t i = 0; i < candidates.size() && labor_infos[labor].active_dwarfs < max_dwarfs; i++) + { + int dwarf = candidates[i]; + + if (dwarf_info[dwarf].trader && trader_requested) + continue; + if (dwarf_info[dwarf].diplomacy) + continue; + + bool preferred_dwarf = false; + if (dwarf_skillxp[dwarf] > 0 && dwarf_skill[dwarf] >= best_skill / 2) + preferred_dwarf = true; + if (previously_enabled[dwarf] && labor_infos[labor].is_exclusive && dwarf_info[dwarf].state == EXCLUSIVE) + preferred_dwarf = true; + if (dwarf_info[dwarf].medical && labor == df::unit_labor::DIAGNOSE) + preferred_dwarf = true; + + if (labor_infos[labor].active_dwarfs >= min_dwarfs && !preferred_dwarf && !aggressive_mode) + continue; + + if (!dwarfs[dwarf]->status.labors[labor]) + dwarf_info[dwarf].assigned_jobs++; + + if (Units::isValidLabor(dwarfs[dwarf], labor)) + dwarfs[dwarf]->status.labors[labor] = true; + + if (labor_infos[labor].is_exclusive) + { + dwarf_info[dwarf].has_exclusive_labor = true; + // all the exclusive labors require equipment so this should force the dorf to reequip if needed + dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; + } + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\" assigned {}: value {} {} {}\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor), values[dwarf], + dwarf_info[dwarf].trader ? "(trader)" : "", + dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + labor_infos[labor].active_dwarfs++; + + if (dwarf_info[dwarf].state == IDLE) + aggressive_mode = false; + } +} + +void LegacyEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + + cycle_timestamp = world->frame_counter; + + std::vector dwarfs; + + auto bscan = autolabor::scan_buildings(); + bool has_butchers = bscan.has_butchers; + bool has_fishery = bscan.has_fishery; + bool trader_requested = bscan.trader_requested; + + for (auto& cre : world->units.active) + { + if (Units::isCitizen(cre)) + { + if (cre->burrows.size() > 0) + continue; // dwarfs assigned to active burrows are skipped entirely + dwarfs.push_back(cre); + } + } + + int n_dwarfs = dwarfs.size(); + + if (n_dwarfs == 0) + return; + + std::vector dwarf_info(n_dwarfs); + + // Find total skill and highest skill for each dwarf. More skilled dwarves shouldn't be used for minor tasks. + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarfs[dwarf]->status.souls.size() <= 0) + continue; + + // compute noble penalty + + auto pos = autolabor::unit_position_info(dwarfs[dwarf]); + dwarf_info[dwarf].noble_penalty = pos.noble_penalty; + dwarf_info[dwarf].medical = pos.medical; + dwarf_info[dwarf].trader = pos.trader; + + // identify dwarfs who are needed for meetings and mark them for exclusion + + if (autolabor::in_diplomacy_meeting(dwarfs[dwarf])) + { + dwarf_info[dwarf].diplomacy = true; + DEBUG(legacy_cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", + dwarf, dwarfs[dwarf]->name.first_name); + } + + dwarf_info[dwarf].total_skill = autolabor::total_skill(dwarfs[dwarf]); + dwarf_info[dwarf].highest_skill = autolabor::top_skill(dwarfs[dwarf]); + } + + // Calculate a base penalty for using each dwarf for a task he isn't good at. + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + dwarf_info[dwarf].mastery_penalty -= 40 * dwarf_info[dwarf].highest_skill; + dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill; + dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty; + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor]) + dwarf_info[dwarf].mastery_penalty -= 100; + } + } + + // Find the activity state for each dwarf. It's important to get this right - a dwarf who we think is IDLE but + // can't work will gum everything up. In the future I might add code to auto-detect slacker dwarves. + + state_count.clear(); + state_count.resize(NUM_STATE); + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + dwarf_info[dwarf].state = autolabor::get_dwarf_state(dwarfs[dwarf]); + if (dwarf_info[dwarf].state == OTHER && + dwarfs[dwarf]->job.current_job && + (dwarfs[dwarf]->job.current_job->job_type < 0 || + size_t(dwarfs[dwarf]->job.current_job->job_type) >= dwarf_state_count)) + { + WARN(legacy_cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", + dwarf, dwarfs[dwarf]->name.first_name, int(dwarfs[dwarf]->job.current_job->job_type)); + } + + state_count[dwarf_info[dwarf].state]++; + + autolabor::probe_labor_observation(dwarfs[dwarf]); + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\": penalty {}, state {}\n", + dwarf, dwarfs[dwarf]->name.first_name, dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); + } + + std::vector labors; + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + labor_infos[labor].active_dwarfs = 0; + + labors.push_back(labor); + } + laborinfo_sorter lasorter; + std::sort(labors.begin(), labors.end(), lasorter); + + // Handle DISABLED skills (just bookkeeping). + // Note that autolabor should *NEVER* enable or disable a skill that has been marked as DISABLED, for any reason. + // The user has told us that they want manage this skill manually, and we must respect that. + for (auto& labor: labors) + { + if (labor_infos[labor].mode() != DISABLE) + continue; + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarfs[dwarf]->status.labors[labor]) + { + if (labor_infos[labor].is_exclusive) + dwarf_info[dwarf].has_exclusive_labor = true; + + dwarf_info[dwarf].assigned_jobs++; + } + } + } + + // Handle all skills except those marked HAULERS + + for (auto& labor : labors) + { + assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out); + } + + // Set about 1/3 of the dwarfs as haulers. The haulers have all HAULER labors enabled. Having a lot of haulers helps + // make sure that hauling jobs are handled quickly rather than building up. + + int num_haulers = state_count[IDLE] + (state_count[BUSY] + state_count[EXCLUSIVE]) * hauler_pct / 100; + + if (num_haulers < 1) + num_haulers = 1; + + std::vector hauler_ids; + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if ((dwarf_info[dwarf].trader && trader_requested) || + dwarf_info[dwarf].diplomacy) + { + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + if (labor_infos[labor].mode() != HAULERS) + continue; + dwarfs[dwarf]->status.labors[labor] = false; + } + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + { + num_haulers--; + } + continue; + } + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + hauler_ids.push_back(dwarf); + } + dwarfinfo_sorter sorter(dwarf_info); + // Idle dwarves come first, then we sort from least-skilled to most-skilled. + std::sort(hauler_ids.begin(), hauler_ids.end(), sorter); + + // don't set any haulers if everyone is off drinking or something + if (hauler_ids.size() == 0) { + num_haulers = 0; + } + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + if (labor_infos[labor].mode() != HAULERS) + continue; + + for (int i = 0; i < num_haulers; i++) + { + int dwarf = hauler_ids[i]; + + dwarfs[dwarf]->status.labors[labor] = true; + dwarf_info[dwarf].assigned_jobs++; + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + labor_infos[labor].active_dwarfs++; + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\" assigned {}: hauler\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor)); + } + + for (size_t i = num_haulers; i < hauler_ids.size(); i++) + { + int dwarf = hauler_ids[i]; + + dwarfs[dwarf]->status.labors[labor] = false; + } + } +} + +static void print_labor (df::unit_labor labor, color_ostream &out) +{ + std::string labor_name = ENUM_KEY_STR(unit_labor, labor); + out << labor_name << ": "; + for (int i = 0; i < 20 - (int)labor_name.length(); i++) + out << ' '; + if (labor_infos[labor].mode() == DISABLE) + out << "disabled" << std::endl; + else + { + if (labor_infos[labor].mode() == HAULERS) + out << "haulers"; + else + out << "minimum " << labor_infos[labor].minimum_dwarfs() << ", maximum " << labor_infos[labor].maximum_dwarfs() + << ", pool " << labor_infos[labor].talent_pool(); + out << ", currently " << labor_infos[labor].active_dwarfs << " dwarfs" << std::endl; + } +} + +bool LegacyEngine::command(color_ostream &out, std::vector & parameters) +{ + if (parameters.empty()) + return false; + if (!initialized) + { + out << "Error: The plugin is not enabled in legacy mode." << std::endl; + return true; + } + + if (parameters.size() == 2 && parameters[0] == "haulpct") + { + int pct = atoi (parameters[1].c_str()); + hauler_pct = pct; + return true; + } + else if (parameters.size() >= 2 && parameters.size() <= 4) + { + df::unit_labor labor = unit_labor::NONE; + + FOR_ENUM_ITEMS(unit_labor, test_labor) + { + if (parameters[0] == ENUM_KEY_STR(unit_labor, test_labor)) + labor = test_labor; + } + + if (labor == unit_labor::NONE) + { + out.printerr("Could not find labor {}.\n", parameters[0]); + return true; + } + + if (parameters[1] == "haulers") + { + labor_infos[labor].set_mode(HAULERS); + print_labor(labor, out); + return true; + } + if (parameters[1] == "disable") + { + labor_infos[labor].set_mode(DISABLE); + print_labor(labor, out); + return true; + } + if (parameters[1] == "reset") + { + reset_labor(labor); + print_labor(labor, out); + return true; + } + + int minimum = atoi (parameters[1].c_str()); + int maximum = 200; + int pool = 200; + + if (parameters.size() >= 3) + maximum = atoi (parameters[2].c_str()); + if (parameters.size() == 4) + pool = std::stoi(parameters[3]); + + if (maximum < minimum || maximum < 0 || minimum < 0) + { + out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); + return true; + } + + labor_infos[labor].set_minimum_dwarfs(minimum); + labor_infos[labor].set_maximum_dwarfs(maximum); + labor_infos[labor].set_talent_pool(pool); + labor_infos[labor].set_mode(AUTOMATIC); + print_labor(labor, out); + + return true; + } + else if (parameters.size() == 1 && parameters[0] == "reset-all") + { + for (size_t i = 0; i < labor_infos.size(); i++) + { + reset_labor((df::unit_labor) i); + } + out << "All labors reset." << std::endl; + return true; + } + else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) + { + bool need_comma = 0; + for (int i = 0; i < NUM_STATE; i++) + { + if (state_count[i] == 0) + continue; + if (need_comma) + out << ", "; + out << state_count[i] << ' ' << state_names[i]; + need_comma = 1; + } + out << std::endl; + + if (parameters[0] == "list") + { + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + print_labor(labor, out); + } + } + + return true; + } + + return false; +} diff --git a/plugins/autolabor/legacyengine.h b/plugins/autolabor/legacyengine.h new file mode 100644 index 0000000000..28336cf135 --- /dev/null +++ b/plugins/autolabor/legacyengine.h @@ -0,0 +1,24 @@ +#pragma once + +#include "engines.h" +#include "workdetails.h" + +// The classic autolabor engine: decides which units should have each labor +// and writes unit->status.labors directly. While enabled it sets +// game->external_flag.automatic_professions_disabled so the vanilla work +// detail system does not fight the plugin's writes. +class LegacyEngine : public LaborEngine { +public: + LegacyEngine(WorkDetailManager *wdm) : wdm(wdm) {} + + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + +private: + WorkDetailManager *wdm; + bool initialized = false; +}; diff --git a/plugins/autolabor/modernengine.cpp b/plugins/autolabor/modernengine.cpp new file mode 100644 index 0000000000..0fc7af63e2 --- /dev/null +++ b/plugins/autolabor/modernengine.cpp @@ -0,0 +1,2554 @@ +/* + * Modern engine for the autolabor plugin ("labormanager" command). + * + * Expresses labor policy through DF's work detail system rather than writing + * the labor matrix directly. The v50+ job auction already prefers skilled + * workers for skilled jobs, so this engine focuses on what the auction does + * badly: starving jobs that require no skill, and giving skilled dwarves + * room to practice and teach. + */ + +#include "joblabormapper.h" +#include "modernengine.h" +#include "monitorengine.h" + +#include +#include +#include +#include +#include +#include + +#include "Debug.h" +#include "MiscUtils.h" +#include "PluginManager.h" + +#include "modules/Job.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; +using df::global::game; + +namespace DFHack { + DBG_DECLARE(autolabor, modern_cycle, DebugCategory::LINFO); + DBG_DECLARE(autolabor, tool_detail, DebugCategory::LINFO); + DBG_DECLARE(autolabor, assign, DebugCategory::LINFO); + DBG_EXTERN(autolabor, labor_probe); +} + +using namespace autolabor; + +static const int32_t CYCLE_TICKS = 61; +static int32_t cycle_timestamp = 0; + +// a posting that sits unclaimed for this many cycles counts as starving +static const int STARVE_CYCLES = 5; +// a posting on the board longer than this (one game day) triggers a +// task starvation warning in the DFHack notification panel +static const int32_t STARVE_TICKS = 1200; +// specialists work this many consecutive cycles before earning rest +static const int BUSY_LIMIT = 10; + +static const std::string LABORER_DETAIL = "auto:Laborers"; +static const std::string CARE_DETAIL = "auto:Care"; +static const std::string SKILL_PREFIX = "auto:Skill:"; + +// labors that should stay available to every unit even when specialized; +// these are covered by the Care detail so flag-restricted units keep them +static bool is_care_labor(df::unit_labor l) +{ + switch (l) { + case unit_labor::RECOVER_WOUNDED: + case unit_labor::FEED_WATER_CIVILIANS: + case unit_labor::HAUL_WATER: + return true; + default: + return false; + } +} + +enum tools_enum { + TOOL_NONE, TOOL_PICK, TOOL_AXE, TOOL_CROSSBOW, + TOOLS_MAX +}; + +static tools_enum labor_tool(df::unit_labor l) +{ + switch (l) { + case unit_labor::MINE: return TOOL_PICK; + case unit_labor::CUTWOOD: return TOOL_AXE; + case unit_labor::HUNT: return TOOL_CROSSBOW; + default: return TOOL_NONE; + } +} + +// a skill whose labors include a tool labor (mining, woodcutting, +// hunting); toggling these makes the unit drop and re-equip its tool +static bool is_tool_skill(df::job_skill s) +{ + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && labor_to_skill[l] == s && + is_exclusive_labor(l)) + return true; + return false; +} + +// a work detail whose icon marks it as one of the tool-carrying details: +// the builtin Miners/Woodcutters/Hunters, or a managed fallback for one +// of those skills (managed details reuse the same icons) +static bool is_tool_detail(df::work_detail *wd) +{ + if (!wd) + return false; + switch (wd->icon) { + case work_detail_icon_type::MINERS: + case work_detail_icon_type::WOODCUTTERS: + case work_detail_icon_type::HUNTERS: + return true; + default: + return false; + } +} + +namespace autolabor { + +void modern_labor_coverage(std::vector &missing) +{ + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + // the exclusive-labor check and the tool-cap mapping must agree; + // a labor that needs a tool without being flagged would let the + // engine oversubscribe equipment + if (is_exclusive_labor(l) != (labor_tool(l) != TOOL_NONE)) + missing.push_back(ENUM_KEY_STR(unit_labor, l) + + ": is_exclusive_labor/labor_tool disagree"); + } +} + +} // namespace autolabor + +// --------------------------------------------------------------------------- +// per-labor managed flag ("autolabor/modern/labors/", ival(0) != 0 managed) +// --------------------------------------------------------------------------- + +static bool labor_managed(df::unit_labor l) +{ + std::stringstream key; + key << "autolabor/modern/labors/" << int(l); + auto item = World::GetPersistentSiteData(key.str()); + return !item.isValid() || item.ival(0) != 0; +} + +static void set_labor_managed(df::unit_labor l, bool managed) +{ + std::stringstream key; + key << "autolabor/modern/labors/" << int(l); + auto item = World::GetPersistentSiteData(key.str(), true); + if (item.isValid()) + item.ival(0) = managed ? 1 : 0; +} + +// migrate "unmanaged" marks from the old labormanager/2.0 config once +static void migrate_old_config() +{ + auto marker = World::GetPersistentSiteData("autolabor/modern/migrated", true); + if (!marker.isValid() || marker.ival(0) == 1) + return; + marker.ival(0) = 1; + + std::vector items; + World::GetPersistentWorldData(&items, "labormanager/2.0/labors/", true); + for (auto &p : items) + { + std::string key = p.key(); + int labor = atoi(key.substr(strlen("labormanager/2.0/labors/")).c_str()); + // old config: maximum_dwarfs in ival(2), -1 meant unmanaged + if (labor >= 0 && labor < NUM_LABORS && p.ival(2) == -1) + set_labor_managed((df::unit_labor)labor, false); + } +} + +// --------------------------------------------------------------------------- +// per-unit scan info +// --------------------------------------------------------------------------- + +// how a unit's unmet needs (personality.needs, focus_level < 0) bias the +// role assignment. pull values are scaled 0..50. +struct NeedBias { + int laborer_pull = 0; // hauling/care work satisfies the need + int idle_pull = 0; // needs downtime (socialize, rest) + std::map favored; // skills that satisfy unmet needs +}; + +struct UnitInfo { + df::unit *u = nullptr; + dwarf_state state = OTHER; + df::job_skill skill = job_skill::NONE; // top Normal/Medical skill + int rating = 0; + int total = 0; + int noble_penalty = 0; + bool trader = false; + bool diplomacy = false; + bool excluded = false; // cannot take assignments at all + NeedBias needs; +}; + +// per-unit persistent tracking for guild-hall rest rotation +struct SpecTrack { + int busy_streak = 0; + int rest_left = 0; + // the skill detail the unit was last assigned to; tool labors stick to + // their holders so they don't drop and re-equip tools between cycles + df::job_skill detail_skill = df::job_skill::NONE; +}; + +static JobLaborMapper *labor_mapper = nullptr; + +// per-posting tracking: how long it's been on the board and which skill +// the job exercises (job pointers go bad once a posting is dead, so the +// skill is recorded up front) +struct PostingTrack { + int32_t first_seen = 0; + df::job_skill skill = df::job_skill::NONE; +}; +static std::map posting_track; // posting idx -> info + +// job-history skill usage, decayed per cycle so recently-used skills +// outweigh ones the fort abandoned +static std::map skill_usage; +static std::map unclaimed_streak; // labor -> cycles +// oldest unclaimed posting per labor; the job pointer is only valid within +// the cycle that scan_demand filled it (jobs aren't stable across cycles) +static std::map unclaimed_job; +static std::map unclaimed_age; + +// labor-mismatch escalation state, keyed by the mapped labor. When a posting +// starves despite eligible units holding its mapped labor, we warn and then +// enable the disputed labors on idle dwarves one at a time; whichever labor +// unlocks the job identifies the labor the game actually gates it on. +struct LaborEscalation { + int32_t job_id = -1; + df::job_type jtype = df::job_type::NONE; + std::string job_name; + std::vector candidates; + size_t next = 0; + std::map> writes; // unit -> labors we set +}; +static std::map labor_escalations; +// job ids that already completed a probe round; a posting that can never +// be claimed would otherwise re-escalate every cycle and probe forever +static std::set probed_jobs; +static std::map labor_work; // live postings per labor, claimed or not +// tool-detail membership as of the end of the last applied cycle; diffs +// flag assignments made outside our reconcile calls (game or player) +static std::map> tool_snapshot; +static std::map unit_track; // unit id -> tracking +static std::vector managed_labor_cache; + +// status for the overlay +static int stat_laborers = 0; +static int stat_specialists = 0; +static int stat_resting = 0; +static int stat_generalists = 0; + +// job-board starvation stats (since the engine was last enabled) +static int stat_starving = 0; // live postings older than STARVE_TICKS +static int32_t stat_oldest_wait = 0; // age of the oldest live posting +static df::job_type stat_oldest_job = df::job_type::NONE; // oldest posting's job +static std::string stat_oldest_reaction; // custom reaction code, if any +static df::coord stat_oldest_pos; // oldest posting's job location +static int stat_resolved = 0; // postings that left the board +static int64_t stat_resolved_ticks = 0; // total ticks they spent on the board +static int32_t stat_max_wait = 0; // longest time on the board + +// workshop profile constraints collected during the demand scan: a job at +// a shop with a permitted-worker list needs a listed worker assigned the +// job's labor; a nondefault skill range needs an in-range worker +struct ShopNeed { + df::unit_labor labor = df::unit_labor::NONE; + std::set permitted; // empty = no worker restriction + int32_t min_level = 0; + int32_t max_level = df::skill_rating::Legendary5; +}; +static std::vector shop_needs; +// unit id -> skills exercised by jobs at shops where the unit is on the +// permitted list; units on any list are dispreferred for other labors so +// they stay available for their shop's work +static std::map> shop_reserved; +static std::set permitted_anywhere; + +static bool has_worker(df::job *j) +{ + for (auto ref : j->general_refs) + if (ref->getType() == df::general_ref_type::UNIT_WORKER) + return true; + return false; +} + +// --------------------------------------------------------------------------- +// working-state persistence +// --------------------------------------------------------------------------- + +// The engine's accumulated state (posting ages, skill usage history, +// starvation streaks, guild rest rotation, stats) is written to site data +// every cycle so it's included whenever the fort is saved, then restored +// on enable after a load. + +// does work remain for the skill? any live posting (claimed or not) for +// the skill's labors, the unit's own current job mapping to it, or a +// PickupEquipment job (a tool job suspends into it while the unit fetches +// its tool, and the claimed posting may not be visible to either other +// test then) +static bool skill_demand_remains(df::job_skill skill, df::unit *u) +{ + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && labor_to_skill[l] == skill && + labor_work[l] > 0) + return true; + if (u->job.current_job) + { + if (u->job.current_job->job_type == df::job_type::PickupEquipment) + return true; + df::unit_labor jl = labor_mapper->find_job_labor(u->job.current_job); + if (jl >= 0 && jl < NUM_LABORS && labor_to_skill[jl] == skill) + return true; + } + return false; +} + +// remove the labors an escalation experiment enabled and rebuild each +// affected unit's labor matrix from its work details +static void revert_escalation(LaborEscalation &esc) +{ + for (auto &kv : esc.writes) + if (auto u = df::unit::find(kv.first)) + { + for (auto m : kv.second) + u->status.labors[m] = false; + Units::setAutomaticProfessions(u); + } + esc.writes.clear(); +} + +static std::vector split_str(const std::string &s, char delim) +{ + std::vector out; + std::string cur; + for (char c : s) + { + if (c == delim) + { + out.push_back(cur); + cur.clear(); + } + else + cur += c; + } + out.push_back(cur); + return out; +} + +static void save_state() +{ + auto p = World::GetPersistentSiteData("autolabor/engine-state", true); + if (!p.isValid()) + return; + + std::stringstream ss; + ss << "stats:" << stat_resolved << ',' << stat_resolved_ticks << ',' + << stat_max_wait << ';'; + ss << "post:"; + for (auto &kv : posting_track) + ss << kv.first << ',' << kv.second.first_seen << ',' + << (int)kv.second.skill << '|'; + ss << ";usage:"; + for (auto &kv : skill_usage) + ss << (int)kv.first << ',' << int(kv.second * 1000) << '|'; + ss << ";streak:"; + for (auto &kv : unclaimed_streak) + ss << kv.first << ',' << kv.second << '|'; + ss << ";track:"; + for (auto &kv : unit_track) + ss << kv.first << ',' << kv.second.busy_streak << ',' + << kv.second.rest_left << ',' << (int)kv.second.detail_skill + << '|'; + p.val() = ss.str(); +} + +static void restore_state() +{ + auto p = World::GetPersistentSiteData("autolabor/engine-state"); + if (!p.isValid()) + return; + + for (auto &sec : split_str(p.val(), ';')) + { + size_t colon = sec.find(':'); + if (colon == std::string::npos) + continue; + std::string name = sec.substr(0, colon); + std::string body = sec.substr(colon + 1); + + if (name == "stats") + { + auto f = split_str(body, ','); + if (f.size() == 3) + { + stat_resolved = atoi(f[0].c_str()); + stat_resolved_ticks = atoll(f[1].c_str()); + stat_max_wait = atoi(f[2].c_str()); + } + } + else if (name == "post") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 3) + { + PostingTrack t; + t.first_seen = atoi(f[1].c_str()); + t.skill = (df::job_skill)atoi(f[2].c_str()); + posting_track[atoi(f[0].c_str())] = t; + } + } + } + else if (name == "usage") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 2) + skill_usage[(df::job_skill)atoi(f[0].c_str())] = + atoi(f[1].c_str()) / 1000.0; + } + } + else if (name == "streak") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 2) + unclaimed_streak[atoi(f[0].c_str())] = atoi(f[1].c_str()); + } + } + else if (name == "track") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() >= 3) + { + SpecTrack t; + t.busy_streak = atoi(f[1].c_str()); + t.rest_left = atoi(f[2].c_str()); + if (f.size() > 3) + t.detail_skill = + (df::job_skill)atoi(f[3].c_str()); + unit_track[atoi(f[0].c_str())] = t; + } + } + } + } +} + +void ModernEngine::enable(color_ostream &out) +{ + game->external_flag.bits.automatic_professions_disabled = false; + + if (!labor_mapper) + labor_mapper = new JobLaborMapper(); + init_labor_to_skill(); + migrate_old_config(); + + managed_labor_cache.assign(NUM_LABORS, true); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE) + managed_labor_cache[l] = labor_managed(l); + + wdm->reset(); + posting_track.clear(); + skill_usage.clear(); + unclaimed_streak.clear(); + unclaimed_job.clear(); + unclaimed_age.clear(); + for (auto &kv : labor_escalations) + revert_escalation(kv.second); + labor_escalations.clear(); + probed_jobs.clear(); + unit_track.clear(); + cycle_timestamp = 0; + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; + restore_state(); + initialized = true; + + out << "Enabling labormanager (work detail mode)." << std::endl; +} + +void ModernEngine::disable(color_ostream &out) +{ + initialized = false; + for (auto &kv : labor_escalations) + revert_escalation(kv.second); + labor_escalations.clear(); + wdm->shutdown(); + out << "Disabling labormanager." << std::endl; +} + +void ModernEngine::map_unload() +{ + initialized = false; + posting_track.clear(); + skill_usage.clear(); + unclaimed_streak.clear(); + unclaimed_job.clear(); + unclaimed_age.clear(); + labor_escalations.clear(); + probed_jobs.clear(); + unit_track.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; +} + +int ModernEngine::starving_jobs() +{ + return initialized ? stat_starving : 0; +} + +df::job_type ModernEngine::oldest_starving_job() +{ + return initialized && stat_starving > 0 ? stat_oldest_job + : df::job_type::NONE; +} + +int32_t ModernEngine::oldest_starving_wait() +{ + return initialized && stat_starving > 0 ? stat_oldest_wait : 0; +} + +df::coord ModernEngine::oldest_starving_pos() +{ + return initialized && stat_starving > 0 ? stat_oldest_pos + : df::coord(); +} + +// resolve a custom reaction's code to the reaction's own name; "" if the +// code is empty or the reaction can't be found +static std::string reaction_display_name(const std::string &code) +{ + if (!code.empty()) + for (auto r : df::reaction::get_vector()) + if (r->code == code && !r->name.empty()) + return r->name; + return ""; +} + +// display name for a job: for custom reactions, resolve the job's reaction +// code to the reaction's own name +static std::string job_display_name(df::job *j) +{ + if (j->job_type == df::job_type::CustomReaction) + { + std::string name = reaction_display_name(j->reaction_name); + if (!name.empty()) + return name; + } + const char *cap = ENUM_ATTR(job_type, caption, j->job_type); + return cap ? cap : ENUM_KEY_STR(job_type, j->job_type); +} + +// display name for the oldest starving job: for custom reactions, resolve +// the job's reaction code to the reaction's own name +static std::string oldest_starving_name() +{ + if (stat_oldest_job == df::job_type::CustomReaction) + { + std::string name = reaction_display_name(stat_oldest_reaction); + if (!name.empty()) + return name; + } + const char *cap = ENUM_ATTR(job_type, caption, stat_oldest_job); + return cap ? cap : ENUM_KEY_STR(job_type, stat_oldest_job); +} + +std::string ModernEngine::oldest_starving_name() +{ + return initialized && stat_starving > 0 ? ::oldest_starving_name() : ""; +} + +std::string ModernEngine::status_line() +{ + std::stringstream ss; + ss << stat_laborers << " laborers, " << stat_specialists << " specialists"; + if (stat_resting > 0) + ss << " (" << stat_resting << " on guild rest)"; + ss << ", " << stat_generalists << " generalists"; + return ss.str(); +} + +// --------------------------------------------------------------------------- +// demand scan +// --------------------------------------------------------------------------- + +static void reconcile(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired); +static void reconcile_builtin(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired); +static df::work_detail_icon_type skill_icon(df::job_skill skill); + +static void scan_demand(std::map &backlog) +{ + backlog.clear(); + labor_work.clear(); + unclaimed_job.clear(); + unclaimed_age.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + shop_needs.clear(); + shop_reserved.clear(); + permitted_anywhere.clear(); + + // every unit on a workshop's permitted list is reserved for that + // shop's work; the labors each shop actually needs come from the + // postings below + for (auto b : world->buildings.all) + { + df::workshop_profile *prof = b->getWorkshopProfile(); + if (prof && !prof->permitted_workers.empty()) + permitted_anywhere.insert(prof->permitted_workers.begin(), + prof->permitted_workers.end()); + } + + std::set live_postings; + + for (auto jp : world->jobs.postings) + { + df::job *j = jp->job; + if (jp->flags.bits.dead || !j) + { + TRACE(assign).print("posting {}: skipped, dead={} job={}\n", + jp->idx, (int)jp->flags.bits.dead, j ? "set" : "null"); + continue; + } + if (j->flags.bits.suspend || j->flags.bits.item_lost) + { + TRACE(assign).print("posting {}: {} skipped, suspend={} item_lost={}\n", + jp->idx, job_display_name(j), + (int)j->flags.bits.suspend, (int)j->flags.bits.item_lost); + continue; + } + + // work exists for this labor even when every posting is claimed; + // tool-labor stickiness keys off this so a unit fetching its tool + // or taking a break doesn't lose the detail and drop the tool + df::unit_labor labor = labor_mapper->find_job_labor(j); + if (labor >= 0 && labor < NUM_LABORS) + labor_work[labor]++; + + TRACE(assign).print("posting {}: {} -> labor {} ({})\n", + jp->idx, job_display_name(j), + labor >= 0 && labor < NUM_LABORS ? + ENUM_KEY_STR(unit_labor, labor) : "none", + has_worker(j) ? "claimed" : "unclaimed"); + + if (has_worker(j)) + continue; + + live_postings.insert(jp->idx); + + if (!posting_track.count(jp->idx)) + { + PostingTrack t; + t.first_seen = world->frame_counter; + if (labor >= 0 && labor < NUM_LABORS) + t.skill = labor_to_skill[labor]; + posting_track[jp->idx] = t; + } + + // starvation stats: how long has this posting been on the board + int32_t age = world->frame_counter - posting_track[jp->idx].first_seen; + if (age > stat_oldest_wait) + { + stat_oldest_wait = age; + stat_oldest_job = j->job_type; + stat_oldest_reaction = j->reaction_name; + stat_oldest_pos = j->pos; + } + if (age >= STARVE_TICKS) + stat_starving++; + + if (labor < 0 || labor >= NUM_LABORS) + continue; + backlog[labor]++; + if (age >= unclaimed_age[labor]) + { + unclaimed_age[labor] = age; + unclaimed_job[labor] = j; + } + + // a job at a shop with a permitted-worker list or a nondefault + // skill range needs a qualifying worker holding the labor + if (df::building *bld = Job::getHolder(j)) + { + df::workshop_profile *prof = bld->getWorkshopProfile(); + if (prof && (!prof->permitted_workers.empty() || + prof->min_level > 0 || + prof->max_level < df::skill_rating::Legendary5)) + { + ShopNeed need; + need.labor = labor; + need.permitted.insert(prof->permitted_workers.begin(), + prof->permitted_workers.end()); + need.min_level = prof->min_level; + need.max_level = prof->max_level; + shop_needs.push_back(need); + if (labor_to_skill[labor] != job_skill::NONE) + for (auto uid : prof->permitted_workers) + shop_reserved[uid].insert(labor_to_skill[labor]); + } + } + } + + // postings that left the board (assigned, cancelled, or suspended): + // record how long they spent on the board and the skill they exercised + for (auto it = posting_track.begin(); it != posting_track.end();) + { + if (!live_postings.count(it->first)) + { + int32_t wait = world->frame_counter - it->second.first_seen; + stat_resolved++; + stat_resolved_ticks += wait; + stat_max_wait = std::max(stat_max_wait, wait); + if (it->second.skill != df::job_skill::NONE) + skill_usage[it->second.skill] += 1.0; + it = posting_track.erase(it); + } + else + ++it; + } + + // decay usage so recently-used skills outweigh abandoned ones + for (auto &kv : skill_usage) + kv.second *= 0.995; + + // stockpile job counters catch hauling demand that hasn't spawned jobs yet + backlog[unit_labor::HAUL_STONE] += world->stockpile.num_jobs[1]; + backlog[unit_labor::HAUL_WOOD] += world->stockpile.num_jobs[2]; + backlog[unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[3]; + backlog[unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[4]; + backlog[unit_labor::HAUL_BODY] += world->stockpile.num_jobs[5]; + backlog[unit_labor::HAUL_FOOD] += world->stockpile.num_jobs[6]; + backlog[unit_labor::HAUL_REFUSE] += world->stockpile.num_jobs[7]; + backlog[unit_labor::HAUL_FURNITURE] += world->stockpile.num_jobs[8]; + backlog[unit_labor::HAUL_ANIMALS] += world->stockpile.num_jobs[9]; + + for (auto v : world->vehicles.all) + if (v->route_id != -1) + backlog[unit_labor::HANDLE_VEHICLES]++; + + // track how many consecutive cycles each labor has had unclaimed work + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + if (backlog[l] > 0) + unclaimed_streak[l]++; + else + unclaimed_streak[l] = 0; + if (backlog[l] > 0 || unclaimed_streak[l] > 0) + { + TRACE(assign).print("labor {}: {} unclaimed, streak {}, managed={}, {}\n", + ENUM_KEY_STR(unit_labor, l), backlog[l], unclaimed_streak[l], + (int)(l >= 0 && l < NUM_LABORS && managed_labor_cache[l]), + is_unskilled(l) ? "unskilled" : + ENUM_KEY_STR(job_skill, labor_to_skill[l])); + } + } +} + +// --------------------------------------------------------------------------- +// guild halls +// --------------------------------------------------------------------------- + +static std::set guilded_professions() +{ + std::set profs; + + auto site = df::world_site::find(plotinfo->site_id); + if (!site) + return profs; + + for (auto ab : site->buildings) + { + if (auto gh = virtual_cast(ab)) + { + df::profession p = gh->contents.profession; + if (p != profession::NONE) + profs.insert(p); + } + } + + return profs; +} + +// does this skill's profession (or an ancestor profession) have a guild hall? +static bool is_guilded(df::job_skill skill, const std::set &guilded) +{ + df::profession p = (df::profession)ENUM_ATTR(job_skill, profession, skill); + while (p != profession::NONE) + { + if (guilded.count(p)) + return true; + p = (df::profession)ENUM_ATTR(profession, parent, p); + } + return false; +} + +// --------------------------------------------------------------------------- +// tools +// --------------------------------------------------------------------------- + +static void count_tools(int tool_count[TOOLS_MAX]) +{ + for (int e = 0; e < TOOLS_MAX; e++) + tool_count[e] = 0; + + df::item_flags bad_flags; + bad_flags.whole = 0; +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); +#undef F + + for (auto item : world->items.other.IN_PLAY) + { + if (item->flags.whole & bad_flags.whole) + continue; + if (!item->isWeapon()) + continue; + + df::itemdef_weaponst* weapondef = ((df::item_weaponst*)item)->subtype; + if ((df::job_skill)weapondef->skill_melee == df::job_skill::MINING) + tool_count[TOOL_PICK]++; + else if ((df::job_skill)weapondef->skill_melee == df::job_skill::AXE) + tool_count[TOOL_AXE]++; + else if ((df::job_skill)weapondef->skill_ranged == df::job_skill::CROSSBOW) + tool_count[TOOL_CROSSBOW]++; + } +} + +// --------------------------------------------------------------------------- +// needs +// --------------------------------------------------------------------------- + +static int rating_in_skill(df::unit *u, df::job_skill skill) +{ + if (u->status.current_soul) + for (auto s : u->status.current_soul->skills) + if (s->id == skill) + return s->rating; + return 0; +} + +// craft labors satisfy the BeCreative / CraftObject / ThinkAbstractly needs +static const df::unit_labor craft_labors[] = { + unit_labor::WOOD_CRAFT, unit_labor::STONE_CRAFT, unit_labor::BONE_CARVE, + unit_labor::METAL_CRAFT, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::BOWYER, unit_labor::CLOTHESMAKER, unit_labor::WEAVER, + unit_labor::LEATHER, unit_labor::CUT_GEM, unit_labor::ENCRUST_GEM, + unit_labor::PAPERMAKING, unit_labor::BOOKBINDING, unit_labor::WAX_WORKING, + unit_labor::EXTRACT_STRAND, unit_labor::SPINNER, unit_labor::DYER, +}; + +// labors whose products carry a quality modifier that scales with the +// worker's skill -- skilled specialists matter most for these +static const df::unit_labor quality_labors[] = { + unit_labor::CARPENTER, unit_labor::STONECUTTER, unit_labor::STONE_CARVER, + unit_labor::ENGRAVER, unit_labor::MASON, unit_labor::MECHANIC, + unit_labor::WOOD_CRAFT, unit_labor::STONE_CRAFT, unit_labor::BONE_CARVE, + unit_labor::METAL_CRAFT, unit_labor::FORGE_WEAPON, unit_labor::FORGE_ARMOR, + unit_labor::FORGE_FURNITURE, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::GLAZING, unit_labor::BOWYER, unit_labor::CLOTHESMAKER, + unit_labor::LEATHER, unit_labor::CUT_GEM, unit_labor::ENCRUST_GEM, + unit_labor::SIEGECRAFT, unit_labor::COOK, unit_labor::PAPERMAKING, + unit_labor::BOOKBINDING, +}; + +// labors whose skills a strange mood can raise to legendary +static const df::unit_labor moodable_labors[] = { + unit_labor::CARPENTER, unit_labor::STONECUTTER, unit_labor::STONE_CARVER, + unit_labor::WOOD_CRAFT, unit_labor::BONE_CARVE, unit_labor::METAL_CRAFT, + unit_labor::FORGE_WEAPON, unit_labor::FORGE_ARMOR, + unit_labor::FORGE_FURNITURE, unit_labor::BOWYER, unit_labor::MECHANIC, + unit_labor::MASON, unit_labor::CLOTHESMAKER, unit_labor::WEAVER, + unit_labor::LEATHER, unit_labor::TANNER, unit_labor::CUT_GEM, + unit_labor::ENCRUST_GEM, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::GLAZING, unit_labor::WAX_WORKING, unit_labor::SMELT, + unit_labor::SIEGECRAFT, +}; + +static bool is_labor_skill(const df::unit_labor *labors, size_t count, + df::job_skill skill) +{ + for (size_t i = 0; i < count; i++) + if (labor_to_skill[labors[i]] == skill) + return true; + return false; +} + +// products of these skills carry skill-scaled quality modifiers +static bool is_quality_skill(df::job_skill skill) +{ + return is_labor_skill(quality_labors, + sizeof(quality_labors)/sizeof(df::unit_labor), skill); +} + +static bool has_moodable_skill(df::unit *u) +{ + for (auto l : moodable_labors) + if (rating_in_skill(u, labor_to_skill[l]) > 0) + return true; + return false; +} + +// frequently-used skills are worth specializing more aggressively +static int usage_bonus(df::job_skill skill) +{ + auto it = skill_usage.find(skill); + if (it == skill_usage.end()) + return 0; + return int(std::min(it->second, 20.0) * 5); // 0..100 +} + +static NeedBias scan_needs(df::unit *u) +{ + NeedBias bias; + if (!u->status.current_soul) + return bias; + + for (auto need : u->status.current_soul->personality.needs) + { + // focus_level runs -999 (desperate) to 400 (satisfied) + if (need->focus_level >= 0) + continue; + int urgency = std::min(-need->focus_level, 400) / 8; // 0..50 + if (!urgency) + continue; + + switch (need->id) + { + // staying occupied, acquiring things (stockpiling, hauling to the + // depot), and helping (patients, prisoners, wounded) are all + // satisfied by unskilled laborer work + case need_type::StayOccupied: + case need_type::AcquireObject: + bias.laborer_pull += urgency; + break; + // helping: patient care is laborer work, and leading guildhall + // demonstrations needs idle time + case need_type::HelpSomebody: + bias.laborer_pull += urgency; + bias.idle_pull += urgency / 2; + break; + // social needs are satisfied by free time; keep these units out of + // the restrictive pools so they can idle and socialize, and let + // guilded ones earn rest (and teach) sooner + case need_type::Socialize: + case need_type::MakeMerry: + case need_type::TakeItEasy: + case need_type::BeWithFamily: + case need_type::BeWithFriends: + case need_type::HearEloquence: + case need_type::LearnSomething: // watching demos is guild idle time + bias.idle_pull += urgency; + break; + // wandering satisfied by fishing, hunting, plant gathering + case need_type::Wander: + bias.favored[labor_to_skill[unit_labor::FISH]] += urgency; + bias.favored[labor_to_skill[unit_labor::HUNT]] += urgency; + bias.favored[labor_to_skill[unit_labor::HERBALIST]] += urgency; + break; + // excitement: catching vermin, danger (hunting); performing and + // socializing are downtime activities + case need_type::Excitement: + bias.favored[labor_to_skill[unit_labor::TRAPPER]] += urgency; + bias.favored[labor_to_skill[unit_labor::HUNT]] += urgency; + bias.idle_pull += urgency / 2; + break; + // creativity: artwork and crafts + case need_type::BeCreative: + case need_type::CraftObject: + case need_type::ThinkAbstractly: + for (auto l : craft_labors) + bias.favored[labor_to_skill[l]] += urgency; + break; + default: + break; + } + } + return bias; +} + +// --------------------------------------------------------------------------- +// main cycle +// --------------------------------------------------------------------------- + +void ModernEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + cycle_timestamp = world->frame_counter; + + auto bscan = scan_buildings(); + int tool_count[TOOLS_MAX]; + count_tools(tool_count); + + std::map backlog; + scan_demand(backlog); + + auto guilded = guilded_professions(); + + // members of tool details before this cycle's changes, for logging + std::set tool_members; + std::vector tool_details; + for (auto icon : {work_detail_icon_type::MINERS, + work_detail_icon_type::WOODCUTTERS, + work_detail_icon_type::HUNTERS}) + if (auto *wd = wdm->find_builtin(icon)) + tool_details.push_back(wd); + for (auto wd : wdm->managed_details()) + if (is_tool_detail(wd)) + tool_details.push_back(wd); + for (auto wd : tool_details) + { + tool_members.insert(wd->assigned_units.begin(), + wd->assigned_units.end()); + // diff against the post-apply snapshot: our own changes were + // already logged by reconcile, so anything here is an external + // write (the game or the player) that we need to see + std::set cur(wd->assigned_units.begin(), + wd->assigned_units.end()); + auto &snap = tool_snapshot[wd]; + for (int32_t id : cur) + if (!snap.count(id)) + { + TRACE(tool_detail).print( + "tool detail {}: +unit {} EXTERNAL\n", wd->name, id); + } + for (int32_t id : snap) + if (!cur.count(id)) + { + TRACE(tool_detail).print( + "tool detail {}: -unit {} EXTERNAL\n", wd->name, id); + } + } + + // --- collect citizens --------------------------------------------------- + + std::vector units; + for (auto cre : world->units.active) + { + if (!is_assignable(cre) || cre->burrows.size() > 0) + continue; + + UnitInfo info; + info.u = cre; + info.state = get_dwarf_state(cre); + info.rating = top_skill(cre, &info.skill); + info.total = total_skill(cre); + + auto pos = unit_position_info(cre); + info.noble_penalty = pos.noble_penalty; + info.trader = pos.trader && bscan.trader_requested; + info.diplomacy = in_diplomacy_meeting(cre); + + info.excluded = info.diplomacy || info.trader || + info.state == CHILD || info.state == MILITARY || + info.state == OTHER || cre->status2.limbs_grasp_count == 0; + + info.needs = scan_needs(cre); + + // observe units fetching a tool while the engine (or the player) + // has them in a tool detail; a member=0 line with a tool + // detail_skill means the detail assignment was already lost + if (cre->job.current_job && + cre->job.current_job->job_type == df::job_type::PickupEquipment) + { + auto tk = unit_track.find(cre->id); + df::job_skill pinned = tk != unit_track.end() ? + tk->second.detail_skill : job_skill::NONE; + bool member = tool_members.count(cre->id) > 0; + if (member || is_tool_skill(pinned)) + { + TRACE(tool_detail).print( + "unit {} ({}) PickupEquipment: detail_skill={} member={}\n", + cre->id, Units::getReadableName(cre), + pinned == job_skill::NONE ? "NONE" : + ENUM_KEY_STR(job_skill, pinned), + (int)member); + } + } + + probe_labor_observation(cre); + + units.push_back(info); + } + + if (units.empty()) + return; + + int n = units.size(); + std::map unit_idx; + for (int i = 0; i < n; i++) + unit_idx[units[i].u->id] = i; + + // --- priority weights from the balance slider --------------------------- + + double starve_w = double(NUM_BALANCE_STOPS - 1 - balance()) / (NUM_BALANCE_STOPS - 1); + double skill_w = double(balance()) / (NUM_BALANCE_STOPS - 1); + + // skills that map to a managed labor AND are actually used by the fort + // (per posting history); a citizen whose top skill has no labor (e.g. + // teaching) or whose skill is never exercised (gelders, animal + // dissectors in most forts) stays a generalist rather than getting a + // reserved detail + std::set viable_skills; + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && managed_labor_cache[l] && !is_unskilled(l) && + skill_usage[labor_to_skill[l]] >= 0.5) + viable_skills.insert(labor_to_skill[l]); + + { + std::string vs; + for (auto s : viable_skills) + { + if (!vs.empty()) + vs += ", "; + vs += ENUM_KEY_STR(job_skill, s); + } + TRACE(assign).print("viable skills ({}): {}\n", viable_skills.size(), vs); + } + + auto skill_allowed = [&](df::job_skill s) { + if (s == labor_to_skill[unit_labor::FISH]) + return config_flag(CF_ALLOW_FISHING) && bscan.has_fishery; + if (s == labor_to_skill[unit_labor::HUNT]) + return config_flag(CF_ALLOW_HUNTING) && bscan.has_butchers; + return true; + }; + + // units pinned to a skill detail keep the pin while work remains -- + // the laborer pool must not pull them out of their detail and abort + // a claimed job (or make a tool user drop their tool). demand is the + // gate, not viability: a freshly drafted unit has no usage history yet + std::set pinned; + for (auto &kv : unit_track) + { + df::job_skill pin = kv.second.detail_skill; + if (pin == job_skill::NONE || !skill_allowed(pin)) + continue; + auto it = unit_idx.find(kv.first); + if (it == unit_idx.end()) + continue; + UnitInfo &info = units[it->second]; + if (!info.excluded && skill_demand_remains(pin, info.u)) + pinned.insert(kv.first); + } + + // --- laborer pool sizing ------------------------------------------------ + + int unskilled_backlog = 0; + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE || !is_unskilled(l)) + continue; + if (!managed_labor_cache[l]) + continue; + int count = backlog[l]; + if (unclaimed_streak[l] >= STARVE_CYCLES) + count *= 2; // starving labors get doubled weight + unskilled_backlog += count; + } + + int laborer_target = 1 + unskilled_backlog / 2 + + int(std::round(n * 0.25 * starve_w)); + laborer_target = std::min(laborer_target, n); + + // --- pick laborers ------------------------------------------------------ + // pool score: lower is a better laborer; skilled dwarves are protected in + // proportion to the skill-priority weight + + std::vector order; + std::map pool_score; + for (int i = 0; i < n; i++) + { + if (units[i].excluded) + continue; + int score = 0; + switch (units[i].state) { + case IDLE: score = 0; break; + case BUSY: score = 200; break; + case EXCLUSIVE: score = 400; break; + default: score = 800; break; + } + score += units[i].total * 10; + // skilled specialists are protected in proportion to the skill + // weight; quality-affecting and frequently-used skills count extra + // since losing their time costs the fort more + int prot = units[i].rating * 40; + if (is_quality_skill(units[i].skill)) + prot = prot * 3 / 2; + prot += usage_bonus(units[i].skill); + score += int(prot * skill_w * 4); + score += units[i].noble_penalty / 100; + // units whose unmet needs are satisfied by hauling/patient care get + // pulled in; units needing social downtime get pushed out + score -= units[i].needs.laborer_pull * 4; + score += units[i].needs.idle_pull * 4; + // permitted workers stay available for their shop's jobs rather + // than getting drafted into the laborer pool + if (permitted_anywhere.count(units[i].u->id)) + score += 150; + pool_score[i] = score; + order.push_back(i); + } + std::sort(order.begin(), order.end(), + [&](int a, int b) { return pool_score[a] < pool_score[b]; }); + + std::set laborer_ids; + for (int i = 0; i < (int)order.size() && (int)laborer_ids.size() < laborer_target; i++) + if (!pinned.count(units[order[i]].u->id)) + laborer_ids.insert(units[order[i]].u->id); + + // --- specialists -------------------------------------------------------- + // every remaining citizen with a usable skill gets pinned to that skill's + // detail; guilded specialists rotate through rest periods + + std::map> skill_members; // skill -> unit ids + std::set specialist_ids; + stat_resting = 0; + + for (int i = 0; i < n; i++) + { + UnitInfo &info = units[i]; + + df::job_skill prev = job_skill::NONE; + auto tk = unit_track.find(info.u->id); + if (tk != unit_track.end()) + prev = tk->second.detail_skill; + + if (info.excluded || laborer_ids.count(info.u->id)) + { + if (tk != unit_track.end()) + tk->second.detail_skill = job_skill::NONE; + TRACE(assign).print( + "unit {} ({}): skipped, excluded={} laborer={}\n", + info.u->id, Units::getReadableName(info.u), + (int)info.excluded, (int)laborer_ids.count(info.u->id)); + continue; + } + + // pick the specialty. normally the unit's top skill; need-favored + // skills and fort usage stats can pull the pick elsewhere. + // mood-shaping: a unit that can still have a strange mood and has + // started a moodable craft gets steered toward moodable skills -- + // preferably valuable ones -- so a mood pays out in a skill the + // fort wants. needs a caste STRANGE_MOODS token and no spent mood. + bool mood_prot = + Units::casteFlagSet(info.u->race, info.u->caste, + df::caste_raw_flags::STRANGE_MOODS) && + !info.u->flags1.bits.had_mood && has_moodable_skill(info.u); + + df::job_skill chosen = job_skill::NONE; + + // assignment stickiness: a unit holding a skill detail keeps it + // while work for that skill remains, so claimed jobs aren't + // aborted by a rescore. the demand test covers the PickupEquipment + // window so tool users don't drop and re-equip mid-fetch + if (prev != job_skill::NONE && skill_allowed(prev)) + { + bool demand = skill_demand_remains(prev, info.u); + if (demand) + chosen = prev; + if (is_tool_skill(prev)) + { + TRACE(tool_detail).print( + "unit {} ({}) tool pin {}: allowed=1 demand={} job={} -> {}\n", + info.u->id, Units::getReadableName(info.u), + ENUM_KEY_STR(job_skill, prev), (int)demand, + info.u->job.current_job ? + ENUM_KEY_STR(job_type, + info.u->job.current_job->job_type) : "none", + chosen == prev ? "kept" : "released"); + } + else + { + TRACE(assign).print( + "unit {} ({}) pin {}: demand={} job={} -> {}\n", + info.u->id, Units::getReadableName(info.u), + ENUM_KEY_STR(job_skill, prev), (int)demand, + info.u->job.current_job ? + ENUM_KEY_STR(job_type, + info.u->job.current_job->job_type) : "none", + chosen == prev ? "kept" : "released"); + } + } + + int best_score = -1; + auto consider = [&](df::job_skill want, int bonus) { + if (want == job_skill::NONE || !viable_skills.count(want) || + !skill_allowed(want)) + return; + int r = rating_in_skill(info.u, want); + int score = r * 100 + bonus + usage_bonus(want) + + (is_quality_skill(want) ? r * 20 : 0); + // permitted workers are dispreferred for specialties their + // shop doesn't need and preferred for the ones it does + if (permitted_anywhere.count(info.u->id)) + score += shop_reserved[info.u->id].count(want) ? 150 : -150; + if (score > best_score) + { + best_score = score; + chosen = want; + } + }; + + if (chosen == job_skill::NONE) + { + if (mood_prot) + { + // candidates restricted to moodable skills; quality-affecting + // skills get a large bonus so the pick trends toward valuable + // mood outcomes, and need-favored moodable skills still count + for (auto l : moodable_labors) + { + df::job_skill want = labor_to_skill[l]; + int bonus = is_quality_skill(want) ? 300 : 100; + auto it = info.needs.favored.find(want); + if (it != info.needs.favored.end()) + bonus += it->second * 20; + consider(want, bonus); + } + } + else + { + if (info.skill != job_skill::NONE && info.rating > 0) + consider(info.skill, 0); + for (auto &kv : info.needs.favored) + consider(kv.first, kv.second * 20); + } + } + if (chosen == job_skill::NONE) + { + // drop any stale tool-labor pin + if (tk != unit_track.end()) + tk->second.detail_skill = job_skill::NONE; + TRACE(assign).print( + "unit {} ({}): no specialty (top={} rating={} mood_prot={})\n", + info.u->id, Units::getReadableName(info.u), + info.skill == job_skill::NONE ? "NONE" : + ENUM_KEY_STR(job_skill, info.skill), + info.rating, (int)mood_prot); + continue; + } + TRACE(assign).print("unit {} ({}): specialist {}\n", + info.u->id, Units::getReadableName(info.u), + ENUM_KEY_STR(job_skill, chosen)); + + specialist_ids.insert(info.u->id); + + // guild-hall rest rotation; units with unmet social/helping needs + // earn rest (and the chance to teach or learn) sooner + SpecTrack &track = unit_track[info.u->id]; + bool guilded_unit = is_guilded(chosen, guilded); + if (guilded_unit) + { + int busy_limit = std::max(4, BUSY_LIMIT - info.needs.idle_pull / 5); + if (info.u->job.current_job) + track.busy_streak++; + else + track.busy_streak = 0; + + if (track.rest_left > 0) + { + track.rest_left--; + stat_resting++; + continue; // resting: keep flag, join no skill detail + } + if (track.busy_streak >= busy_limit) + { + int reserve = idle_reserve(); + track.rest_left = std::max(1, + busy_limit * reserve / std::max(1, 100 - reserve)); + track.busy_streak = 0; + stat_resting++; + continue; + } + } + + skill_members[chosen].push_back(info.u->id); + track.detail_skill = chosen; + } + + // --- apprentices -------------------------------------------------------- + // sustained backlog on a skilled labor: pull in unskilled generalists so + // the job gets done and they train up + + bool any_starving = false; + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && unclaimed_streak[l] >= STARVE_CYCLES && + managed_labor_cache[l] && !is_unskilled(l)) + { + any_starving = true; + TRACE(assign).print("starving labor {}: streak {}, backlog {}, managed={}\n", + ENUM_KEY_STR(unit_labor, l), unclaimed_streak[l], backlog[l], + (int)(l < NUM_LABORS && managed_labor_cache[l])); + } + + for (int i = 0; i < n; i++) + { + UnitInfo &info = units[i]; + // laborers are eligible: a starving skilled labor outranks hauling, + // and a big unskilled backlog could otherwise fill the pool with + // everyone and leave nobody to draft + if (info.excluded || specialist_ids.count(info.u->id)) + { + if (any_starving) + { + TRACE(assign).print( + "unit {} ({}): apprentice skip, excluded={} specialist={}\n", + info.u->id, Units::getReadableName(info.u), + (int)info.excluded, + (int)specialist_ids.count(info.u->id)); + } + continue; + } + if (info.state != IDLE && info.state != BUSY) + { + if (any_starving) + { + TRACE(assign).print( + "unit {} ({}): apprentice skip, state={}\n", + info.u->id, Units::getReadableName(info.u), + (int)info.state); + } + continue; + } + + // find the most-starving skilled labor this unit could learn + df::job_skill want = job_skill::NONE; + int worst_streak = STARVE_CYCLES; + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE || !managed_labor_cache[labor] || + is_unskilled(labor)) + continue; + if (unclaimed_streak[labor] < worst_streak) + continue; + df::job_skill skill = labor_to_skill[labor]; + tools_enum tool = labor_tool(labor); + if (tool != TOOL_NONE && + (int)skill_members[skill].size() >= tool_count[tool]) + continue; + if (!Units::isValidLabor(info.u, labor)) + continue; + want = skill; + worst_streak = unclaimed_streak[labor]; + } + if (want != job_skill::NONE) + { + skill_members[want].push_back(info.u->id); + laborer_ids.erase(info.u->id); + specialist_ids.insert(info.u->id); // restrict them to the detail + // record the pin so tool-labor drafts survive the fetch cycle: + // once they claim the posting it leaves the unclaimed backlog + // and this pass would not pick them again + unit_track[info.u->id].detail_skill = want; + TRACE(assign).print("unit {} ({}): apprentice {}\n", + info.u->id, Units::getReadableName(info.u), + ENUM_KEY_STR(job_skill, want)); + } + else if (any_starving) + { + TRACE(assign).print( + "unit {} ({}): eligible but no draftable starving labor\n", + info.u->id, Units::getReadableName(info.u)); + } + } + + // starving labors that still have no members in their skill detail + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE || unclaimed_streak[l] < STARVE_CYCLES || + !managed_labor_cache[l] || is_unskilled(l)) + continue; + df::job_skill sk = labor_to_skill[l]; + if (skill_members[sk].empty()) + { + int eligible = 0; + for (auto &u : units) + if (!u.excluded && Units::isValidLabor(u.u, l)) + eligible++; + TRACE(assign).print( + "starving labor {}: no units drafted into {} " + "({} eligible citizens)\n", + ENUM_KEY_STR(unit_labor, l), ENUM_KEY_STR(job_skill, sk), + eligible); + } + } + + // --- workshop restrictions ---------------------------------------------- + // a posting at a shop with a permitted-worker list needs a listed worker + // holding the job's labor; a nondefault skill range needs an in-range + // holder. only managed skilled labors need enforcement -- unskilled and + // unmanaged labors stay available to everyone via the EverybodyDoesThis + // details + + for (auto &need : shop_needs) + { + df::unit_labor labor = need.labor; + if (labor < 0 || labor >= NUM_LABORS || !managed_labor_cache[labor] || + is_unskilled(labor)) + continue; + df::job_skill skill = labor_to_skill[labor]; + if (skill == job_skill::NONE) + continue; + + auto &members = skill_members[skill]; + auto is_member = [&](int32_t uid) { + return std::find(members.begin(), members.end(), uid) != + members.end(); + }; + auto in_range = [&](int i) { + int r = rating_in_skill(units[i].u, skill); + return r >= need.min_level && r <= need.max_level; + }; + + bool permit_ok = need.permitted.empty(); + bool range_ok = need.min_level <= 0 && + need.max_level >= df::skill_rating::Legendary5; + TRACE(assign).print( + "shop need: labor {}, permitted={}, range [{},{}], members={}\n", + ENUM_KEY_STR(unit_labor, labor), need.permitted.size(), + need.min_level, need.max_level, members.size()); + for (auto uid : members) + { + auto it = unit_idx.find(uid); + if (it == unit_idx.end()) + continue; + if (!permit_ok && need.permitted.count(uid)) + permit_ok = true; + if (!range_ok && in_range(it->second)) + range_ok = true; + } + if (permit_ok && range_ok) + continue; + + // pick the best qualifying citizen; satisfy each constraint with a + // unit that also covers the other one when possible + auto satisfy = [&](bool must_permit, bool must_range) { + int best = -1, best_i = -1; + for (int i = 0; i < n; i++) + { + if (units[i].excluded || is_member(units[i].u->id) || + !Units::isValidLabor(units[i].u, labor)) + continue; + if (must_permit && !need.permitted.count(units[i].u->id)) + continue; + if (must_range && !in_range(i)) + continue; + int score = rating_in_skill(units[i].u, skill) * 100; + if (need.permitted.count(units[i].u->id)) + score += 500; + if (in_range(i)) + score += 300; + if (score > best) + { + best = score; + best_i = i; + } + } + if (best_i < 0) + { + TRACE(assign).print( + "shop need {}: no qualifying citizen (permit={} range={})\n", + ENUM_KEY_STR(unit_labor, labor), (int)must_permit, + (int)must_range); + return; + } + int32_t uid = units[best_i].u->id; + TRACE(assign).print("shop need {}: drafted unit {} ({})\n", + ENUM_KEY_STR(unit_labor, labor), uid, + Units::getReadableName(units[best_i].u)); + members.push_back(uid); + specialist_ids.insert(uid); + unit_track[uid].detail_skill = skill; + // shop work takes priority over the laborer pool + laborer_ids.erase(uid); + }; + + if (!permit_ok) + satisfy(true, false); + // the permitted pick may have covered the range too + if (!range_ok) + { + for (auto uid : members) + { + auto it = unit_idx.find(uid); + if (it != unit_idx.end() && in_range(it->second)) + { + range_ok = true; + break; + } + } + if (!range_ok) + satisfy(false, true); + } + } + + // --- apply -------------------------------------------------------------- + + std::set specialized_ids = laborer_ids; + specialized_ids.insert(specialist_ids.begin(), specialist_ids.end()); + + // Laborers: unskilled labors for the pool (Everybody so generalists keep + // them too; membership is what matters for flag-restricted units) + auto *wd_laborers = wdm->ensure_detail(LABORER_DETAIL, + work_detail_icon_type::HAULERS, work_detail_mode::EverybodyDoesThis); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && is_unskilled(l) && managed_labor_cache[l]) + wdm->cover(wd_laborers, l, true); + reconcile(wdm, wd_laborers, laborer_ids); + + // Care: safety labors for all specialized units + auto *wd_care = wdm->ensure_detail(CARE_DETAIL, + work_detail_icon_type::ORDERLIES, work_detail_mode::EverybodyDoesThis); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && is_care_labor(l) && managed_labor_cache[l]) + wdm->cover(wd_care, l, true); + reconcile(wdm, wd_care, specialized_ids); + + // per-skill details + std::set live_skill_details; + for (auto &kv : skill_members) + { + df::job_skill skill = kv.first; + std::set members(kv.second.begin(), kv.second.end()); + if (members.empty()) + continue; + + // in v50 only the predefined details trigger tool equipping, so + // tool-skill specialists go into the builtin Miners/Woodcutters/ + // Hunters detail rather than a managed one (falls back to a + // managed detail if the builtin is somehow missing) + if (is_tool_skill(skill)) + { + if (auto *wd = wdm->find_builtin(skill_icon(skill))) + { + reconcile_builtin(wdm, wd, members); + continue; + } + } + + const char *cap = ENUM_ATTR(job_skill, caption_noun, skill); + std::string name = SKILL_PREFIX + (cap ? cap : ENUM_KEY_STR(job_skill, skill)); + live_skill_details.insert(name); + TRACE(assign).print("detail {}: {} members\n", name, members.size()); + + auto *wd = wdm->ensure_detail(name, skill_icon(skill), + work_detail_mode::OnlySelectedDoesThis); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && managed_labor_cache[l] && + labor_to_skill[l] == skill) + wdm->cover(wd, l, true); + reconcile(wdm, wd, members); + } + + // delete skill details that no longer have members + for (auto wd : wdm->managed_details()) + if (wd->name.compare(0, SKILL_PREFIX.size(), SKILL_PREFIX) == 0 && + !live_skill_details.count(wd->name)) + wdm->delete_detail(wd); + + // specialization flags + for (int i = 0; i < n; i++) + wdm->set_specialized(units[i].u, specialized_ids.count(units[i].u->id)); + // also clear flags on units no longer assignable (burrows, gone military) + for (int32_t id : std::set(wdm->flagged_units())) + if (auto u = df::unit::find(id)) + if (!is_assignable(u) || u->burrows.size() > 0) + wdm->set_specialized(u, false); + + wdm->commit(); + + // snapshot tool-detail memberships post-apply so next cycle's diff + // only flags writes that happened outside the engine + for (auto wd : tool_details) + tool_snapshot[wd] = std::set(wd->assigned_units.begin(), + wd->assigned_units.end()); + + // --- labor mapping mismatch escalation --------------------------------- + // A posting that starves while an eligible unit holds its mapped labor + // suggests the mapping is wrong (or every enabled holder is somehow + // ineligible). Warn once, then enable labors on idle dwarves one per + // cycle until the job is claimed; whichever labor unlocks it exposes the + // labor the game actually gates the job on. + + FOR_ENUM_ITEMS(unit_labor, l) + { + // tool labors are never probed: enabling one would send the unit + // chasing a pick/axe/crossbow it doesn't need. and a posting must be + // at least 50 ticks old before it can count as starved: some of the + // game's labor assignment passes only run on 50-tick boundaries + if (l == unit_labor::NONE || !managed_labor_cache[l] || + is_unskilled(l) || is_exclusive_labor(l) || backlog[l] == 0 || + unclaimed_streak[l] < STARVE_CYCLES || unclaimed_age[l] < 50 || + labor_escalations.count(l) || !unclaimed_job.count(l)) + continue; + // if every unit holding the labor is busy on another job, the + // posting is starved by staffing, not by a mapping problem -- + // only probe when an enabled holder is actually free to take it + int enabled = 0, idle_enabled = 0; + for (auto &info : units) + if (!info.excluded && info.u->status.labors[l]) + { + enabled++; + if (info.state == IDLE) + idle_enabled++; + } + if (!idle_enabled) + continue; + df::job *j = unclaimed_job[l]; + // custom reactions map through the reaction's declared skill, which + // the game always honors; there is nothing for probing to discover. + // PlantSeeds/HarvestPlants are likewise confirmed mappings (PLANT) + if (j->job_type == df::job_type::CustomReaction || + j->job_type == df::job_type::PlantSeeds || + j->job_type == df::job_type::HarvestPlants) + continue; + // a posting that already finished a probe round can never be + // claimed; starting another escalation for it would probe forever + if (probed_jobs.count(j->id)) + continue; + LaborEscalation esc; + esc.job_id = j->id; + esc.jtype = j->job_type; + esc.job_name = job_display_name(j); + // test the mapped labor on an idle dwarf first: distinguishes + // "wrong labor" from "right labor, but every holder was busy" + esc.candidates.push_back(l); + // the disputed set follows; unmanaged labors belong to the player's + // own work details, so the probe must not touch them + for (auto m : disputed_labors) + if (m != l && managed_labor_cache[m] && !is_exclusive_labor(m)) + esc.candidates.push_back(m); + INFO(labor_probe).print( + "possible labor mapping mismatch: posting {} ({}) maps to {} " + "and {} eligible unit(s) have it enabled ({} idle), yet it has " + "gone unclaimed for {} cycles; probing labors on idle dwarves\n", + j->id, esc.job_name, + ENUM_KEY_STR(unit_labor, l), enabled, idle_enabled, + unclaimed_streak[l]); + labor_escalations[l] = esc; + } + + for (auto it = labor_escalations.begin(); it != labor_escalations.end();) + { + df::unit_labor l = it->first; + LaborEscalation &esc = it->second; + + // job pointers aren't stable across cycles; re-resolve each time + df::job *j = nullptr; + bool posted = false; + for (auto jp : world->jobs.postings) + if (jp->job && jp->job->id == esc.job_id && !jp->flags.bits.dead) + { + j = jp->job; + posted = true; + break; + } + if (!j) + for (auto jl = world->jobs.list.next; jl; jl = jl->next) + if (jl->item && jl->item->id == esc.job_id) + { + j = jl->item; + break; + } + + if (posted && j && !has_worker(j)) + { + // still waiting: re-assert experimental labors (a work-detail + // recompute can clear status.labors between our writes), then + // put the next untried candidate on its own idle dwarf so the + // labor that unlocks the job is unambiguous + for (auto wi = esc.writes.begin(); wi != esc.writes.end();) + { + if (auto u = df::unit::find(wi->first)) + { + // a unit that already claimed a job can't take the + // target posting anyway; re-asserting only lets it + // chain more work its details don't cover + if (!u->job.current_job) + for (auto m : wi->second) + u->status.labors[m] = true; + ++wi; + } + else + wi = esc.writes.erase(wi); + } + + if (esc.next < esc.candidates.size()) + { + // specialists are pinned to a skill detail; probing one + // would enable work its detail is meant to forbid + df::unit *idle = nullptr; + for (auto &info : units) + if (info.state == IDLE && !info.excluded && + !specialist_ids.count(info.u->id) && + !esc.writes.count(info.u->id)) + { + idle = info.u; + break; + } + // if no idle dwarf is free to experiment on, next stays put + // and we retry next cycle + if (idle) + { + df::unit_labor m = esc.candidates[esc.next++]; + idle->status.labors[m] = true; + esc.writes[idle->id].insert(m); + TRACE(assign).print( + "mismatch probe: enabled {} on unit {} ({}) for job " + "{} (mapped {})\n", + ENUM_KEY_STR(unit_labor, m), idle->id, + Units::getReadableName(idle), esc.job_id, + ENUM_KEY_STR(unit_labor, l)); + } + ++it; + } + else + { + WARN(labor_probe).print( + "job {} ({}) still unclaimed after probing all disputed " + "labors (mapped {}); the gating labor may lie outside " + "the probed set\n", + esc.job_id, esc.job_name, + ENUM_KEY_STR(unit_labor, l)); + probed_jobs.insert(esc.job_id); + revert_escalation(esc); + it = labor_escalations.erase(it); + } + continue; + } + + // resolved one way or another + if (j && has_worker(j)) + { + df::unit *worker = nullptr; + for (auto ref : j->general_refs) + if (ref->getType() == df::general_ref_type::UNIT_WORKER) + { + worker = df::unit::find( + ((df::general_ref_unit_workerst*)ref)->unit_id); + break; + } + std::set exp; + if (worker && esc.writes.count(worker->id)) + exp = esc.writes[worker->id]; + + std::stringstream es; + bool comma = false; + if (worker) + FOR_ENUM_ITEMS(unit_labor, wl) + { + if (wl == unit_labor::NONE || !worker->status.labors[wl]) + continue; + if (comma) + es << ','; + es << ENUM_KEY_STR(unit_labor, wl); + comma = true; + } + std::string enabled = comma ? es.str() : "NONE"; + + std::stringstream xs; + bool xcomma = false; + for (auto m : exp) + { + if (xcomma) + xs << ','; + xs << ENUM_KEY_STR(unit_labor, m); + xcomma = true; + } + std::string experiment = xcomma ? xs.str() : "none"; + + if (worker && worker->status.labors[l]) + { + // claimant holds the mapped labor: the mapping is fine and + // the starvation was eligibility/staffing, not a mis-map + INFO(labor_probe).print( + "job {} ({}) was claimed by unit {} ({}) holding the " + "mapped labor {}; not a mismatch (experimental labors " + "on claimant: {}; enabled: {{{}}})\n", + esc.job_id, esc.job_name, + worker->id, Units::getReadableName(worker), + ENUM_KEY_STR(unit_labor, l), experiment, enabled); + } + else if (worker) + { + WARN(labor_probe).print( + "labor mapping mismatch confirmed: job {} ({}) was " + "claimed by unit {} ({}) which does not hold the mapped " + "labor {}; enabled labors: {{{}}}, experimental labors " + "on claimant: {}\n", + esc.job_id, esc.job_name, + worker->id, Units::getReadableName(worker), + ENUM_KEY_STR(unit_labor, l), enabled, experiment); + } + else + { + INFO(labor_probe).print( + "job {} ({}) was claimed but its worker could not be " + "identified (mapped {})\n", + esc.job_id, esc.job_name, + ENUM_KEY_STR(unit_labor, l)); + } + } + else + { + WARN(labor_probe).print( + "job {} ({}) left the board unclaimed while probing its " + "mapped labor {}\n", + esc.job_id, esc.job_name, + ENUM_KEY_STR(unit_labor, l)); + } + + probed_jobs.insert(esc.job_id); + revert_escalation(esc); + it = labor_escalations.erase(it); + } + + save_state(); + + stat_laborers = laborer_ids.size(); + stat_specialists = specialist_ids.size(); + stat_generalists = n - stat_laborers - stat_specialists; + + DEBUG(modern_cycle, out).print( + "cycle: {} units, {} laborers, {} specialists ({} resting), backlog={}\n", + n, stat_laborers, stat_specialists, stat_resting, unskilled_backlog); +} + +// reconcile a detail's membership with the desired id set +static void reconcile(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired) +{ + bool trace = is_tool_detail(wd); + // remove stale members + for (auto it = wd->assigned_units.begin(); it != wd->assigned_units.end();) + { + if (!desired.count(*it)) + { + int32_t id = *it; + it = wd->assigned_units.erase(it); + if (auto u = df::unit::find(id)) + { + if (trace) + { + TRACE(tool_detail).print( + "tool detail {}: -unit {} ({})\n", + wd->name, id, Units::getReadableName(u)); + } + wdm->touch(u); + } + } + else + ++it; + } + // add new members + for (int32_t id : desired) + { + bool had = std::binary_search(wd->assigned_units.begin(), + wd->assigned_units.end(), id); + wdm->set_membership(wd, id, true); + if (trace && !had) + if (auto u = df::unit::find(id)) + { + TRACE(tool_detail).print( + "tool detail {}: +unit {} ({})\n", + wd->name, id, Units::getReadableName(u)); + } + } +} + +// reconcile a builtin detail's membership with the desired id set, but +// only ever remove memberships we created: player-assigned members of +// builtin details are left alone +static void reconcile_builtin(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired) +{ + for (auto it = wd->assigned_units.begin(); it != wd->assigned_units.end();) + { + int32_t id = *it; + if (!desired.count(id) && wdm->is_borrowed(wd, id)) + { + it = wd->assigned_units.erase(it); + wdm->unborrow(wd, id); + if (auto u = df::unit::find(id)) + { + TRACE(tool_detail).print( + "tool detail {}: -unit {} ({})\n", + wd->name, id, Units::getReadableName(u)); + wdm->touch(u); + } + } + else + ++it; + } + for (int32_t id : desired) + { + if (!std::binary_search(wd->assigned_units.begin(), + wd->assigned_units.end(), id)) + { + wdm->set_membership(wd, id, true); + wdm->borrow(wd, id); + if (auto u = df::unit::find(id)) + { + TRACE(tool_detail).print( + "tool detail {}: +unit {} ({})\n", + wd->name, id, Units::getReadableName(u)); + } + } + } +} + +static df::work_detail_icon_type skill_icon(df::job_skill skill) +{ + df::unit_labor l = (df::unit_labor)ENUM_ATTR(job_skill, labor, skill); + switch (l) { + case unit_labor::MINE: return work_detail_icon_type::MINERS; + case unit_labor::CUTWOOD: return work_detail_icon_type::WOODCUTTERS; + case unit_labor::HUNT: return work_detail_icon_type::HUNTERS; + case unit_labor::PLANT: return work_detail_icon_type::PLANTERS; + case unit_labor::FISH: return work_detail_icon_type::FISHERMEN; + case unit_labor::HERBALIST: return work_detail_icon_type::PLANT_GATHERERS; + case unit_labor::SIEGEOPERATE: return work_detail_icon_type::SIEGE_OPERATORS; + default: return work_detail_icon_type::CUSTOM_1; + } +} + +// --------------------------------------------------------------------------- +// state dump ("labormanager dump") -- diagnostic snapshot for bug reports +// --------------------------------------------------------------------------- + +namespace autolabor { + +void dump_engine_state(color_ostream &out) +{ + if (!world || !world->map.block_index) + { + out.print("no map loaded\n"); + return; + } + if (!labor_mapper) + labor_mapper = new JobLaborMapper(); + init_labor_to_skill(); + if (managed_labor_cache.empty()) + { + managed_labor_cache.assign(NUM_LABORS, true); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE) + managed_labor_cache[l] = labor_managed(l); + } + + // -- config -------------------------------------------------------------- + out.print("-- config --\n"); + out.print("balance={} idle_reserve={} fishing={} hunting={}\n", + balance_stop_name(balance()), idle_reserve(), + (int)config_flag(CF_ALLOW_FISHING), (int)config_flag(CF_ALLOW_HUNTING)); + out.print("automatic_professions_disabled={}\n", + (int)game->external_flag.bits.automatic_professions_disabled); + + // -- every job on the board, whether or not it has a posting ------------- + out.print("-- jobs.list --\n"); + int njobs = 0; + for (df::job_list_link *link = world->jobs.list.next; link; + link = link->next) + { + df::job *j = link->item; + if (!j) + continue; + njobs++; + df::unit_labor labor = labor_mapper->find_job_labor(j); + std::string btype = "none"; + if (df::building *bld = Job::getHolder(j)) + btype = ENUM_KEY_STR(building_type, bld->getType()); + out.print( + " job {} {}: labor={} posting={} susp={} lost={} work={} " + "repeat={} worker={} at {}\n", + j->id, ENUM_KEY_STR(job_type, j->job_type), + labor >= 0 && labor < NUM_LABORS ? + ENUM_KEY_STR(unit_labor, labor) : "unmapped", + j->posting_index, (int)j->flags.bits.suspend, + (int)j->flags.bits.item_lost, (int)j->flags.bits.working, + (int)j->flags.bits.repeat, (int)has_worker(j), btype); + } + out.print(" {} jobs total\n", njobs); + + // -- the auction board ---------------------------------------------------- + out.print("-- postings --\n"); + int live = 0, dead = 0; + for (auto jp : world->jobs.postings) + { + if (jp->flags.bits.dead || !jp->job) + { + dead++; + continue; + } + live++; + df::job *j = jp->job; + out.print(" posting {}: {} susp={} lost={} worker={} age={}\n", + jp->idx, ENUM_KEY_STR(job_type, j->job_type), + (int)j->flags.bits.suspend, (int)j->flags.bits.item_lost, + (int)has_worker(j), + posting_track.count(jp->idx) ? + world->frame_counter - posting_track[jp->idx].first_seen : -1); + } + out.print(" {} live, {} dead\n", live, dead); + + // -- demand --------------------------------------------------------------- + out.print("-- demand --\n"); + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + int claimed = labor_work.count(l) ? labor_work.at(l) : 0; + if (claimed == 0 && unclaimed_streak[l] == 0) + continue; + out.print(" {}: live={}, streak={}, managed={} ({})\n", + ENUM_KEY_STR(unit_labor, l), claimed, unclaimed_streak[l], + (int)managed_labor_cache[l], + is_unskilled(l) ? "unskilled" : + ENUM_KEY_STR(job_skill, labor_to_skill[l])); + } + out.print("starving={} oldest={} ({} ticks), resolved={}, " + "avg_wait={}, max_wait={}\n", + stat_starving, + stat_oldest_job == df::job_type::NONE ? "none" : + ENUM_KEY_STR(job_type, stat_oldest_job), + stat_oldest_wait, stat_resolved, + stat_resolved ? stat_resolved_ticks / stat_resolved : 0, + stat_max_wait); + + // -- skills --------------------------------------------------------------- + out.print("-- skills --\n"); + out.print("usage:"); + for (auto &kv : skill_usage) + if (kv.second >= 0.01) + out.print(" {}={:.1f}", ENUM_KEY_STR(job_skill, kv.first), + kv.second); + out.print("\n"); + + // -- shop constraints ------------------------------------------------------ + if (!shop_needs.empty()) + { + out.print("-- shop needs --\n"); + for (auto &need : shop_needs) + out.print(" {}: permitted={} range=[{},{}]\n", + ENUM_KEY_STR(unit_labor, need.labor), need.permitted.size(), + need.min_level, need.max_level); + } + + // -- tools ------------------------------------------------------------------ + int tool_count[TOOLS_MAX]; + count_tools(tool_count); + out.print("tools: picks={} axes={} crossbows={}\n", + tool_count[TOOL_PICK], tool_count[TOOL_AXE], tool_count[TOOL_CROSSBOW]); + + // -- work details ------------------------------------------------------------- + out.print("-- work details --\n"); + for (auto wd : plotinfo->labor_info.work_details) + { + std::string members; + for (int32_t id : wd->assigned_units) + { + if (!members.empty()) + members += ","; + members += std::to_string(id); + } + out.print(" '{}' icon={} mode={} no_modify={} managed={} " + "members=[{}]\n", + wd->name, ENUM_KEY_STR(work_detail_icon_type, wd->icon), + (int)wd->flags.bits.mode, (int)wd->flags.bits.no_modify, + (int)(wd->name.compare(0, 5, "auto:") == 0), members); + } + + // -- citizens ------------------------------------------------------------------ + out.print("-- citizens --\n"); + for (auto cre : world->units.active) + { + if (!Units::isCitizen(cre)) + continue; + // which details hold this unit + std::string det; + for (auto wd : plotinfo->labor_info.work_details) + if (std::binary_search(wd->assigned_units.begin(), + wd->assigned_units.end(), cre->id)) + { + if (!det.empty()) + det += "|"; + det += wd->name; + } + // enabled skilled labors + std::string labors; + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && cre->status.labors[l] && + labor_to_skill[l] != job_skill::NONE) + { + if (!labors.empty()) + labors += "|"; + labors += ENUM_KEY_STR(unit_labor, l); + } + auto tk = unit_track.find(cre->id); + out.print( + " {} ({}): assignable={} burrows={} state={} specialized={} " + "pin={} busy={} rest={} job={} details=[{}] " + "skilled_labors=[{}]\n", + cre->id, Units::getReadableName(cre), + (int)is_assignable(cre), cre->burrows.size(), + (int)get_dwarf_state(cre), + (int)cre->flags4.bits.only_do_assigned_jobs, + tk != unit_track.end() && tk->second.detail_skill != job_skill::NONE ? + ENUM_KEY_STR(job_skill, tk->second.detail_skill) : "NONE", + tk != unit_track.end() ? tk->second.busy_streak : 0, + tk != unit_track.end() ? tk->second.rest_left : 0, + cre->job.current_job ? + ENUM_KEY_STR(job_type, cre->job.current_job->job_type) : "none", + det, labors); + if (!is_assignable(cre)) + out.print( + " not assignable: own_civ={} own_group={} active={} " + "visitor={} ghost={} can_assign={} profession={}\n", + (int)Units::isOwnCiv(cre), (int)Units::isOwnGroup(cre), + (int)Units::isActive(cre), (int)cre->flags2.bits.visitor, + (int)cre->flags3.bits.ghostly, + (int)ENUM_ATTR(profession, can_assign_labor, cre->profession), + ENUM_KEY_STR(profession, cre->profession)); + else if (get_dwarf_state(cre) == dwarf_state::OTHER || + get_dwarf_state(cre) == dwarf_state::CHILD) + out.print(" state detail: migrant={} specific_refs={} " + "profession={}\n", + (int)(Units::getMiscTrait(cre, misc_trait_type::Migrant) != + nullptr), + cre->specific_refs.size(), + ENUM_KEY_STR(profession, cre->profession)); + } +} + +} // namespace autolabor + +// --------------------------------------------------------------------------- +// command dialect +// --------------------------------------------------------------------------- + +bool ModernEngine::command(color_ostream &out, std::vector ¶meters) +{ + if (parameters.empty()) + return false; + + if (managed_labor_cache.empty()) + { + managed_labor_cache.assign(NUM_LABORS, true); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE) + managed_labor_cache[l] = labor_managed(l); + } + + if (parameters.size() == 2 && parameters[0] == "balance") + { + bool ok = false; + for (int i = 0; i < NUM_BALANCE_STOPS; i++) + if (parameters[1] == balance_stop_name(i)) + { + store_balance(i); + ok = true; + } + if (!ok) + { + int v = atoi(parameters[1].c_str()); + if (v >= 0 && v < NUM_BALANCE_STOPS) + { + store_balance(v); + ok = true; + } + } + if (!ok) + { + out.printerr("Syntax: labormanager balance <0-{}|{}|...|{}>\n", + NUM_BALANCE_STOPS - 1, balance_stop_name(0), + balance_stop_name(NUM_BALANCE_STOPS - 1)); + return true; + } + out << "Balance: " << balance_stop_name(balance()) << std::endl; + return true; + } + else if (parameters.size() == 2 && parameters[0] == "idle-reserve") + { + int v = atoi(parameters[1].c_str()); + if (v < 0 || v > 100) + { + out.printerr("Syntax: labormanager idle-reserve <0-100>\n"); + return true; + } + store_idle_reserve(v); + out << "Idle reserve: " << v << "%" << std::endl; + return true; + } + else if (parameters.size() == 3 && parameters[0] == "labor") + { + df::unit_labor labor = unit_labor::NONE; + FOR_ENUM_ITEMS(unit_labor, test) + if (parameters[1] == ENUM_KEY_STR(unit_labor, test)) + labor = test; + if (labor == unit_labor::NONE) + { + out.printerr("Could not find labor {}.\n", parameters[1]); + return true; + } + bool managed = parameters[2] == "managed"; + if (!managed && parameters[2] != "unmanaged") + { + out.printerr("Syntax: labormanager labor managed|unmanaged\n"); + return true; + } + set_labor_managed(labor, managed); + managed_labor_cache[labor] = managed; + wdm->touch_all(); + wdm->commit(); + out << ENUM_KEY_STR(unit_labor, labor) << ": " + << (managed ? "managed" : "unmanaged") << std::endl; + return true; + } + else if (parameters.size() == 1 && + (parameters[0] == "allow-fishing" || parameters[0] == "forbid-fishing")) + { + set_config_flag(CF_ALLOW_FISHING, parameters[0] == "allow-fishing"); + return true; + } + else if (parameters.size() == 1 && + (parameters[0] == "allow-hunting" || parameters[0] == "forbid-hunting")) + { + set_config_flag(CF_ALLOW_HUNTING, parameters[0] == "allow-hunting"); + return true; + } + else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) + { + out << "Balance: " << balance_stop_name(balance()) + << ", idle reserve " << idle_reserve() << "%" << std::endl; + out << status_line() << std::endl; + out << "job board: " << stat_resolved << " postings resolved"; + if (stat_resolved > 0) + out << ", avg wait " << (stat_resolved_ticks / stat_resolved) + << " ticks, longest " << stat_max_wait << " ticks"; + if (stat_starving > 0) + out << "; " << stat_starving << " starving (> " << STARVE_TICKS + << " ticks, oldest " << stat_oldest_wait << ")"; + out << std::endl; + // most-used skills drive specialization priorities + std::vector> top_used; + for (auto &kv : skill_usage) + if (kv.second >= 0.5) + top_used.push_back(kv); + std::sort(top_used.begin(), top_used.end(), + [](const std::pair &a, + const std::pair &b) { + return a.second > b.second; + }); + if (!top_used.empty()) + { + out << "top skills in use:"; + for (size_t i = 0; i < top_used.size() && i < 5; i++) + out << " " << ENUM_KEY_STR(job_skill, top_used[i].first) + << "(" << int(top_used[i].second) << ")"; + out << std::endl; + } + if (parameters[0] == "list") + { + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + out << ENUM_KEY_STR(unit_labor, l) << ": " + << (managed_labor_cache[l] ? "managed" : "unmanaged"); + if (unclaimed_streak[l] >= STARVE_CYCLES) + out << " (starved " << unclaimed_streak[l] << " cycles)"; + out << std::endl; + } + } + return true; + } + + return false; +} + +// --------------------------------------------------------------------------- +// monitor mode +// --------------------------------------------------------------------------- +// shares the posting-tracking machinery and persisted state above, but +// performs no labor management -- it just watches the job board and feeds +// the task starvation notification. + +void MonitorEngine::enable(color_ostream &out) +{ + // undo the legacy engine's flag if switching straight from it, so + // vanilla work details govern labors again + game->external_flag.bits.automatic_professions_disabled = false; + + if (!labor_mapper) + labor_mapper = new JobLaborMapper(); + init_labor_to_skill(); + + posting_track.clear(); + skill_usage.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; + restore_state(); + initialized = true; + + out << "Enabling labor monitor (starvation warnings only)." << std::endl; +} + +void MonitorEngine::disable(color_ostream &out) +{ + initialized = false; + out << "Disabling labor monitor." << std::endl; +} + +void MonitorEngine::map_unload() +{ + initialized = false; + posting_track.clear(); + skill_usage.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; +} + +void MonitorEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + cycle_timestamp = world->frame_counter; + + // the demand scan does the posting-age tracking and starvation stats; + // the per-labor backlog it also fills is unused here + std::map backlog; + scan_demand(backlog); + save_state(); +} + +int MonitorEngine::starving_jobs() +{ + return initialized ? stat_starving : 0; +} + +df::job_type MonitorEngine::oldest_starving_job() +{ + return initialized && stat_starving > 0 ? stat_oldest_job + : df::job_type::NONE; +} + +int32_t MonitorEngine::oldest_starving_wait() +{ + return initialized && stat_starving > 0 ? stat_oldest_wait : 0; +} + +df::coord MonitorEngine::oldest_starving_pos() +{ + return initialized && stat_starving > 0 ? stat_oldest_pos + : df::coord(); +} + +std::string MonitorEngine::oldest_starving_name() +{ + return initialized && stat_starving > 0 ? ::oldest_starving_name() : ""; +} + +bool MonitorEngine::command(color_ostream &out, std::vector ¶meters) +{ + if (parameters.size() == 1 && + (parameters[0] == "list" || parameters[0] == "status")) + { + out << "labor monitor: " << stat_starving << " starving postings (> " + << STARVE_TICKS << " ticks"; + if (stat_starving > 0 && stat_oldest_job != df::job_type::NONE) + out << ", oldest " << ::oldest_starving_name() + << " at " << stat_oldest_wait << " ticks"; + out << ")" << std::endl; + out << stat_resolved << " postings resolved"; + if (stat_resolved > 0) + out << ", avg wait " << (stat_resolved_ticks / stat_resolved) + << " ticks, longest " << stat_max_wait << " ticks"; + out << std::endl; + return true; + } + return false; +} diff --git a/plugins/autolabor/modernengine.h b/plugins/autolabor/modernengine.h new file mode 100644 index 0000000000..4ae915fb83 --- /dev/null +++ b/plugins/autolabor/modernengine.h @@ -0,0 +1,58 @@ +#pragma once + +#include "engines.h" +#include "laborcommon.h" +#include "workdetails.h" + +#include + +// The modern engine: works through DF's work detail system instead of +// writing the labor matrix directly. It maintains plugin-owned work details +// (named "auto: ...", visible and editable in the vanilla UI) and uses the +// per-unit only_do_assigned_jobs flag to create dedicated pools: +// +// - "auto: Laborers": unskilled labors only; members are specialized so +// the game's job auction always has units that can only take unskilled +// work, preventing starvation of hauling/cleaning jobs. +// - "auto: ": one per exercised skill; members are specialized to +// jobs that train that skill. Specialists whose profession has a guild +// hall get protected idle time to participate in guild activities. +// +// The balance slider trades laborer coverage against specialist protection. +class ModernEngine : public LaborEngine { +public: + ModernEngine(WorkDetailManager *wdm) : wdm(wdm) {} + + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + + // status summary for the overlay, e.g. "3 laborers, 5 specialists" + std::string status_line(); + + // number of live job postings that have been on the board longer than + // one game day (drives the task starvation notification) + int starving_jobs(); + + // the job type, age (ticks), location, and display name of the + // longest-starving posting, for the starvation notification + df::job_type oldest_starving_job(); + int32_t oldest_starving_wait(); + df::coord oldest_starving_pos(); + std::string oldest_starving_name(); + +private: + WorkDetailManager *wdm; + bool initialized = false; +}; + +namespace autolabor { + +// prints a snapshot of the engine's observed state to the console for +// diagnostics (the 'dump' command); usable in modern or monitor mode +void dump_engine_state(color_ostream &out); + +} // namespace autolabor diff --git a/plugins/autolabor/monitorengine.h b/plugins/autolabor/monitorengine.h new file mode 100644 index 0000000000..3b0717e649 --- /dev/null +++ b/plugins/autolabor/monitorengine.h @@ -0,0 +1,34 @@ +#pragma once + +#include "engines.h" +#include "laborcommon.h" + +#include +#include + +// Monitor mode: watches the job board and feeds the task starvation +// notification, but performs no labor management at all -- everything is +// left to the player's own work details and the labor matrix. +// +// The implementation lives in modernengine.cpp so it shares the +// posting-tracking machinery (posting ages, starvation stats, skill usage +// history, persisted state) with the modern engine; switching between +// modes keeps the accumulated history. +class MonitorEngine : public LaborEngine { +public: + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + + int starving_jobs(); + df::job_type oldest_starving_job(); + int32_t oldest_starving_wait(); + df::coord oldest_starving_pos(); + std::string oldest_starving_name(); + +private: + bool initialized = false; +}; diff --git a/plugins/autolabor/workdetails.cpp b/plugins/autolabor/workdetails.cpp new file mode 100644 index 0000000000..5e86039634 --- /dev/null +++ b/plugins/autolabor/workdetails.cpp @@ -0,0 +1,324 @@ +#include "workdetails.h" + +#include + +#include + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; + +const char * const WorkDetailManager::NAME_PREFIX = "auto:"; + +static std::vector &details() +{ + return plotinfo->labor_info.work_details; +} + +bool WorkDetailManager::is_managed(df::work_detail *wd) +{ + return wd && wd->name.compare(0, strlen(NAME_PREFIX), NAME_PREFIX) == 0; +} + +void WorkDetailManager::reset() +{ + dirty.clear(); + flagged.clear(); + borrowed.clear(); + recompute_all = false; + flagged_cfg = World::GetPersistentSiteData("autolabor/flagged"); + borrowed_cfg = World::GetPersistentSiteData("autolabor/borrowed"); + load_flagged(); + load_borrowed(); +} + +void WorkDetailManager::load_flagged() +{ + flagged.clear(); + if (!flagged_cfg.isValid()) + return; + size_t count = flagged_cfg.data_size() / PersistentDataItem::int28_size; + for (size_t i = 0; i < count; i++) + flagged.insert(flagged_cfg.get_int28(i * PersistentDataItem::int28_size)); +} + +void WorkDetailManager::save_flagged() +{ + if (!flagged_cfg.isValid()) + flagged_cfg = World::AddPersistentSiteData("autolabor/flagged"); + if (!flagged_cfg.isValid()) + return; + + size_t off = 0; + for (int32_t id : flagged) + { + flagged_cfg.set_int28(off, id); + off += PersistentDataItem::int28_size; + } + flagged_cfg.val().resize(off); +} + +void WorkDetailManager::load_borrowed() +{ + borrowed.clear(); + if (!borrowed_cfg.isValid()) + return; + size_t count = borrowed_cfg.data_size() / (2 * PersistentDataItem::int28_size); + for (size_t i = 0; i < count; i++) + { + int32_t icon = borrowed_cfg.get_int28(i * 2 * PersistentDataItem::int28_size); + int32_t uid = borrowed_cfg.get_int28((i * 2 + 1) * PersistentDataItem::int28_size); + borrowed[icon].insert(uid); + } +} + +void WorkDetailManager::save_borrowed() +{ + if (!borrowed_cfg.isValid()) + borrowed_cfg = World::AddPersistentSiteData("autolabor/borrowed"); + if (!borrowed_cfg.isValid()) + return; + + size_t off = 0; + for (auto &kv : borrowed) + for (int32_t id : kv.second) + { + borrowed_cfg.set_int28(off, kv.first); + borrowed_cfg.set_int28(off + PersistentDataItem::int28_size, id); + off += 2 * PersistentDataItem::int28_size; + } + borrowed_cfg.val().resize(off); +} + +std::vector WorkDetailManager::managed_details() +{ + std::vector out; + for (auto wd : details()) + if (is_managed(wd)) + out.push_back(wd); + return out; +} + +df::work_detail *WorkDetailManager::find_detail(const std::string &name) +{ + for (auto wd : details()) + if (is_managed(wd) && wd->name == name) + return wd; + return NULL; +} + +df::work_detail *WorkDetailManager::find_builtin(df::work_detail_icon_type icon) +{ + // builtin details occupy a no_modify prefix run in the vector (the + // same convention the work-details screen uses); matching on icon + // distinguishes Miners/Woodcutters/Hunters from the other builtins + for (auto wd : details()) + if (!is_managed(wd) && wd->flags.bits.no_modify && wd->icon == icon) + return wd; + return NULL; +} + +df::work_detail *WorkDetailManager::ensure_detail(const std::string &name, + df::work_detail_icon_type icon, df::work_detail_mode mode) +{ + if (auto wd = find_detail(name)) + return wd; + + auto wd = df::allocate(); + wd->name = name; + wd->icon = icon; + wd->flags.bits.mode = mode; + details().push_back(wd); + + // a new non-Everybody detail clears its covered labors globally + if (mode != work_detail_mode::EverybodyDoesThis) + touch_all(); + else + recompute_all = true; + + return wd; +} + +void WorkDetailManager::delete_detail(df::work_detail *wd) +{ + auto &vec = details(); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + if (*it == wd) + { + for (int32_t id : wd->assigned_units) + if (auto u = df::unit::find(id)) + touch(u); + vec.erase(it); + delete wd; + touch_all(); + return; + } + } +} + +void WorkDetailManager::cover(df::work_detail *wd, df::unit_labor labor, bool on) +{ + if (labor < 0 || labor > ENUM_LAST_ITEM(unit_labor)) + return; + if (wd->allowed_labors[labor] != on) + { + wd->allowed_labors[labor] = on; + // coverage changes affect every unit, not just members + recompute_all = true; + } +} + +void WorkDetailManager::set_membership(df::work_detail *wd, int32_t unit_id, bool want) +{ + auto &vec = wd->assigned_units; + auto it = std::lower_bound(vec.begin(), vec.end(), unit_id); + bool has = (it != vec.end() && *it == unit_id); + + if (want && !has) + { + vec.insert(it, unit_id); + if (auto u = df::unit::find(unit_id)) + touch(u); + } + else if (!want && has) + { + vec.erase(it); + if (auto u = df::unit::find(unit_id)) + touch(u); + } +} + +void WorkDetailManager::set_specialized(df::unit *u, bool on) +{ + if (!u) + return; + + if (on) + { + if (!u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = true; + touch(u); + } + flagged.insert(u->id); + } + else + { + // only strip flags we set ourselves + if (flagged.count(u->id)) + { + flagged.erase(u->id); + if (u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = false; + touch(u); + } + } + } +} + +bool WorkDetailManager::is_borrowed(df::work_detail *wd, int32_t unit_id) const +{ + auto it = borrowed.find(wd ? wd->icon : -1); + return it != borrowed.end() && it->second.count(unit_id); +} + +void WorkDetailManager::borrow(df::work_detail *wd, int32_t unit_id) +{ + if (wd) + borrowed[wd->icon].insert(unit_id); +} + +void WorkDetailManager::unborrow(df::work_detail *wd, int32_t unit_id) +{ + if (wd) + borrowed[wd->icon].erase(unit_id); +} + +void WorkDetailManager::touch(df::unit *u) +{ + if (u) + dirty.insert(u->id); +} + +void WorkDetailManager::touch_all() +{ + recompute_all = true; +} + +void WorkDetailManager::commit() +{ + if (recompute_all) + { + dirty.clear(); + for (auto u : world->units.active) + if (Units::isCitizen(u)) + touch(u); + recompute_all = false; + } + + for (int32_t id : dirty) + if (auto u = df::unit::find(id)) + Units::setAutomaticProfessions(u); + dirty.clear(); + + save_flagged(); + save_borrowed(); +} + +void WorkDetailManager::shutdown() +{ + // remove all managed details + for (auto wd : managed_details()) + delete_detail(wd); + + // drop memberships we added to builtin details + for (auto wd : details()) + { + if (is_managed(wd)) + continue; + auto it = borrowed.find(wd->icon); + if (it == borrowed.end()) + continue; + auto &vec = wd->assigned_units; + for (auto uit = vec.begin(); uit != vec.end();) + { + if (it->second.count(*uit)) + { + int32_t id = *uit; + uit = vec.erase(uit); + if (auto u = df::unit::find(id)) + touch(u); + } + else + ++uit; + } + } + borrowed.clear(); + save_borrowed(); + + // clear specialization flags we set on any still-living unit + for (int32_t id : flagged) + if (auto u = df::unit::find(id)) + if (u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = false; + touch(u); + } + flagged.clear(); + save_flagged(); + + // detail removal affects everyone + touch_all(); + commit(); +} diff --git a/plugins/autolabor/workdetails.h b/plugins/autolabor/workdetails.h new file mode 100644 index 0000000000..aef777689d --- /dev/null +++ b/plugins/autolabor/workdetails.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include + +#include "modules/Persistence.h" + +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +// Manages the plugin-owned work details through which the modern engine +// expresses assignments. Managed details are identified by an "auto:" name +// prefix so the player can see (and, with caveats, edit) what the tool is +// doing in the vanilla work details screen. +// +// The plugin never writes to unit->status.labors directly; all changes go +// through work detail membership and df::unit flags4.only_do_assigned_jobs, +// followed by a call to Units::setAutomaticProfessions() which asks the game +// to recompute the labor matrix. + +class WorkDetailManager { +public: + static const char * const NAME_PREFIX; // "auto:" + + // Rescan labor_info.work_details, adopting any surviving managed details + // and reloading the flagged-unit set. Call on map load before use. + void reset(); + + // Remove all managed details, clear specialization flags we set, and + // recompute every citizen. Call when the modern engine is disabled. + void shutdown(); + + // Find a managed detail by exact name (including prefix). + df::work_detail *find_detail(const std::string &name); + + // Find a builtin (predefined, non-managed) detail by icon type, e.g. + // the vanilla Miners detail. Needed because in DF v50 only the + // predefined details trigger tool equipping for their members. + df::work_detail *find_builtin(df::work_detail_icon_type icon); + + // Find or create a managed detail with the given name, icon, and mode. + // Coverage (allowed_labors) is only set on creation; callers that change + // coverage or mode afterwards must call touch_all() since pass 1 of the + // game's labor computation clears covered labors for every unit. + df::work_detail *ensure_detail(const std::string &name, + df::work_detail_icon_type icon, df::work_detail_mode mode); + + // Delete a managed detail entirely (labor becomes unrestricted again). + void delete_detail(df::work_detail *wd); + + // Set the detail's coverage of a labor. Requires touch_all() semantics; + // we just mark every citizen dirty. + void cover(df::work_detail *wd, df::unit_labor labor, bool on); + + // Add/remove a unit from a detail's assigned_units (kept sorted). + void set_membership(df::work_detail *wd, int32_t unit_id, bool want); + + // Mark a unit as only doing assigned work (the game's "specialized" + // flag). We track which units we flagged so shutdown() doesn't strip + // flags the player set themselves. + void set_specialized(df::unit *u, bool on); + + // Queue a unit for labor recomputation at commit(). + void touch(df::unit *u); + + // Queue every citizen for recomputation; needed when detail coverage or + // mode changes since non-Everybody details clear their labors globally. + void touch_all(); + + // Apply queued recomputations via Units::setAutomaticProfessions(). + void commit(); + + // Enumerate details whose name starts with the managed prefix. + std::vector managed_details(); + + // Unit ids we have flagged as only_do_assigned_jobs. + const std::set &flagged_units() const { return flagged; } + + // Track memberships we added to builtin (non-managed) details, keyed + // by the detail's icon type, so we only ever remove memberships we + // created and never strip a player's own assignments. + bool is_borrowed(df::work_detail *wd, int32_t unit_id) const; + void borrow(df::work_detail *wd, int32_t unit_id); + void unborrow(df::work_detail *wd, int32_t unit_id); + + bool is_managed(df::work_detail *wd); + +private: + void load_flagged(); + void save_flagged(); + void load_borrowed(); + void save_borrowed(); + + std::set dirty; + std::set flagged; + std::map> borrowed; // icon -> unit ids + bool recompute_all = false; + PersistentDataItem flagged_cfg; + PersistentDataItem borrowed_cfg; +}; diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index f7f1bdd84b..cc45507b2b 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -63,17 +63,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -235,9 +235,9 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci unit->general_refs.push_back(ref); zone->assigned_units.push_back(unit->id); - INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n", - unit->id, Units::getRaceName(unit).c_str(), - zone->id, zone->name.c_str()); + INFO(cycle,out).print("Unit {} ({}) assigned to nestbox zone {} ({})\n", + unit->id, Units::getRaceName(unit), + zone->id, zone->name); return true; } @@ -247,13 +247,13 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building_ci static size_t getFreeNestboxZones(color_ostream &out, vector &free_zones) { size_t assigned = 0; for (auto zone : world->buildings.other.ZONE_PEN) { - TRACE(cycle,out).print("scanning pasture %d (%s)\n", zone->id, zone->name.c_str()); + TRACE(cycle,out).print("scanning pasture {} ({})\n", zone->id, zone->name); if (!Buildings::isActive(zone)) { - TRACE(cycle,out).print("pasture %d is inactive\n", zone->id); + TRACE(cycle,out).print("pasture {} is inactive\n", zone->id); continue; } if (!isEmptyPasture(zone)) { - TRACE(cycle,out).print("pasture %d is not empty\n", zone->id); + TRACE(cycle,out).print("pasture {} is not empty\n", zone->id); continue; } @@ -262,23 +262,23 @@ static size_t getFreeNestboxZones(color_ostream &out, vectorx1, zone->y1, zone->z); auto bld = Buildings::findAtTile(pos); if (!bld || bld->getType() != df::building_type::NestBox) { - TRACE(cycle,out).print("pasture %d does not have nestbox in upper left corner\n", zone->id); + TRACE(cycle,out).print("pasture {} does not have nestbox in upper left corner\n", zone->id); continue; } - TRACE(cycle,out).print("found nestbox %d in pasture %d\n", bld->id, zone->id); + TRACE(cycle,out).print("found nestbox {} in pasture {}\n", bld->id, zone->id); df::building_nest_boxst *nestbox = virtual_cast(bld); if (!nestbox) { - TRACE(cycle,out).print("nestbox %d is somehow not a nestbox\n", bld->id); + TRACE(cycle,out).print("nestbox {} is somehow not a nestbox\n", bld->id); continue; } if (nestbox->claimed_by >= 0) { if (auto unit = df::unit::find(nestbox->claimed_by)) { - TRACE(cycle,out).print("nestbox %d is claimed by unit %d (%s)\n", bld->id, - nestbox->claimed_by, Units::getReadableName(unit).c_str()); + TRACE(cycle,out).print("nestbox {} is claimed by unit {} ({})\n", bld->id, + nestbox->claimed_by, Units::getReadableName(unit)); if (!isFreeEgglayer(unit)) { - DEBUG(cycle,out).print("cannot assign unit %d to nestbox %d: not a free egg layer\n", unit->id, bld->id); + DEBUG(cycle,out).print("cannot assign unit {} to nestbox {}: not a free egg layer\n", unit->id, bld->id); } else { // if the nestbox is claimed by a free egg layer, attempt to assign that unit to the zone if (assignUnitToZone(out, unit, zone)) @@ -327,7 +327,7 @@ static size_t assign_nestboxes(color_ostream &out) { DEBUG(cycle,out).print("Failed to assign unit to building.\n"); return assigned; } - DEBUG(cycle,out).print("assigned unit %d to zone %d\n", + DEBUG(cycle,out).print("assigned unit {} to zone {}\n", free_units[idx]->id, free_zones[idx]->id); ++assigned; } diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index f8f93ac747..e8c06d575d 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -53,7 +53,7 @@ static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); return CR_OK; } @@ -62,28 +62,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_cycle(out); } else { - DEBUG(control, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control, out).print("shutting down %s\n", plugin_name); + DEBUG(control, out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -104,7 +104,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) // all the other state we can directly read/modify from the persistent // data structure. is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); + DEBUG(control, out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -114,7 +114,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan { if (is_enabled) { - DEBUG(control, out).print("world unloaded; disabling %s\n", plugin_name); + DEBUG(control, out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } } @@ -141,6 +141,7 @@ static void createSlabJob(df::unit *unit) order->specdata.hist_figure_id = unit->hist_figure_id; order->amount_left = 1; order->amount_total = 1; + order->frequency = df::workquota_frequency_type::OneTime; world->manager_orders.all.push_back(order); } @@ -174,7 +175,7 @@ static void checkslabs(color_ostream &out) { createSlabJob(ghost); auto fullName = Units::getReadableName(ghost); - out.print("Added slab order for %s\n", DF2CONSOLE(fullName).c_str()); + out.print("Added slab order for {}\n", DF2CONSOLE(fullName)); } } } diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index c7ce4114e7..d1b7a502b4 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -5,10 +5,8 @@ * Written by cdombroski. */ -#include -#include - #include "Console.h" +#include "Core.h" #include "DataDefs.h" #include "DataFuncs.h" #include "DataIdentity.h" @@ -19,8 +17,11 @@ #include "TileTypes.h" #include "modules/Buildings.h" +#include "modules/Constructions.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" +#include "modules/Maps.h" #include "modules/World.h" #include "df/building_axle_horizontalst.h" @@ -35,12 +36,18 @@ #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" +#include "df/construction.h" #include "df/engraving.h" +#include "df/entity_position.h" #include "df/tile_bitmask.h" #include "df/tile_designation.h" #include "df/tile_occupancy.h" #include "df/world.h" +#include +#include +#include + using std::endl; using std::map; using std::ofstream; @@ -53,8 +60,6 @@ using namespace DFHack; DFHACK_PLUGIN("blueprint"); REQUIRE_GLOBAL(world); -static const string BLUEPRINT_USER_DIR = "dfhack-config/blueprints/"; - namespace DFHack { DBG_DECLARE(blueprint,log); } @@ -106,18 +111,17 @@ struct blueprint_options { bool construct = false; bool build = false; bool place = false; - // bool zone = false; - // bool query = false; - // bool rooms = false; + bool zone = false; + static struct_identity _identity; }; static const struct_field_info blueprint_options_fields[] = { { struct_field_info::PRIMITIVE, "help", offsetof(blueprint_options, help), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "start", offsetof(blueprint_options, start), &df::coord::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "start", offsetof(blueprint_options, start), df::identity_traits::get(), 0, 0}, { struct_field_info::PRIMITIVE, "format", offsetof(blueprint_options, format), df::identity_traits::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "nometa", offsetof(blueprint_options, nometa), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "playback_start", offsetof(blueprint_options, playback_start), &df::coord2d::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "playback_start", offsetof(blueprint_options, playback_start), df::identity_traits::get(),0, 0 }, { struct_field_info::PRIMITIVE, "playback_start_comment", offsetof(blueprint_options, playback_start_comment), df::identity_traits::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "split_strategy", offsetof(blueprint_options, split_strategy), df::identity_traits::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "width", offsetof(blueprint_options, width), &df::identity_traits::identity, 0, 0 }, @@ -132,9 +136,7 @@ static const struct_field_info blueprint_options_fields[] = { { struct_field_info::PRIMITIVE, "construct", offsetof(blueprint_options, construct), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, - // { struct_field_info::PRIMITIVE, "rooms", offsetof(blueprint_options, rooms), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "zone", offsetof(blueprint_options, zone), &df::identity_traits::identity, 0, 0 }, { struct_field_info::END } }; struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn, NULL, "blueprint_options", NULL, blueprint_options_fields); @@ -164,8 +166,7 @@ typedef vector bp_row; // index is x coordinate typedef map bp_area; // key is y coordinate typedef map bp_volume; // key is z coordinate -typedef const char * (get_tile_fn)(const df::coord &pos, - const tile_context &ctx); +typedef const char * (get_tile_fn)(color_ostream &out, const df::coord &pos, const tile_context &ctx); typedef void (init_ctx_fn)(const df::coord &pos, tile_context &ctx); struct blueprint_processor { @@ -183,7 +184,12 @@ struct blueprint_processor { get_tile(get_tile), init_ctx(init_ctx) { } }; -// global caches, cleared when the string cache is cleared +// global caches, lazily initialized and cleared at the end of each blueprint +// this assumes that no two blueprints are being generated at the same time, +// which is currently ensured by the higher-level DFHack command handling code. +// if this assumption ever becomes untrue, we'll need to protect the caches +// with thread synchronization primitives or make the caches per-blueprint. +static std::set string_cache; static std::unordered_map engravings_cache; static std::unordered_map dig_job_cache; static PersistentDataItem warm_config, damp_config; @@ -215,6 +221,14 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { }); } +static void clear_caches() { + string_cache.clear(); + engravings_cache.clear(); + dig_job_cache.clear(); + warm_config = PersistentDataItem(); + damp_config = PersistentDataItem(); +} + // We use const char * throughout this code instead of std::string to avoid // having to allocate memory for all the small string literals. This // significantly speeds up processing and allows us to handle very large maps @@ -223,19 +237,9 @@ static void init_caches(DFHack::color_ostream &out, bool cache_engravings) { // allocated until we write out the blueprints at the end. // If NULL is passed as the str, the cache is cleared. static const char * cache(const char *str) { - // this local static assumes that no two blueprints are being generated at - // the same time, which is currently ensured by the higher-level DFHack - // command handling code. if this assumption ever becomes untrue, we'll - // need to protect the cache with thread synchronization primitives or make - // the cache per-blueprint. - static std::set _cache; - if (!str) { - _cache.clear(); - engravings_cache.clear(); - dig_job_cache.clear(); + if (!str) return NULL; - } - return _cache.emplace(str).first->c_str(); + return string_cache.emplace(str).first->c_str(); } // Convenience wrapper for std::string. @@ -337,7 +341,7 @@ static const char * get_tile_dig_job(df::tile_designation *td, df::job *job) { } } -static const char * get_tile_dig(const df::coord &pos, const tile_context &) { +static const char * get_tile_dig(color_ostream &out, const df::coord &pos, const tile_context &) { df::tile_designation *td = Maps::getTileDesignation(pos); if (td && td->bits.dig != df::tile_dig_designation::No) return add_markers(pos, get_tile_dig_designation(pos, td->bits.dig)); @@ -376,8 +380,7 @@ static const char * get_tile_dig(const df::coord &pos, const tile_context &) { } } -static const char * get_tile_smooth_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_smooth_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { if (dig_job_cache.contains(pos) && dig_job_cache[pos]->job_type == df::job_type::CarveFortification) return "s"; @@ -401,9 +404,8 @@ static const char * get_tile_smooth_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_with_engravings(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_with_engravings(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -434,9 +436,8 @@ static const char * get_tile_smooth_with_engravings(const df::coord &pos, return NULL; } -static const char * get_tile_smooth_all(const df::coord &pos, - const tile_context &tc) { - const char * smooth_minimal = get_tile_smooth_minimal(pos, tc); +static const char * get_tile_smooth_all(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * smooth_minimal = get_tile_smooth_minimal(out, pos, tc); if (smooth_minimal) return smooth_minimal; @@ -484,8 +485,7 @@ static const char * get_track_str(const char *prefix, df::tiletype tt, df::tile_ return cache(prefix + dir); } -static const char * get_tile_carve_minimal(const df::coord &pos, - const tile_context &) { +static const char * get_tile_carve_minimal(color_ostream &out, const df::coord &pos, const tile_context &) { df::tiletype *tt = Maps::getTileType(pos); if (!tt) return NULL; @@ -536,8 +536,8 @@ static const char * get_tile_carve_minimal(const df::coord &pos, return NULL; } -static const char * get_tile_carve(const df::coord &pos, const tile_context &tc) { - const char * tile_carve_minimal = get_tile_carve_minimal(pos, tc); +static const char * get_tile_carve(color_ostream &out, const df::coord &pos, const tile_context &tc) { + const char * tile_carve_minimal = get_tile_carve_minimal(out, pos, tc); if (tile_carve_minimal) return tile_carve_minimal; @@ -612,6 +612,7 @@ static const char * get_construction_str(df::building *b) { case construction_type::TrackRampNEW: return "trackrampNEW"; case construction_type::TrackRampSEW: return "trackrampSEW"; case construction_type::TrackRampNSEW: return "trackrampNSEW"; + case construction_type::ReinforcedWall: return "CW"; case construction_type::NONE: default: return "~"; @@ -634,6 +635,13 @@ static const char * get_constructed_track_str(df::tiletype *tt, return cache(str); } +static const char * get_constructed_wall_str(df::coord pos) { + df::construction *c = Constructions::findAtTile(pos); + if (!c || !(c->flags.bits.reinforced)) + return "Cw"; + return "CW"; +} + static const char * get_constructed_floor_str(df::tiletype *tt) { if (tileSpecial(*tt) != df::tiletype_special::TRACK) return "Cf"; @@ -646,8 +654,7 @@ static const char * get_constructed_ramp_str(df::tiletype *tt) { return get_constructed_track_str(tt, "trackramp"); } -static const char * get_tile_construct(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_construct(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (ctx.b && ctx.b->getType() == building_type::Construction) return get_construction_str(ctx.b); @@ -656,7 +663,7 @@ static const char * get_tile_construct(const df::coord &pos, return NULL; switch (tileShape(*tt)) { - case tiletype_shape::WALL: return "Cw"; + case tiletype_shape::WALL: return get_constructed_wall_str(pos); case tiletype_shape::FLOOR: return get_constructed_floor_str(tt); case tiletype_shape::RAMP: return get_constructed_ramp_str(tt); case tiletype_shape::FORTIFICATION: return "CF"; @@ -679,7 +686,7 @@ static const char * if_pretty(const tile_context &ctx, const char *c) { } static bool is_rectangular(const df::building *bld) { - const df::building_extents &room = bld->room; + const df::building::T_room &room = bld->room; if (!room.extents) return true; for (int32_t y = 0; y < room.height; ++y) { @@ -735,7 +742,17 @@ static const char * get_bridge_str(df::building *b) { static const char * get_siege_str(df::building *b) { df::building_siegeenginest *se = virtual_cast(b); - return !se || se->type == df::siegeengine_type::Catapult ? "ic" : "ib"; + if (!se) + return "ic"; + + switch(se->type) { + case df::siegeengine_type::Catapult: return "ic"; + case df::siegeengine_type::Ballista: return "ib"; + case df::siegeengine_type::BoltThrower: return "it"; + default: + return "ic"; + } + } static const char * get_workshop_str(df::building *b) { @@ -1030,23 +1047,19 @@ static const char * add_expansion_syntax(const df::building *bld, return cache(s); } +static void add_expansion_syntax(const df::building *bld, ostringstream &keys) { + if (keys.str().empty()) + return; + pair size = get_building_size(bld); + keys << "(" << size.first << "x" << size.second << ")"; +} + static const char * add_expansion_syntax(const tile_context &ctx, const char *keys) { return add_expansion_syntax(ctx.b, keys); } -static const char * add_label(const tile_context &ctx, const char *keys) { - if (!keys) - return "~"; - auto bld = ctx.b; - ostringstream s; - // use building's id as the unique label - s << keys << "/" << "bld_" << bld->id; - return cache(s); -} - -static const char * get_tile_build(const df::coord &pos, - const tile_context &ctx) { +static const char * get_tile_build(color_ostream &out, const df::coord &pos, const tile_context &ctx) { if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { return NULL; } @@ -1059,251 +1072,311 @@ static const char * get_tile_build(const df::coord &pos, return add_expansion_syntax(ctx, keys); } -static const char * get_place_keys(const tile_context &ctx) { - df::building_stockpilest* sp = - virtual_cast(ctx.b); - if (!sp) { - return NULL; - } +static string quotify_inner(const string &s) { + if (s.find_first_of(" ,") == string::npos) + return s; + ostringstream buf; + buf << "\"\"" << s << "\"\""; + return buf.str(); +} - string keys; - df::stockpile_group_set &flags = sp->settings.flags; - if (flags.bits.animals) keys += 'a'; - if (flags.bits.food) keys += 'f'; - if (flags.bits.furniture) keys += 'u'; - if (flags.bits.coins) keys += 'n'; - if (flags.bits.corpses) keys += 'y'; - if (flags.bits.refuse) keys += 'r'; - if (flags.bits.stone) keys += 's'; - if (flags.bits.wood) keys += 'w'; - if (flags.bits.gems) keys += 'e'; - if (flags.bits.bars_blocks) keys += 'b'; - if (flags.bits.cloth) keys += 'h'; - if (flags.bits.leather) keys += 'l'; - if (flags.bits.ammo) keys += 'z'; - if (flags.bits.sheet) keys += 'S'; - if (flags.bits.finished_goods) keys += 'g'; - if (flags.bits.weapons) keys += 'p'; - if (flags.bits.armor) keys += 'd'; - - if (keys.empty()) - return "c"; - return cache(keys); +static string quotify_outer(const string &s) { + if (s.find_first_of("\",") == string::npos) + return s; + ostringstream buf; + buf << "\"" << s << "\""; + return buf.str(); } -static const char * get_tile_place(const df::coord &pos, - const tile_context &ctx) { - if (!ctx.b || ctx.b->getType() != building_type::Stockpile) - return NULL; +static string quotify_outer(const ostringstream &s) { + return quotify_outer(s.str()); +} - if (!is_rectangular(ctx)) - return add_label(ctx, get_place_keys(ctx)); +static void get_place_keys(color_ostream &out, ostringstream &keys, df::building_stockpilest* sp, bool add_label, bool add_properties) { + df::stockpile_group_set &flags = sp->settings.flags; + if (flags.bits.animals) keys << 'a'; + if (flags.bits.food) keys << 'f'; + if (flags.bits.furniture) keys << 'u'; + if (flags.bits.coins) keys << 'n'; + if (flags.bits.corpses) keys << 'y'; + if (flags.bits.refuse) keys << 'r'; + if (flags.bits.stone) keys << 's'; + if (flags.bits.wood) keys << 'w'; + if (flags.bits.gems) keys << 'e'; + if (flags.bits.bars_blocks) keys << 'b'; + if (flags.bits.cloth) keys << 'h'; + if (flags.bits.leather) keys << 'l'; + if (flags.bits.ammo) keys << 'z'; + if (flags.bits.sheet) keys << 'S'; + if (flags.bits.finished_goods) keys << 'g'; + if (flags.bits.weapons) keys << 'p'; + if (flags.bits.armor) keys << 'd'; + + if (keys.str().empty()) + keys << 'c'; + + if (!add_label && !add_properties) + return; - if (ctx.b->x1 != static_cast(pos.x) - || ctx.b->y1 != static_cast(pos.y)) { - return if_pretty(ctx, "`"); - } + if (add_label) + keys << "/" << "sp_" << sp->id; - return add_expansion_syntax(ctx, get_place_keys(ctx)); -} + if (!add_properties) + return; -/* TODO: understand how this changes for v50 -static bool hospital_maximums_eq(const df::hospital_supplies &a, - const df::hospital_supplies &b) { - return a.max_thread == b.max_thread && - a.max_cloth == b.max_cloth && - a.max_splints == b.max_splints && - a.max_crutches == b.max_crutches && - a.max_plaster == b.max_plaster && - a.max_buckets == b.max_buckets && - a.max_soap == b.max_soap; -} + vector properties; -static const char * get_zone_keys(const df::building_civzonest *zone) { - static const uint32_t DEFAULT_GATHER_FLAGS = - df::building_civzonest::T_gather_flags::mask_pick_trees | - df::building_civzonest::T_gather_flags::mask_pick_shrubs | - df::building_civzonest::T_gather_flags::mask_gather_fallen; - static const df::hospital_supplies DEFAULT_HOSPITAL; + if (!sp->name.empty()) + properties.push_back("name=" + quotify_inner(sp->name)); - ostringstream keys; - const df::building_civzonest::T_zone_flags &flags = zone->zone_flags; - - // inverted logic for Active since it's on by default - if (!flags.bits.active) keys << 'a'; - - // in UI order - if (flags.bits.water_source) keys << 'w'; - if (flags.bits.fishing) keys << 'f'; - if (flags.bits.gather) { - keys << 'g'; - if (zone->gather_flags.whole != DEFAULT_GATHER_FLAGS) { - keys << 'G'; - // logic is inverted since they're all on by default - if (!zone->gather_flags.bits.pick_trees) keys << 't'; - if (!zone->gather_flags.bits.pick_shrubs) keys << 's'; - if (!zone->gather_flags.bits.gather_fallen) keys << 'f'; - keys << '^'; - } + // only include take_from and give_to targets if they are named + vector take_from, give_to; + for (auto & target : sp->links.take_from_pile) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); + } + for (auto & target : sp->links.take_from_workshop) { + if (target->name.empty()) + continue; + take_from.push_back(target->name); } - if (flags.bits.garbage_dump) keys << 'd'; - if (flags.bits.pen_pasture) keys << 'n'; - if (flags.bits.pit_pond) { - keys << 'p'; - if (zone->pit_flags.bits.is_pond) - keys << "Pf^"; + for (auto & target : sp->links.give_to_pile) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); } - if (flags.bits.sand) keys << 's'; - if (flags.bits.clay) keys << 'c'; - if (flags.bits.meeting_area) keys << 'm'; - if (flags.bits.hospital) { - keys << 'h'; - const df::hospital_supplies &hospital = zone->hospital; - if (!hospital_maximums_eq(hospital, DEFAULT_HOSPITAL)) { - keys << "H{hospital"; - if (hospital.max_thread != DEFAULT_HOSPITAL.max_thread) - keys << " thread=" << hospital.max_thread; - if (hospital.max_cloth != DEFAULT_HOSPITAL.max_cloth) - keys << " cloth=" << hospital.max_cloth; - if (hospital.max_splints != DEFAULT_HOSPITAL.max_splints) - keys << " splints=" << hospital.max_splints; - if (hospital.max_crutches != DEFAULT_HOSPITAL.max_crutches) - keys << " crutches=" << hospital.max_crutches; - if (hospital.max_plaster != DEFAULT_HOSPITAL.max_plaster) - keys << " plaster=" << hospital.max_plaster; - if (hospital.max_buckets != DEFAULT_HOSPITAL.max_buckets) - keys << " buckets=" << hospital.max_buckets; - if (hospital.max_soap != DEFAULT_HOSPITAL.max_soap) - keys << " soap=" << hospital.max_soap; - keys << "}^"; - } + for (auto & target : sp->links.give_to_workshop) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + if (!take_from.empty()) + properties.push_back("take_from=" + quotify_inner(join_strings(",", take_from))); + if (!give_to.empty()) + properties.push_back("give_to=" + quotify_inner(join_strings(",", give_to))); + + if (sp->stockpile_flag.bits.use_links_only) + properties.push_back("links_only=true"); + + // simplify implementation; always record container counts, even if they are set to default values + if (!sp->storage.max_barrels && !sp->storage.max_bins && !sp->storage.max_wheelbarrows) + properties.push_back("containers=0"); + else { + properties.push_back("barrels=" + int_to_string(sp->storage.max_barrels)); + properties.push_back("bins=" + int_to_string(sp->storage.max_bins)); + properties.push_back("wheelbarrows=" + int_to_string(sp->storage.max_wheelbarrows)); } - if (flags.bits.animal_training) keys << 't'; - string keys_str = keys.str(); + // logistics features + Lua::CallLuaModuleFunction(out, "plugins.blueprint", "get_logistics_settings", + std::make_tuple(sp->stockpile_number), 6, [&](lua_State *L) { + if (lua_toboolean(L, -6)) properties.push_back("automelt=true"); + if (lua_toboolean(L, -5)) properties.push_back("autotrade=true"); + if (lua_toboolean(L, -4)) properties.push_back("autodump=true"); + if (lua_toboolean(L, -3)) properties.push_back("autotrain=true"); + if (lua_toboolean(L, -2)) properties.push_back("autoforbid=true"); + if (lua_toboolean(L, -1)) properties.push_back("autoclaim=true"); + }); - // there is no way to represent an active, but empty zone in quickfort - if (keys_str.empty()) - return NULL; + if (!properties.empty()) + keys << '{' << join_strings(" ", properties) << '}'; +} - // remove final '^' character if there is one - if (keys_str.back() == '^') - keys_str.pop_back(); +static df::coord get_first_tile(df::building *bld) { + df::coord first_pos; + cuboid bld_area(bld->x1, bld->y1, bld->z, bld->x2, bld->y2, bld->z); + bld_area.forCoord([&](const df::coord &pos) { + if (Buildings::containsTile(bld, pos)) { + first_pos = pos; + return false; + } + return true; + }, true); - return cache(keys_str); + return first_pos; } -static const char * get_tile_zone(const df::coord &pos, - const tile_context &ctx) { - vector civzones; - if (!Buildings::findCivzonesAt(&civzones, pos)) +static const char * get_tile_place(color_ostream &out, const df::coord &pos, const tile_context &ctx) { + df::building_stockpilest* sp = virtual_cast(ctx.b); + + if (!sp || sp->getType() != building_type::Stockpile) return NULL; - // we only have one "zone" blueprint, so use the "topmost" zone (that is, - // the one that is highlighted when the cursor is over this tile). - // overlapping zones are outside the scope of this plugin, I think. - df::building_civzonest *zone = civzones.back(); + bool rectangular = is_rectangular(sp); + bool is_first_tile = pos == get_first_tile(sp); + ostringstream keys; - if (!is_rectangular(zone)) - return get_zone_keys(zone); + if (!rectangular){ + get_place_keys(out, keys, sp, true, is_first_tile); + return cache(quotify_outer(keys)); + } - if (zone->x1 != static_cast(pos.x) - || zone->y1 != static_cast(pos.y)) { + if (!is_first_tile) return if_pretty(ctx, "`"); - } - return add_expansion_syntax(zone, get_zone_keys(zone)); + get_place_keys(out, keys, sp, false, true); + add_expansion_syntax(sp, keys); + return cache(quotify_outer(keys)); } -// surrounds the given string in quotes and replaces internal double quotes (") -// with double double quotes ("") (as per the csv spec) -static string csv_quote(const string &str) { - ostringstream outstr; - outstr << "\""; - - size_t start = 0; - auto end = str.find('"'); - while (end != string::npos) { - outstr << str.substr(start, end - start); - outstr << "\"\""; - start = end + 1; - end = str.find('"', start); - } - outstr << str.substr(start, end) << "\""; - - return outstr.str(); +static string get_reservation(color_ostream &out, df::building_civzonest *zone) { + string res; + Lua::CallLuaModuleFunction(out, "plugins.preserve-rooms", "preserve_rooms_getRoleAssignmentForZone", + std::make_tuple(zone), 1, [&](lua_State *L) { + if (lua_isstring(L, -1)) + res = lua_tostring(L, -1); + }); + return res; } -static const char * get_tile_query(const df::coord &pos, - const tile_context &ctx) { - string bld_name, zone_name; - auto & seen = ctx.processor->seen; +// TODO: handle locations +static void get_zone_keys(color_ostream &out, ostringstream &keys, df::building_civzonest *zone, bool add_label, bool add_properties) { + vector properties; - if (ctx.b && !seen.count(ctx.b)) { - bld_name = ctx.b->name; - seen.emplace(ctx.b); + if (!zone->name.empty()) + properties.push_back(quotify_inner("name=" + zone->name)); + if (!zone->spec_sub_flag.bits.active) + properties.push_back("active=false"); + if (auto reserved_for = get_reservation(out, zone); !reserved_for.empty()) { + properties.push_back("assigned_unit=" + reserved_for); } - vector civzones; - if (Buildings::findCivzonesAt(&civzones, pos)) { - auto civzone = civzones.back(); - if (!seen.count(civzone)) { - zone_name = civzone->name; - seen.emplace(civzone); + // in DFHack docs order + switch (zone->type) { + using namespace df::enums::civzone_type; + case MeetingHall: keys << "m"; break; + case Bedroom: keys << "b"; break; + case DiningHall: keys << "h"; break; + case Pen: keys << "n"; break; + case Pond: + keys << "p"; + { + if (zone->zone_settings.pond.flag.bits.keep_filled) + properties.push_back("pond=true"); + } + break; + case WaterSource: keys << "w"; break; + case Dungeon: keys << "j"; break; + case FishingArea: keys << "f"; break; + case SandCollection: keys << "s"; break; + case Office: keys << "o"; break; + case Dormitory: keys << "D"; break; + case Barracks: keys << "B"; break; + case ArcheryRange: + keys << "a"; + { + auto & archery = zone->zone_settings.archery; + if (archery.dir_x == 1 && archery.dir_y == 0) + properties.push_back("shoot_from=west"); + else if (archery.dir_x == -1 && archery.dir_y == 0) + properties.push_back("shoot_from=east"); + else if (archery.dir_x == 0 && archery.dir_y == 1) + properties.push_back("shoot_from=north"); + else if (archery.dir_x == 0 && archery.dir_y == -1) + properties.push_back("shoot_from=south"); + else { + keys.clear(); + return; // invalid direction + } + } + break; + case Dump: keys << "d"; break; + case AnimalTraining: keys << "t"; break; + case Tomb: + keys << "T"; + { + auto & tomb = zone->zone_settings.tomb; + if (!tomb.flags.bits.no_pets) + properties.push_back("pets=true"); + if (tomb.flags.bits.no_citizens) + properties.push_back("citizens=false"); + } + break; + case PlantGathering: + keys << "g"; + { + auto & gather = zone->zone_settings.gather; + if (!gather.flags.bits.pick_trees) + properties.push_back("pick_trees=false"); + if (!gather.flags.bits.pick_shrubs) + properties.push_back("pick_shrubs=false"); + if (!gather.flags.bits.gather_fallen) + properties.push_back("gather_fallen=false"); } + break; + case ClayCollection: keys << "c"; break; + default: + return; } - if (!bld_name.size() && !zone_name.size()) - return NULL; - - ostringstream str; - if (bld_name.size()) - str << "{givename name=" + csv_quote(bld_name) + "}"; - if (zone_name.size()) - str << "{namezone name=" + csv_quote(zone_name) + "}"; + if (!add_label && (!add_properties || properties.empty())) + return; - return cache(csv_quote(str.str())); + if (add_label) + keys << "/" << "zone_" << zone->id; + if (add_properties && !properties.empty()) + keys << '{' << join_strings(" ", properties) << '}'; } -static const char * get_tile_rooms(const df::coord &, const tile_context &ctx) { - if (!ctx.b || !ctx.b->is_room) +static const char * get_tile_zone(color_ostream &out, const df::coord &pos, const tile_context &ctx) { + vector civzones; + if (!Buildings::findCivzonesAt(&civzones, pos)) return NULL; - // get the maximum distance from the center of the building - df::building_extents &room = ctx.b->room; - int32_t x1 = room.x; - int32_t x2 = room.x + room.width - 1; - int32_t y1 = room.y; - int32_t y2 = room.y + room.height - 1; - - int32_t dimx = std::max(ctx.b->centerx - x1, x2 - ctx.b->centerx); - int32_t dimy = std::max(ctx.b->centery - y1, y2 - ctx.b->centery); - int32_t max_dim = std::max(dimx, dimy); - - switch (max_dim) { - case 0: return "r---&"; - case 1: return "r--&"; - case 2: return "r-&"; - case 3: return "r&"; - case 4: return "r+&"; + // we can handle overlapping zones in a single blueprint, but only if one of + // the following is true: + // -- they exactly overlap (even if they aren't rectangular) + // -- no two non-rectangular zones overlap + + // for a first implementation, we will only handle overlapping zones if they + // are rectangular and have different upper-left corners. if this is the upper + // left corner of a rectangular zone, we will output for that zone. otherwise, + // if this pos is interior to all zones, then it doesn't matter which we choose. + + df::building_civzonest * primary_zone = civzones[0]; + df::coord upper_left_corner; + + if (civzones.size() == 1) { + upper_left_corner = get_first_tile(primary_zone); + } else if (civzones.size() > 1) { + for (auto zone : civzones) { + if (!is_rectangular(zone)) + continue; + primary_zone = zone; + upper_left_corner = get_first_tile(zone); + if (pos == upper_left_corner) + break; + } } - ostringstream str; - str << "r{+ " << (max_dim - 3) << "}&"; - return cache(str); + bool rectangular = is_rectangular(primary_zone); + bool is_first_tile = pos == upper_left_corner; + ostringstream keys; + + if (!rectangular) { + get_zone_keys(out, keys, primary_zone, true, is_first_tile); + return cache(quotify_outer(keys)); + } + + if (!is_first_tile) + return if_pretty(ctx, "`"); + + get_zone_keys(out, keys, primary_zone, false, true); + add_expansion_syntax(primary_zone, keys); + return cache(quotify_outer(keys)); } -*/ static bool create_output_dir(color_ostream &out, const blueprint_options &opts) { - string basename = BLUEPRINT_USER_DIR + opts.name; - size_t last_slash = basename.find_last_of("/"); - string parent_path = basename.substr(0, last_slash); + std::filesystem::path BLUEPRINT_USER_DIR = Core::getInstance().getConfigPath() / "blueprints"; + std::filesystem::path basename = BLUEPRINT_USER_DIR / opts.name; + std::filesystem::path parent_path = basename.parent_path(); // create output directory if it doesn't already exist if (!Filesystem::mkdir_recursive(parent_path)) { - out.printerr("could not create output directory: '%s'\n", - parent_path.c_str()); + out.printerr("could not create output directory: '{}'\n", + parent_path); return false; } return true; @@ -1503,13 +1576,7 @@ static bool do_transform(color_ostream &out, get_tile_build, ensure_building); add_processor(processors, opts, "place", "place", opts.place, get_tile_place, ensure_building); -/* TODO: understand how this changes for v50 add_processor(processors, opts, "zone", "zone", opts.zone, get_tile_zone); - add_processor(processors, opts, "query", "query", opts.query, - get_tile_query, ensure_building); - add_processor(processors, opts, "query", "rooms", opts.rooms, - get_tile_rooms, ensure_building); -*/ if (processors.empty()) { out.printerr("no phases requested! nothing to do!\n"); return false; @@ -1530,7 +1597,7 @@ static bool do_transform(color_ostream &out, ctx.processor = &processor; if (processor.init_ctx) processor.init_ctx(pos, ctx); - const char *tile_str = processor.get_tile(pos, ctx); + const char *tile_str = processor.get_tile(out, pos, ctx); if (tile_str) { // ensure our z-index is in the order we want to write auto area = processor.mapdata.emplace(abs(z - start.z), @@ -1599,9 +1666,9 @@ static command_result do_blueprint(color_ostream &out, command << " " << parameters[i]; } string command_str = command.str(); - out.print("launching %s\n", command_str.c_str()); + out.print("launching {}\n", command_str); - Core::getInstance().setHotkeyCmd(command_str); + Core::getInstance().getHotkeyManager()->setHotkeyCommand(command_str); return CR_OK; } @@ -1627,7 +1694,7 @@ static command_result do_blueprint(color_ostream &out, } } if (!Maps::isValidTilePos(start)) { - out.printerr("Invalid start position: %d,%d,%d\n", + out.printerr("Invalid start position: {},{},{},\n", start.x, start.y, start.z); return CR_FAILURE; } @@ -1652,8 +1719,7 @@ static command_result do_blueprint(color_ostream &out, bool ok = do_transform(out, start, end, options, files); - // clear caches - cache(NULL); + clear_caches(); return ok ? CR_OK : CR_FAILURE; } @@ -1691,7 +1757,7 @@ command_result blueprint(color_ostream &out, vector ¶meters) { else { out.print("Generated blueprint file(s):\n"); for (string &fname : files) - out.print(" %s\n", fname.c_str()); + out.print(" {}\n", fname); } } return cr; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index fe317e4705..8af8992450 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -13,6 +13,7 @@ #include "df/construction_type.h" #include "df/burrow.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/job_item.h" #include "df/organic_mat_category.h" @@ -64,7 +65,7 @@ static Tasks tasks; // planned_buildings, then it has either been built or desroyed. therefore there is // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { - DEBUG(control,out).print("removing persistent data for building %d\n", id); + DEBUG(control,out).print("removing persistent data for building {}\n", id); World::DeletePersistentData(bld_config); planned_buildings.erase(id); } @@ -97,8 +98,8 @@ static const vector & get_job_items(color_ostream &out, Bu std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), index+1), 1, [&](lua_State *L) { df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(control,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); + DEBUG(control,out).print("retrieving job_item for {} index={}: {}\n", + key, index, static_cast(jitem)); if (!jitem) failed = true; else @@ -125,35 +126,35 @@ static void cache_matched(int16_t type, int32_t index) { MaterialInfo mi; mi.decode(type, index); if (mi.matches(stone_cat)) { - DEBUG(control).print("cached stone material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached stone material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "stone")); } else if (mi.matches(wood_cat)) { - DEBUG(control).print("cached wood material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached wood material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "wood")); } else if (mi.matches(metal_cat)) { - DEBUG(control).print("cached metal material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached metal material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "metal")); } else if (mi.matches(glass_cat)) { - DEBUG(control).print("cached glass material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached glass material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "glass")); } else if (mi.matches(gem_cat)) { - DEBUG(control).print("cached gem material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached gem material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "gem")); } else if (mi.matches(clay_cat)) { - DEBUG(control).print("cached clay material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached clay material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "clay")); } else if (mi.matches(cloth_cat)) { - DEBUG(control).print("cached cloth material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached cloth material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "cloth")); } else if (mi.matches(silk_cat)) { - DEBUG(control).print("cached silk material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached silk material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "silk")); } else if (mi.matches(yarn_cat)) { - DEBUG(control).print("cached yarn material: %s (%d, %d)\n", mi.toString().c_str(), type, index); + DEBUG(control).print("cached yarn material: {} ({}, {})\n", mi.toString(), type, index); mat_cache.emplace(mi.toString(), std::make_pair(mi, "yarn")); } else - TRACE(control).print("not matched: %s\n", mi.toString().c_str()); + TRACE(control).print("not matched: {}\n", mi.toString()); } static void load_organic_material_cache(df::organic_mat_category cat) { @@ -166,12 +167,12 @@ static void load_organic_material_cache(df::organic_mat_category cat) { } static void load_material_cache() { - df::world_raws &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + auto &raws = world->raws; + for (int i = 1; i < df::builtin_mats::CREATURE_1; ++i) if (raws.mat_table.builtin[i]) cache_matched(i, -1); - for (size_t i = 0; i < raws.inorganics.size(); i++) + for (size_t i = 0; i < raws.inorganics.all.size(); i++) cache_matched(0, i); load_organic_material_cache(df::organic_mat_category::Wood); @@ -201,7 +202,7 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bool unsuspend_on_finalize); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -214,12 +215,12 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector getType(), get_subtype(bld), bld->getCustomType()); if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { - WARN(control).print("loaded state for building %d doesn't match world\n", pb.id); + WARN(control).print("loaded state for building {} doesn't match world\n", pb.id); pb.remove(out); continue; } @@ -354,7 +355,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot configure %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot configure {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -428,8 +429,8 @@ vector getVectorIds(color_ostream &out, const df::job_it // if the filter already has the vector_id set to something specific, use it if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) { - DEBUG(control,out).print("using vector_id from job_item: %s\n", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + DEBUG(control,out).print("using vector_id from job_item: {}\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id)); ret.push_back(job_item->vector_id); return ret; } @@ -460,7 +461,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo return false; if (bld->jobs.size() != 1) { - DEBUG(control,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + DEBUG(control,out).print("unexpected number of jobs: want 1, got {}\n", bld->jobs.size()); return false; } @@ -489,10 +490,10 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb, bo for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { tasks[vector_id][bucket].emplace_back(id, rev_jitem_index); - DEBUG(control,out).print("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, rev_jitem_index, tasks.size(), + DEBUG(control,out).print("added task: {}/{}/{}, {}; " + "{} vector(s), {} filter bucket(s), {} task(s) in bucket\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket, id, rev_jitem_index, tasks.size(), tasks[vector_id].size(), tasks[vector_id][bucket].size()); } } @@ -520,16 +521,16 @@ static string get_desc_string(color_ostream &out, df::job_item *jitem, static void printStatus(color_ostream &out) { DEBUG(control,out).print("entering buildingplan_printStatus\n"); - out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print("buildingplan is {}\n\n", is_enabled ? "enabled" : "disabled"); out.print("Current settings:\n"); - out.print(" use blocks: %s\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); - out.print(" use boulders: %s\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); - out.print(" use logs: %s\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); - out.print(" use bars: %s\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); - out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: %s\n", + out.print(" use blocks: {}\n", config.get_bool(CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: {}\n", config.get_bool(CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: {}\n", config.get_bool(CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: {}\n", config.get_bool(CONFIG_BARS) ? "yes" : "no"); + out.print(" plan constructions on tiles with existing constructed floors/ramps when using box select: {}\n", config.get_bool(CONFIG_RECONSTRUCT) ? "yes" : "no"); auto burrow = df::burrow::find(config.get_int(CONFIG_BURROW)); - out.print(" ignore building materials in burrow: %s\n", burrow ? burrow->name.c_str() : "none"); + out.print(" ignore building materials in burrow: {}\n", burrow ? burrow->name.c_str() : "none"); out.print("\n"); size_t bld_count = 0; @@ -560,10 +561,10 @@ static void printStatus(color_ostream &out) { } if (bld_count) { - out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + out.print("Waiting for {} item(s) to be produced for {} building(s):\n", total, bld_count); for (auto &count : counts) - out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); + out.print(" {:3} {}{}\n", count.second, count.first, count.second == 1 ? "" : "s"); } else { out.print("Currently no planned buildings\n"); } @@ -571,7 +572,7 @@ static void printStatus(color_ostream &out) { } static bool setSetting(color_ostream &out, string name, bool value) { - DEBUG(control,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + DEBUG(control,out).print("entering setSetting ({} -> {})\n", name, value ? "true" : "false"); if (name == "blocks") config.set_bool(CONFIG_BLOCKS, value); else if (name == "boulders") @@ -583,7 +584,7 @@ static bool setSetting(color_ostream &out, string name, bool value) { else if (name == "reconstruct") config.set_bool(CONFIG_RECONSTRUCT, value); else { - out.printerr("unrecognized setting: '%s'\n", name.c_str()); + out.printerr("unrecognized setting: '{}'\n", name); return false; } @@ -654,8 +655,8 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ vector *item_ids = NULL, map *counts = NULL) { DEBUG(control,out).print( - "entering scanAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering scanAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = heat_override ? *heat_override : get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); @@ -700,7 +701,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ } } - DEBUG(control,out).print("found matches %d\n", count); + DEBUG(control,out).print("found matches {}\n", count); return count; } @@ -713,8 +714,8 @@ static int getAvailableItems(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, false, NULL, &item_ids); Lua::PushVector(L, item_ids); @@ -731,8 +732,8 @@ static int getAvailableItemsByHeat(lua_State *L) { int index = luaL_checkint(L, 4); HeatSafety heat = (HeatSafety)luaL_checkint(L, 5); DEBUG(control,*out).print( - "entering getAvailableItemsByHeat building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getAvailableItemsByHeat building_type={} subtype={} custom={} index={} heat={}\n", + ENUM_AS_STR(type), subtype, custom, index, static_cast(heat)); vector item_ids; scanAvailableItems(*out, type, subtype, custom, index, true, true, &heat, &item_ids); Lua::PushVector(L, item_ids); @@ -756,8 +757,8 @@ static int getGlobalSettings(lua_State *L) { static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(control,out).print( - "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering countAvailableItems building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); int count = scanAvailableItems(out, type, subtype, custom, index, false, false); if (count) return count; @@ -816,8 +817,8 @@ static int setMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialMaskFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialMaskFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -846,8 +847,8 @@ static int setMaterialMaskFilter(lua_State *L) { mask |= yarn_cat.whole; } DEBUG(control,*out).print( - "setting material mask filter for building_type=%d subtype=%d custom=%d index=%d to %x\n", - type, subtype, custom, index, mask); + "setting material mask filter for building_type={} subtype={} custom={} index={} to {:x}\n", + ENUM_AS_STR(type), subtype, custom, index, mask); ItemFilter filter = filters[index]; filter.setMaterialMask(mask); set new_mats; @@ -875,8 +876,8 @@ static int getMaterialMaskFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key); if (index < 0 || filters.getItemFilters().size() <= (size_t)index) @@ -906,8 +907,8 @@ static int setMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering setMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering setMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -920,8 +921,8 @@ static int setMaterialFilter(lua_State *L) { mats.emplace(mat_cache.at(mat).first); } DEBUG(control,*out).print( - "setting material filter for building_type=%d subtype=%d custom=%d index=%d to %zd materials\n", - type, subtype, custom, index, mats.size()); + "setting material filter for building_type={} subtype={} custom={} index={} to {} materials\n", + ENUM_AS_STR(type), subtype, custom, index, mats.size()); ItemFilter filter = filters[index]; filter.setMaterials(mats); // ensure relevant masks are explicitly enabled @@ -963,8 +964,8 @@ static int getMaterialFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getMaterialFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) @@ -1004,8 +1005,8 @@ static int getMaterialFilter(lua_State *L) { static void setChooseItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int choose) { DEBUG(control,out).print( - "entering setChooseItems building_type=%d subtype=%d custom=%d choose=%d\n", - type, subtype, custom, choose); + "entering setChooseItems building_type={} subtype={} custom={} choose={}\n", + ENUM_AS_STR(type), subtype, custom, choose); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(out, key); filters.setChooseItems(choose); @@ -1020,8 +1021,8 @@ static int getChooseItems(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getChooseItems building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getChooseItems building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getChooseItems()); return 1; @@ -1045,8 +1046,8 @@ static int getHeatSafetyFilter(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getHeatSafetyFilter building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); Lua::Push(L, heat); @@ -1069,8 +1070,8 @@ static int getSpecials(lua_State *L) { int16_t subtype = luaL_checkint(L, 2); int32_t custom = luaL_checkint(L, 3); DEBUG(control,*out).print( - "entering getSpecials building_type=%d subtype=%d custom=%d\n", - type, subtype, custom); + "entering getSpecials building_type={} subtype={} custom={}\n", + ENUM_AS_STR(type), subtype, custom); BuildingTypeKey key(type, subtype, custom); Lua::Push(L, get_item_filters(*out, key).getSpecials()); return 1; @@ -1100,8 +1101,8 @@ static int getQualityFilter(lua_State *L) { int32_t custom = luaL_checkint(L, 3); int index = luaL_checkint(L, 4); DEBUG(control,*out).print( - "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", - type, subtype, custom, index); + "entering getQualityFilter building_type={} subtype={} custom={} index={}\n", + ENUM_AS_STR(type), subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &filters = get_item_filters(*out, key).getItemFilters(); if (index < 0 || filters.size() <= (size_t)index) diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index d55b5f5da4..7e8187dfce 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -77,7 +77,7 @@ static bool isAccessible(color_ostream& out, df::item* item) { df::coord item_pos = Items::getPosition(item); uint16_t walkability_group = Maps::getWalkableGroup(item_pos); bool is_walkable = accessible_walkability_groups.contains(walkability_group); - TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + TRACE(cycle, out).print("item {} in walkability_group {} at ({},{},{}) is {}accessible from job site\n", item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); return is_walkable; } @@ -192,7 +192,7 @@ bool isJobReady(color_ostream &out, const std::vector &jitems) { int needed_items = 0; for (auto job_item : jitems) { needed_items += job_item->quantity; } if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + DEBUG(cycle,out).print("building needs {} more item(s)\n", needed_items); return false; } return true; @@ -208,7 +208,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // 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, bool unsuspend_on_finalize) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + DEBUG(cycle,out).print("finalizing building {}\n", bld->id); auto job = bld->jobs[0]; // sort the items so they get added to the structure in the correct order @@ -255,7 +255,7 @@ static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, if (bld && bld->jobs[0]->job_items.elements[task.second]->quantity) return bld; } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + DEBUG(cycle,out).print("discarding invalid task: bld={}, job_item_idx={}\n", id, task.second); task_queue.pop_front(); } return NULL; @@ -273,9 +273,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); const 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", + DEBUG(cycle,out).print("matching {} item(s) in vector {} against {} filter bucket(s)\n", item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + ENUM_KEY_STR(job_item_vector_id, vector_id), buckets.size()); // items we might want to attach (and their positions) @@ -287,12 +287,12 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, std::vector> matching; size_t num_matching = 0; - DEBUG(cycle,out).print("%zu items available for assignment\n", available.size()); + DEBUG(cycle,out).print("{} items available for assignment\n", available.size()); for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - TRACE(cycle,out).print("scanning bucket: %s/%s\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), bucket_it->first.c_str()); + TRACE(cycle,out).print("scanning bucket: {}/{}\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), bucket_it->first); auto & task_queue = bucket_it->second; bool first_task = true; @@ -322,7 +322,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, num_matching = matching.size(); first_task = false; - TRACE(cycle,out).print("first task in bucket: found %zu matching items\n", + TRACE(cycle,out).print("first task in bucket: found {} matching items\n", num_matching); } // every task: find and attach closest matching item (if any) @@ -337,20 +337,20 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, } auto item = closest->second; // some item must be closest. - if (Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { + if (Job::attachJobItem(job, item, df::job_role_type::Hauled, filter_idx)) { MaterialInfo material; material.decode(item); ItemTypeInfo item_type; item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s (distance %d) to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), + DEBUG(cycle,out).print("attached {} {} (distance {}) to filter {} for {}({}): {}/{}\n", + material.toString(), + item_type.toString(), distance(closest->first, jpos), filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), + ENUM_KEY_STR(building_type, bld->getType()), id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first); // clean up fulfilled task task_queue.pop_front(); // keep quantity aligned with the actual number of remaining @@ -373,10 +373,10 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, if (task_queue.empty()) { DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); + "removing empty item bucket: {}/{}; {} left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), + bucket_it->first, + buckets.size() - 1); bucket_it = buckets.erase(bucket_it); } else { ++bucket_it; @@ -420,8 +420,8 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = it->second; 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(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); it = tasks.erase(it); } @@ -434,12 +434,12 @@ void buildingplan_cycle(color_ostream &out, Tasks &tasks, auto & buckets = tasks[vector_id]; 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(), + DEBUG(cycle,out).print("removing empty vector: {}; {} vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id), tasks.size() - 1); tasks.erase(vector_id); } } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + DEBUG(cycle,out).print("cycle done; {} registered building(s) left\n", planned_buildings.size()); } diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp index 710878d643..e8136b8896 100644 --- a/plugins/buildingplan/buildingtypekey.cpp +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -21,7 +21,7 @@ static BuildingTypeKey deserialize(color_ostream &out, const std::string &serial vector key_parts; split_string(&key_parts, serialized, ","); if (key_parts.size() != 3) { - WARN(control,out).print("invalid key_str: '%s'\n", serialized.c_str()); + WARN(control,out).print("invalid key_str: '{}'\n", serialized); return BuildingTypeKey(df::building_type::NONE, -1, -1); } return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h index 81bb043c55..5b21b6cc2c 100644 --- a/plugins/buildingplan/buildingtypekey.h +++ b/plugins/buildingplan/buildingtypekey.h @@ -17,6 +17,17 @@ struct BuildingTypeKey : public std::tuple std::string serialize() const; }; +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(const BuildingTypeKey& key, FormatContext& ctx) const + { + return fmt::formatter::format( + fmt::format("({}, {}, {})", ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)), ctx); + } +}; + struct BuildingTypeKeyHash { std::size_t operator() (const BuildingTypeKey & key) const; }; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index eaa11335d0..0e1cab8548 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -40,8 +40,8 @@ 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(ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER) { - DEBUG(control,out).print("creating persistent data for filter key %d,%d,%d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key)); + DEBUG(control,out).print("creating persistent data for filter key {},{},{}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key)); filter_config = World::AddPersistentSiteData(FILTER_CONFIG_KEY); filter_config.set_int(FILTER_CONFIG_TYPE, std::get<0>(key)); filter_config.set_int(FILTER_CONFIG_SUBTYPE, std::get<1>(key)); @@ -61,16 +61,16 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f choose_items > ItemSelectionChoice::ITEM_SELECTION_CHOICE_AUTOMATERIAL) choose_items = ItemSelectionChoice::ITEM_SELECTION_CHOICE_FILTER; auto &serialized = filter_config.get_str(); - DEBUG(control,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()); + DEBUG(control,out).print("deserializing default item filters for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); if (!jitems.size()) return; std::vector elems; split_string(&elems, serialized, "|"); std::vector filters = deserialize_item_filters(out, elems[0]); if (filters.size() != jitems.size()) { - WARN(control,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + WARN(control,out).print("ignoring invalid filters_str for key {},{},{}: '{}'\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), serialized); item_filters.resize(jitems.size()); } else item_filters = filters; @@ -99,13 +99,13 @@ void DefaultItemFilters::setSpecial(const std::string &special, bool val) { void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { if (index < 0 || item_filters.size() <= (size_t)index) { - WARN(control,out).print("invalid index for filter key %d,%d,%d: %d\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + WARN(control,out).print("invalid index for filter key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), index); return; } item_filters[index] = filter; filter_config.set_str( serialize(item_filters, specials)); - DEBUG(control,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", - std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.get_str().c_str()); + DEBUG(control,out).print("updated item filter and persisted for key {},{},{}: {}\n", + ENUM_AS_STR(std::get<0>(key)), std::get<1>(key), std::get<2>(key), filter_config.get_str()); } diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index 0e4569f156..09a4a15c8e 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -41,7 +41,7 @@ static bool deserializeMaterialMask(const string& ser, df::dfhack_material_categ return true; if (!parseJobMaterialCategory(&mat_mask, ser)) { - DEBUG(control).print("invalid job material category serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid job material category serialization: '{}'", ser); return false; } return true; @@ -56,7 +56,7 @@ static bool deserializeMaterials(const string& ser, set &m for (auto m = mat_names.begin(); m != mat_names.end(); m++) { DFHack::MaterialInfo material; if (!material.find(*m) || !material.isValid()) { - DEBUG(control).print("invalid material name serialization: '%s'", ser.c_str()); + DEBUG(control).print("invalid material name serialization: '{}'", ser); return false; } materials.emplace(material); @@ -68,7 +68,7 @@ ItemFilter::ItemFilter(color_ostream &out, const string& serialized) : ItemFilte vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() < 5) { - DEBUG(control,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + DEBUG(control,out).print("invalid ItemFilter serialization: '{}'", serialized); return; } @@ -155,8 +155,8 @@ bool ItemFilter::matches(DFHack::MaterialInfo &material) const { bool ItemFilter::matches(df::item *item) const { int16_t quality = (item->flags.bits.artifact ? df::item_quality::Artifact : item->getQuality()); if (quality < min_quality || quality > max_quality) { - TRACE(cycle).print("item outside of quality range (%d not between %d and %d)\n", - quality, min_quality, max_quality); + TRACE(cycle).print("item outside of quality range ({} not between {} and {})\n", + ENUM_AS_STR(static_cast(quality)), ENUM_AS_STR(min_quality), ENUM_AS_STR(max_quality)); return false; } diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 8a1f67d3cf..95a216c302 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -7,6 +7,8 @@ #include "df/dfhack_material_category.h" #include "df/item_quality.h" +#include + class ItemFilter { public: ItemFilter(); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index 5888d169e7..e0f5a8984e 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -41,8 +41,8 @@ static vector> deserialize_vector_ids(color_ostre split_string(&rawstrs, bld_config.get_str(), "|"); const string &serialized = rawstrs[0]; - DEBUG(control,out).print("deserializing vector ids for building %d: %s\n", - bld_config.get_int(BLD_CONFIG_ID), serialized.c_str()); + DEBUG(control,out).print("deserializing vector ids for building {}: {}\n", + bld_config.get_int(BLD_CONFIG_ID), serialized); vector joined; split_string(&joined, serialized, ";"); @@ -100,12 +100,12 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafe : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), item_filters(item_filters.getItemFilters()), specials(item_filters.getSpecials()) { - DEBUG(control,out).print("creating persistent data for building %d\n", id); + DEBUG(control,out).print("creating persistent data for building {}\n", id); bld_config = World::AddPersistentSiteData(BLD_CONFIG_KEY); bld_config.set_int(BLD_CONFIG_ID, id); bld_config.set_int(BLD_CONFIG_HEAT, heat_safety); bld_config.set_str(serialize(vector_ids, item_filters)); - DEBUG(control,out).print("serialized state for building %d: %s\n", id, bld_config.get_str().c_str()); + DEBUG(control,out).print("serialized state for building {}: {}\n", id, bld_config.get_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index d06e95efb6..172a7d37d2 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -45,7 +47,7 @@ static void jobStartedHandler(color_ostream& out, void* ptr); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status, out).print("initializing %s\n", plugin_name); + DEBUG(status, out).print("initializing {}\n", plugin_name); commands.push_back( PluginCommand("burrow", "Quickly adjust burrow tiles and units.", @@ -60,7 +62,7 @@ static void reset() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(status, out).print("%s from the API\n", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API\n", is_enabled ? "enabled" : "disabled"); reset(); if (enable) { init_diggers(out); @@ -71,13 +73,13 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { - DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + DEBUG(status, out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(status, out).print("shutting down %s\n", plugin_name); + DEBUG(status, out).print("shutting down {}\n", plugin_name); reset(); return CR_OK; } @@ -90,7 +92,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -134,8 +136,8 @@ static void jobStartedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job started: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job started: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype *tt = Maps::getTileType(pos); if (tt) active_dig_jobs[pos] = *tt; @@ -197,8 +199,8 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { return; const df::coord &pos = job->pos; - DEBUG(event, out).print("dig job completed: id=%d, pos=(%d,%d,%d), type=%s\n", - job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); + DEBUG(event, out).print("dig job completed: id={}, pos=({}, {}, {}), type={}\n", + job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type)); df::tiletype prev_tt = active_dig_jobs[pos]; df::tiletype *tt = Maps::getTileType(pos); @@ -542,7 +544,7 @@ static void flood_fill(lua_State *L, bool enable) { bool start_outside = is_outside(start_pos, start_des); bool start_hidden = start_des->bits.hidden; uint16_t start_walk = Maps::getWalkableGroup(start_pos); - DEBUG(status).print("starting pos: (%d,%d,%d); outside: %d; hidden: %d\n", + DEBUG(status).print("starting pos: ({},{},{}); outside: {}; hidden: {}\n", start_pos.x, start_pos.y, start_pos.z, start_outside, start_hidden); std::stack flood; @@ -552,7 +554,7 @@ static void flood_fill(lua_State *L, bool enable) { const df::coord pos = flood.top(); flood.pop(); - TRACE(status).print("pos: (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(status).print("pos: ({},{},{})\n", pos.x, pos.y, pos.z); df::tile_designation *des = Maps::getTileDesignation(pos); if (!des || diff --git a/plugins/changeitem.cpp b/plugins/changeitem.cpp index 33387aa141..c5437f79a7 100644 --- a/plugins/changeitem.cpp +++ b/plugins/changeitem.cpp @@ -211,7 +211,7 @@ command_result df_changeitem(color_ostream &out, vector & parameters) // changeitem doesn't do stuff with map blocks... { MapCache MC; - Block * b = MC.BlockAt(pos_cursor / 16); + Block * b = MC.BlockAtTile(pos_cursor); if(!b) { out.printerr("Cursor is in an invalid/uninitialized area. Place it over a floor.\n"); @@ -236,7 +236,7 @@ command_result df_changeitem(color_ostream &out, vector & parameters) changeitem_execute(out, item, info, force, change_material, new_material, change_quality, new_quality, change_subtype, new_subtype); processed_total++; } - out.print("Done. %d items processed.\n", processed_total); + out.print("Done. {} items processed.\n", processed_total); } else { diff --git a/plugins/changevein.cpp b/plugins/changevein.cpp index a73d9584e9..0463b794ca 100644 --- a/plugins/changevein.cpp +++ b/plugins/changevein.cpp @@ -1,13 +1,14 @@ // Allow changing the material of a mineral inclusion #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "TileTypes.h" -#include "DataDefs.h" +#include "modules/Gui.h" #include "modules/Maps.h" #include "modules/Materials.h" -#include "TileTypes.h" #include "df/block_square_event.h" #include "df/block_square_event_mineralst.h" @@ -21,7 +22,6 @@ using namespace df::enums; DFHACK_PLUGIN("changevein"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); constexpr uint8_t NORTH = 0; constexpr uint8_t EAST = 1; @@ -212,7 +212,8 @@ command_result df_changevein (color_ostream &out, vector & parameters) out.printerr("Map is not available!\n"); return CR_FAILURE; } - if (!cursor || cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor detected - please place the cursor over a mineral vein.\n"); return CR_FAILURE; @@ -232,14 +233,14 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } - df::map_block *block = Maps::getTileBlock(cursor->x, cursor->y, cursor->z); + auto block = Maps::getTileBlock(pos); if (!block) { out.printerr("Invalid tile selected.\n"); return CR_FAILURE; } df::block_square_event_mineralst *mineral = NULL; - int tx = cursor->x % 16, ty = cursor->y % 16; + int tx = pos.x % 16, ty = pos.y % 16; for (auto evt : block->block_events) { if (evt->getType() != block_square_event_type::mineral) @@ -254,6 +255,12 @@ command_result df_changevein (color_ostream &out, vector & parameters) return CR_FAILURE; } + if (mineral->inorganic_mat == mi.index) + { + out.printerr("Selected tile is already of the target material.\n"); + return CR_FAILURE; + } + VeinEdgeBitmask mask = VeinEdgeBitmask(mineral); mineral->inorganic_mat = mi.index; ChangeSameBlockVeins(block, mineral, mask, mi.index); diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index c1a5b5953f..71cfdddb8d 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -111,7 +111,7 @@ void ChannelGroups::add(const df::coord &map_pos) { // we already have group "prime" if you will, so we're going to merge the new find into prime Group &group2 = groups.at(index2); // merge - TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(), + TRACE(groups).print(" -> merging two groups. group 1 size: {}. group 2 size: {}\n", group->size(), group2.size()); for (auto pos2: group2) { group->emplace(pos2); @@ -119,7 +119,7 @@ void ChannelGroups::add(const df::coord &map_pos) { } group2.clear(); free_spots.emplace(index2); - TRACE(groups).print(" merged size: %zu\n", group->size()); + TRACE(groups).print(" merged size: {}\n", group->size()); } } } @@ -143,13 +143,13 @@ void ChannelGroups::add(const df::coord &map_pos) { } // puts the "add" in "ChannelGroups::add" group->emplace(map_pos); - DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size()); + DEBUG(groups).print(" = group[{}] of (" COORD ") is size: {}\n", group_index, COORDARGS(map_pos), group->size()); // we may have performed a merge, so we update all the `coord -> group index` mappings for (auto &wpos: *group) { groups_map[wpos] = group_index; } - DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size()); + DEBUG(groups).print(" <- add() exits, there are {} mappings\n", groups_map.size()); } // scans a single tile for channel designations @@ -192,7 +192,7 @@ void ChannelGroups::scan(bool full_scan) { std::set gone_jobs; set_difference(last_jobs, jobs, gone_jobs); set_difference(jobs, last_jobs, new_jobs); - INFO(groups).print("gone jobs: %zd\nnew jobs: %zd\n",gone_jobs.size(), new_jobs.size()); + INFO(groups).print("gone jobs: {}\nnew jobs: {}\n",gone_jobs.size(), new_jobs.size()); for (auto &pos : new_jobs) { add(pos); } @@ -236,7 +236,7 @@ void ChannelGroups::scan(bool full_scan) { for (df::block_square_event* event: block->block_events) { if (auto evT = virtual_cast(event)) { // we want to let the user keep some designations free of being managed - TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); + TRACE(groups).print(" tile designation priority: {}\n", evT->priority[lx][ly]); if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { if (empty_group) { group_blocks.emplace(block); @@ -338,9 +338,9 @@ void ChannelGroups::debug_groups() { int idx = 0; DEBUG(groups).print(" debugging group data\n"); for (auto &group: groups) { - DEBUG(groups).print(" group %d (size: %zu)\n", idx, group.size()); + DEBUG(groups).print(" group {} (size: {})\n", idx, group.size()); for (auto &pos: group) { - DEBUG(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(groups).print(" ({},{},{})\n", pos.x, pos.y, pos.z); } idx++; } @@ -350,9 +350,9 @@ void ChannelGroups::debug_groups() { // prints debug info group mappings void ChannelGroups::debug_map() { if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { - INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); + INFO(groups).print("Group Mappings: {}\n", groups_map.size()); for (auto &pair: groups_map) { - TRACE(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second); + TRACE(groups).print(" map[" COORD "] = {}\n", COORDARGS(pair.first), pair.second); } } } diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 67f8742b38..6c85fda951 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -89,7 +89,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // the tile below is not solid earth // we're counting accessibility then dealing with 0 access DEBUG(manager).print("analysis: cave-in condition found\n"); - INFO(manager).print("(%d) checking accessibility of (" COORD ") from (" COORD ")\n", cavein_possible,COORDARGS(pos),COORDARGS(miner_pos)); + INFO(manager).print("({}) checking accessibility of ({}) from ({})\n", cavein_possible,pos,miner_pos); auto access = count_accessibility(miner_pos, pos); if (access == 0) { TRACE(groups).print(" has 0 access\n"); @@ -100,7 +100,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // todo: engage dig management, swap channel designations for dig } } else { - WARN(manager).print(" has %d access\n", access); + WARN(manager).print(" has {} access\n", access); cavein_possible = config.riskaverse; cavein_candidates.emplace(pos, access); least_access = std::min(access, least_access); @@ -110,13 +110,13 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool dig_now(DFHack::Core::getInstance().getConsole(), pos); } } - DEBUG(manager).print("cavein possible(%d)\n" - "%zu candidates\n" - "least access %d\n", cavein_possible, cavein_candidates.size(), least_access); + DEBUG(manager).print("cavein possible({})\n" + "{} candidates\n" + "least access {}\n", cavein_possible, cavein_candidates.size(), least_access); } // managing designations if (!group.empty()) { - DEBUG(manager).print("managing group #%d\n", groups.debugGIndex(*group.begin())); + DEBUG(manager).print("managing group #{}\n", groups.debugGIndex(*group.begin())); } for (auto &pos: group) { // if no cave-in is possible [or we don't check for], we'll just execute normally and move on @@ -132,7 +132,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool if (CSP::dignow_queue.count(pos) || (cavein_candidates.count(pos) && least_access < MAX && cavein_candidates[pos] <= least_access+OFFSET)) { - TRACE(manager).print("cave-in evaluated true and either of dignow or (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + TRACE(manager).print("cave-in evaluated true and either of dignow or ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); df::coord local(pos); local.x %= 16; local.y %= 16; @@ -143,7 +143,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // we want to let the user keep some designations free of being managed auto b = std::max(0, cavein_candidates[pos] - least_access); auto v = 1000 + (b * 1700); - DEBUG(manager).print("(" COORD ") 1000+1000(%d) -> %d {least-access: %d}\n",COORDARGS(pos), b, v, least_access); + DEBUG(manager).print("({}) 1000+1000({}) -> {} {least-access: {}}\n", pos, b, v, least_access); evT->priority[Coord(local)] = v; } } @@ -152,7 +152,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool } // cavein possible, but we failed to meet the criteria for activation if (cavein_candidates.count(pos)) { - DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as ({} <= {})\n", cavein_candidates[pos], least_access+OFFSET); } else { DEBUG(manager).print("cave-in evaluated true and the position was not a candidate, nor was it set for dignow\n"); } @@ -163,7 +163,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) const { if (Maps::isValidTilePos(map_pos)) { - TRACE(manager).print("manage_one((" COORD "), %d, %d)\n", COORDARGS(map_pos), set_marker_mode, marker_mode); + TRACE(manager).print("manage_one(({}), {}, {})\n", map_pos, set_marker_mode, marker_mode); df::map_block* block = Maps::getTileBlock(map_pos); // we calculate the position inside the block* df::coord local(map_pos); @@ -188,7 +188,7 @@ bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, block->flags.bits.designated = true; } tile_occupancy.bits.dig_marked = marker_mode; - TRACE(manager).print("marker mode %s\n", marker_mode ? "ENABLED" : "DISABLED"); + TRACE(manager).print("marker mode {}\n", marker_mode ? "ENABLED" : "DISABLED"); } else { // if we are though, it should be totally safe to dig tile_occupancy.bits.dig_marked = false; diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 4aa1648453..ac69ffdc8e 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -182,7 +182,7 @@ namespace CSP { psetting.ival(IGNORE_THRESH) = config.ignore_threshold; psetting.ival(FALL_THRESH) = config.fall_threshold; } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } } @@ -208,7 +208,7 @@ namespace CSP { config.refresh_freq = psetting.ival(REFRESH_RATE); config.monitor_freq = psetting.ival(MONITOR_RATE); } catch (std::exception &e) { - ERR(plugin).print("%s\n", e.what()); + ERR(plugin).print("{}\n", e.what()); } } active_workers.clear(); @@ -320,14 +320,14 @@ namespace CSP { dignow_queue.emplace(report->pos); } - DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); } break; case announcement_type::CAVE_COLLAPSE: if (config.resurrect) { - DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), - COORDARGS(report->pos2), report->text.c_str()); + DEBUG(plugin).print("CAVE IN\n{}, pos: {} , pos2: {}\n{}\n", report_id, report->pos, + report->pos2, report->text.c_str()); df::coord below = report->pos; below.z -= 1; @@ -344,12 +344,12 @@ namespace CSP { Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax)); for (auto unit: units) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id); + DEBUG(plugin).print(" [id {}] was near a cave in.\n", unit->id); } for (auto unit : world->units.all) { if (last_safe.count(unit->id)) { endangered_units[unit] = tick; - DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id); + DEBUG(plugin).print(" [id {}] is/was a worker, we'll track them too.\n", unit->id); } } } @@ -472,7 +472,7 @@ namespace CSP { // clean up any "endangered" workers that have been tracked 100 ticks or more for (auto iter = endangered_units.begin(); iter != endangered_units.end();) { if (tick - iter->second >= 1200) { //keep watch 1 day - DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id); + DEBUG(plugin).print("It has been one day since [id {}]'s last incident.\n", iter->first->id); iter = endangered_units.erase(iter); continue; } @@ -525,7 +525,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -582,7 +582,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev command_result channel_safely(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + outs without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -650,23 +650,23 @@ command_result channel_safely(color_ostream &out, std::vector ¶ return DFHack::CR_WRONG_USAGE; } } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); + out.printerr("{}\n", e.what()); return DFHack::CR_FAILURE; } } } else { - out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print("Channel-Safely is {}\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "risk-averse: ", config.riskaverse ? "on." : "off."); - out.print(" %-20s\t%s\n", "monitoring: ", config.monitoring ? "on." : "off."); - out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off."); + out.print(" {:<20}\t{}\n", "risk-averse: ", config.riskaverse ? "on." : "off."); + out.print(" {:<20}\t{}\n", "monitoring: ", config.monitoring ? "on." : "off."); + out.print(" {:<20}\t{}\n", "require-vision: ", config.require_vision ? "on." : "off."); //out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off."); - out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off."); + out.print(" {:<20}\t{}\n", "resurrect: ", config.resurrect ? "on." : "off."); out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq); - out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq); - out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold); - out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold); + out.print(" {:<20}\t{}\n", "refresh-freq: ", config.refresh_freq); + out.print(" {:<20}\t{}\n", "monitor-freq: ", config.monitor_freq); + out.print(" {:<20}\t{}\n", "ignore-threshold: ", config.ignore_threshold); + out.print(" {:<20}\t{}\n", "fall-threshold: ", config.fall_threshold); } CSP::SaveSettings(); return DFHack::CR_OK; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 362fd927a3..16db1ff7d2 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -311,7 +311,7 @@ inline bool dig_now(color_ostream &out, const df::coord &map_pos) { // fully heals the unit specified, resurrecting if need be inline void resurrect(color_ostream &out, const int32_t &unit) { out.color(DFHack::COLOR_RED); - out.print("channel-safely: resurrecting [id: %d]\n", unit); + out.print("channel-safely: resurrecting [id: {}]\n", unit); std::vector params{"-r", "--unit", std::to_string(unit)}; Core::getInstance().runCommand(out,"full-heal", params); } diff --git a/plugins/cleanconst.cpp b/plugins/cleanconst.cpp index b72806cd9b..12fa7f1293 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -2,11 +2,14 @@ // and flags the constructions to recreate their components upon disassembly #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" +#include "Types.h" + #include "modules/Maps.h" -#include "DataDefs.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/world.h" #include "df/construction.h" @@ -38,13 +41,17 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) df::construction *cons = df::construction::find(pos); if (!cons) { - out.printerr("Item at %i,%i,%i marked as construction but no construction is present!\n", pos.x, pos.y, pos.z); + out.printerr("Item at {} marked as construction but no construction is present!\n", pos); continue; } // if the construction is already labeled as "no build item", then leave it alone if (cons->flags.bits.no_build_item) continue; + // Skip reinforced constructions as well + if (cons->flags.bits.reinforced) + continue; + // only destroy the item if the construction claims to be made of the exact same thing if (item->getType() != cons->item_type || item->getSubtype() != cons->item_subtype || @@ -58,7 +65,7 @@ command_result df_cleanconst(color_ostream &out, vector & parameters) cleaned_total++; } - out.print("Done. %d construction items cleaned up.\n", cleaned_total); + out.print("Done. {} construction items cleaned up.\n", cleaned_total); return CR_OK; } diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index 6706bc92d8..74bc40e6ff 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -95,7 +95,7 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud, bool item_spat } if(num_blocks) - out.print("Cleaned %d of %zd map blocks.\n", num_blocks, world->map.map_blocks.size()); + out.print("Cleaned {} of {} map blocks.\n", num_blocks, world->map.map_blocks.size()); return CR_OK; } @@ -123,7 +123,7 @@ command_result cleanitems (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d items.\n", cleaned_total, cleaned_items); + out.print("Removed {} contaminants from {} items.\n", cleaned_total, cleaned_items); return CR_OK; } @@ -143,7 +143,7 @@ command_result cleanunits (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d creatures.\n", cleaned_total, cleaned_units); + out.print("Removed {} contaminants from {} creatures.\n", cleaned_total, cleaned_units); return CR_OK; } @@ -163,7 +163,7 @@ command_result cleanplants (color_ostream &out) } } if (cleaned_total) - out.print("Removed %d contaminants from %d plants.\n", cleaned_total, cleaned_plants); + out.print("Removed {} contaminants from {} plants.\n", cleaned_total, cleaned_plants); return CR_OK; } diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 9e6ddb9849..c8d67d92ad 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -47,7 +47,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) command_result df_cleanowned (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -155,16 +155,16 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) std::string description; item->getItemDescription(&description, 0); out.print( - "[%d] %s (wear level %d)", + "[{}] {} (wear level {})", item->id, - DF2CONSOLE(description).c_str(), + DF2CONSOLE(description), item->getWear() ); df::unit *owner = Items::getOwner(item); if (owner) - out.print(", owner %s", DF2CONSOLE(Units::getReadableName(owner)).c_str()); + out.print(", owner {}", DF2CONSOLE(Units::getReadableName(owner))); if (!dry_run) { diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 1a720c3f4c..703e6a4001 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -16,7 +16,6 @@ #include "df/building.h" #include "df/caste_raw.h" #include "df/creature_raw.h" -#include "df/game_type.h" #include "df/item.h" #include "df/plant_growth.h" #include "df/plant_raw.h" @@ -30,9 +29,7 @@ using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("createitem"); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(gametype); REQUIRE_GLOBAL(cur_year_tick); int dest_container = -1, dest_building = -1; @@ -51,7 +48,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } -bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, +bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_type, int32_t mat_index, int32_t count, bool move_to_cursor = false, bool second_item = false) { // Special logic for making Gloves and Shoes in pairs bool is_gloves = (type == item_type::GLOVES); @@ -67,7 +64,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t bool on_floor = (container == NULL) && (building == NULL) && !move_to_cursor; vector out_items; - if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor)) + if (!Items::createItem(out_items, unit, type, subtype, mat_type, mat_index, !on_floor, count)) return false; for (size_t i = 0; i < out_items.size(); i++) { @@ -84,7 +81,10 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t out_items[i]->moveToGround(building->centerx, building->centery, building->z); } else if (move_to_cursor) - out_items[i]->moveToGround(cursor->x, cursor->y, cursor->z); + { + auto pos = Gui::getCursorPos(); + out_items[i]->moveToGround(pos.x, pos.y, pos.z); + } // else createItem() already put it on the floor at the unit's feet, so we're good // Special logic for creating proper gloves in pairs @@ -103,7 +103,7 @@ bool makeItem(df::unit *unit, df::item_type type, int16_t subtype, int16_t mat_t is_shoes = false; // If we asked for gloves/shoes and only got one (and we're making the first one), make another if ((is_gloves || is_shoes) && !second_item) - return makeItem(unit, type, subtype, mat_type, mat_index, move_to_cursor, true); + return makeItem(unit, type, subtype, mat_type, mat_index, count, move_to_cursor, true); return true; } @@ -142,7 +142,7 @@ static inline bool select_caste_mat(color_ostream &out, vector &tokens, out.printerr("You must also specify a caste.\n"); else out.printerr("The creature you specified has no such caste!\n"); - out.printerr("Valid castes:%s\n", castes.c_str()); + out.printerr("Valid castes:{}\n", castes); return false; } } @@ -196,7 +196,7 @@ static inline bool select_plant_growth(color_ostream &out, vector &token out.printerr("You must also specify a growth ID.\n"); else out.printerr("The plant you specified has no such growth!\n"); - out.printerr("Valid growths:%s\n", growths.c_str()); + out.printerr("Valid growths:{}\n", growths); return false; } } @@ -224,7 +224,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { ItemTypeInfo iinfo(item->getType(), item->getSubtype()); MaterialInfo minfo(item->getMaterial(), item->getMaterialIndex()); - out.print("%s %s\n", iinfo.getToken().c_str(), minfo.getToken().c_str()); + out.print("{} {}\n", iinfo.getToken(), minfo.getToken()); return CR_OK; } else if (parameters[0] == "floor") { @@ -266,7 +266,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_container = item->id; string name; item->getItemDescription(&name, 0); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else if (parameters[0] == "building") { @@ -278,6 +278,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { } switch (building->getType()) { using namespace df::enums::building_type; + case Table: case Coffin: case Furnace: case TradeDepot: @@ -292,8 +293,12 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { case AnimalTrap: case Cage: case Wagon: + case Nest: case NestBox: case Hive: + case Bookcase: + case DisplayFurniture: + case OfferingPlace: break; default: out.printerr("The selected building cannot be used for item storage!\n"); @@ -306,7 +311,7 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { dest_building = building->id; string name; building->getName(&name); - out.print("Items created will be placed inside %s.\n", name.c_str()); + out.print("Items created will be placed inside {}.\n", name); return CR_OK; } else @@ -395,11 +400,12 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { auto unit = Gui::getSelectedUnit(out, true); if (!unit) { - if (*gametype == game_type::ADVENTURE_ARENA || World::isAdventureMode()) + auto pos = Gui::getCursorPos(); + if ( (unit = World::getAdventurer()) ) { // Use the adventurer unit - unit = World::getAdventurer(); + move_to_cursor = pos.isValid(); } - else if (cursor->x >= 0) + else if (pos.isValid()) { // Use the first possible citizen if possible, otherwise the first unit for (auto u : Units::citizensRange(world->units.active)) { unit = u; @@ -431,12 +437,10 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { out.printerr("Previously selected building no longer exists - item will be placed on the floor.\n"); } - for (int i = 0; i < count; i++) { - if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, move_to_cursor, false)) - { - out.printerr("Failed to create item!\n"); - return CR_FAILURE; - } + if (!makeItem(unit, item_type, item_subtype, mat_type, mat_index, count, move_to_cursor, false)) + { + out.printerr("Failed to create item!\n"); + return CR_FAILURE; } return CR_OK; } diff --git a/plugins/cursecheck.cpp b/plugins/cursecheck.cpp index 9c41d73912..f0651889a6 100644 --- a/plugins/cursecheck.cpp +++ b/plugins/cursecheck.cpp @@ -1,8 +1,8 @@ // cursecheck plugin // -// check single tile or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set -// if a cursor is active only the selected tile will be observed -// without cursor the whole map will be checked +// check unit or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set +// if a unit is selected only the selected unit will be observed +// otherwise the whole map will be checked // by default cursed creatures will be only counted // // the tool was intended to help finding vampires but it will also list necromancers, werebeasts and zombies @@ -29,7 +29,6 @@ #include "df/unit_soul.h" #include "df/unit_syndrome.h" #include "df/world.h" -#include "df/world_raws.h" using std::vector; using std::string; @@ -38,7 +37,6 @@ using namespace df::enums; DFHACK_PLUGIN("cursecheck"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); enum curses { None = 0, @@ -92,15 +90,15 @@ curses determineCurse(df::unit * unit) cursetype = curses::Ghost; // zombies: undead or hate life (according to ag), not bloodsuckers - if( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER ) + if( (unit->uwss_add_caste_flag.bits.OPPOSED_TO_LIFE || unit->uwss_add_caste_flag.bits.NOT_LIVING) + && !unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Zombie; // necromancers: alive, don't eat, don't drink, don't age - if(!unit->curse.add_tags1.bits.NOT_LIVING - && unit->curse.add_tags1.bits.NO_EAT - && unit->curse.add_tags1.bits.NO_DRINK - && unit->curse.add_tags2.bits.NO_AGING + if (!unit->uwss_add_caste_flag.bits.NOT_LIVING + && unit->uwss_add_caste_flag.bits.NO_EAT + && unit->uwss_add_caste_flag.bits.NO_DRINK + && unit->uwss_add_property.bits.NO_AGING ) cursetype = curses::Necromancer; @@ -110,21 +108,21 @@ curses determineCurse(df::unit * unit) auto syndrome = df::syndrome::find(active_syndrome->type); if (syndrome) { for (auto classname : syndrome->syn_class) - if (classname && *classname == "WERECURSE") { - cursetype = curses::Werebeast; - break; - } + if (classname && *classname == "WERECURSE") { + cursetype = curses::Werebeast; + break; + } } } // vampires: bloodsucker (obvious enough) - if(unit->curse.add_tags1.bits.BLOODSUCKER) + if (unit->uwss_add_caste_flag.bits.BLOODSUCKER) cursetype = curses::Vampire; return cursetype; } -command_result cursecheck (color_ostream &out, vector & parameters) +command_result cursecheck(color_ostream& out, vector & parameters) { df::unit* selected_unit = Gui::getSelectedUnit(out, true); @@ -135,19 +133,19 @@ command_result cursecheck (color_ostream &out, vector & parameters) bool verbose = false; size_t cursecount = 0; - for(auto parameter : parameters) + for (auto parameter : parameters) { - if(parameter == "help" || parameter == "?") + if (parameter == "help" || parameter == "?") return CR_WRONG_USAGE; - if(parameter == "detail") + if (parameter == "detail") giveDetails = true; - if(parameter == "ids") + if (parameter == "ids") giveUnitID = true; - if(parameter == "nick") + if (parameter == "nick") giveNick = true; - if(parameter == "all") + if (parameter == "all") ignoreDead = false; - if(parameter == "verbose") + if (parameter == "verbose") { // verbose makes no sense without enabling details giveDetails = true; @@ -159,11 +157,11 @@ command_result cursecheck (color_ostream &out, vector & parameters) vector to_check; if (selected_unit) to_check.push_back(selected_unit); - for(df::unit *unit : to_check.size() ? to_check : world->units.all) + for (df::unit* unit : to_check.size() ? to_check : world->units.all) { // filter out all "living" units that are currently removed from play // don't spam all completely dead creatures if not explicitly wanted - if((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) + if ((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead)) { continue; } @@ -172,9 +170,9 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (cursetype != curses::None) { - cursecount++; + cursecount++; - if(giveNick) + if (giveNick) { Units::setNickname(unit, curse_names[cursetype]); //"CURSED"); } @@ -184,10 +182,10 @@ command_result cursecheck (color_ostream &out, vector & parameters) out << DF2CONSOLE(Units::getReadableName(unit)); auto death = df::incident::find(unit->counters.death_id); - out.print(", born in %d, cursed in %d to be a %s. (%s%s)\n", + out.print(", born in {}, cursed in {} to be a {}. ({}{})\n", unit->birth_year, unit->curse_year, - curse_names[cursetype].c_str(), + curse_names[cursetype], // technically most cursed creatures are undead, // therefore output 'active' if they are not completely dead unit->flags2.bits.killed ? "deceased" : "active", @@ -198,22 +196,22 @@ command_result cursecheck (color_ostream &out, vector & parameters) if (verbose) { out << "Curse flags: " - << bitfield_to_string(unit->curse.add_tags1) << std::endl - << bitfield_to_string(unit->curse.add_tags2) << std::endl; + << bitfield_to_string(unit->uwss_add_caste_flag) << std::endl + << bitfield_to_string(unit->uwss_add_property) << std::endl; } } if (giveUnitID) { - out.print("Creature %d, race %d (%x)\n", unit->id, unit->race, unit->race); + out.print("Creature {}, race {} (0x{:x})\n", unit->id, unit->race, unit->race); } } } if (selected_unit && !giveDetails) - out.print("Selected unit is %scursed\n", cursecount == 0 ? "not " : ""); + out.print("Selected unit is {}cursed\n", cursecount == 0 ? "not " : ""); else if (!selected_unit) - out.print("%zd cursed creatures on map\n", cursecount); + out.print("{} cursed creatures on map\n", cursecount); return CR_OK; } diff --git a/plugins/cxxrandom.cpp b/plugins/cxxrandom.cpp index 3f6a405598..a471cda6be 100644 --- a/plugins/cxxrandom.cpp +++ b/plugins/cxxrandom.cpp @@ -140,7 +140,7 @@ class NumberSequence } void Print() { for( auto v : m_numbers ) { - cout->print( "%" PRId64 " ", v ); + cout->print("{} ", v); } } }; diff --git a/plugins/debug.cpp b/plugins/debug.cpp index c3f336afc0..d0e5e0f5a0 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -21,6 +21,7 @@ redistribute it freely, subject to the following restrictions: distribution. */ +#include "Core.h" #include "PluginManager.h" #include "DebugManager.h" #include "Debug.h" @@ -48,30 +49,30 @@ DBG_DECLARE(debug,example,DebugCategory::LINFO); namespace serialization { -template -struct nvp : public std::pair { - using parent_t = std::pair; - nvp(const char* name, T& value) : - parent_t{name, &value} - {} -}; + template + struct nvp : public std::pair { + using parent_t = std::pair; + nvp(const char* name, T& value) : + parent_t{name, &value} + {} + }; -template -nvp make_nvp(const char* name, T& value) { - return {name, value}; -} + template + nvp make_nvp(const char* name, T& value) { + return {name, value}; + } } #define NVP(variable) serialization::make_nvp(#variable, variable) namespace Json { -template -typename std::enable_if::value, ET>::type -get(Json::Value& ar, const std::string &key, const ET& default_) -{ - return static_cast(as(ar.get(key, static_cast(default_)))); -} + template + requires (std::is_enum_v) + ET get(Value& ar, const std::string &key, const ET& default_) + { + return static_cast(as(ar.get(key, static_cast(default_)))); + } } namespace DFHack { namespace debugPlugin { @@ -254,10 +255,10 @@ struct Filter { private: std::regex category_; std::regex plugin_; - DebugCategory::level level_; - size_t matches_; - bool persistent_; - bool enabled_; + DebugCategory::level level_{DebugCategory::level::LTRACE}; + size_t matches_{0}; + bool persistent_{false}; + bool enabled_{false}; std::string categoryText_; std::string pluginText_; }; @@ -352,7 +353,9 @@ struct FilterManager : public std::map //! Current configuration version implemented by the code constexpr static Json::UInt configVersion{1}; //! Path to the configuration file - constexpr static const char* configPath{"dfhack-config/runtime-debug.json"}; + const inline std::filesystem::path getConfigPath() const { + return DFHack::Core::getInstance().getConfigPath() / "runtime-debug.json"; + } //! Get reference to the singleton static FilterManager& getInstance() noexcept @@ -434,8 +437,6 @@ struct FilterManager : public std::map DebugManager::categorySignal_t::Connection connection_; }; -constexpr const char* FilterManager::configPath; - FilterManager::~FilterManager() { } @@ -443,6 +444,7 @@ FilterManager::~FilterManager() command_result FilterManager::loadConfig(DFHack::color_ostream& out) noexcept { nextId_ = 1; + auto configPath = getConfigPath(); if (!Filesystem::isfile(configPath)) return CR_OK; try { @@ -463,6 +465,7 @@ command_result FilterManager::loadConfig(DFHack::color_ostream& out) noexcept command_result FilterManager::saveConfig(DFHack::color_ostream& out) const noexcept { + auto configPath = getConfigPath(); try { DEBUG(command, out) << "Save config to '" << configPath << "'" << std::endl; JsonArchive archive; @@ -804,9 +807,9 @@ static command_result setFilter(color_ostream& out, return v.match(level); }); if (iter == levelNames.end()) { - ERR(command,out).print("level ('%s') parameter must be one of " + ERR(command,out).print("level ('{}') parameter must be one of " "trace, debug, info, warning, error.\n", - parameters[pos].c_str()); + parameters[pos]); return CR_WRONG_USAGE; } @@ -1062,8 +1065,8 @@ CommandDispatch::dispatch_t CommandDispatch::dispatch { static command_result commandDebugFilter(color_ostream& out, std::vector& parameters) { - DEBUG(command,out).print("debugfilter %s, parameter count %zu\n", - parameters.size() > 0 ? parameters[0].c_str() : "", + DEBUG(command,out).print("debugfilter {}, parameter count {}\n", + parameters.size() > 0 ? parameters[0] : "", parameters.size()); auto iter = CommandDispatch::dispatch.end(); if (0u < parameters.size()) @@ -1096,7 +1099,7 @@ DFhackCExport DFHack::command_result plugin_init(DFHack::color_ostream& out, filter.apply(*cat); } } - INFO(init,out).print("plugin_init with %zu commands, %zu filters and %zu categories\n", + INFO(init,out).print("plugin_init with {}, {} filters and {} categories\n", commands.size(), filMan.size(), catMan.size()); filMan.connectTo(catMan.categorySignal); return rv; diff --git a/plugins/deramp.cpp b/plugins/deramp.cpp index 726693b416..6b41701225 100644 --- a/plugins/deramp.cpp +++ b/plugins/deramp.cpp @@ -98,9 +98,9 @@ command_result df_deramp (color_ostream &out, vector & parameters) } } if (count) - out.print("Found and changed %d tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); if (countbad) - out.print("Fixed %d bad down ramps.\n", countbad); + out.print("Fixed {} bad down ramps.\n", countbad); return CR_OK; } diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 210669a75d..c044fa1044 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -23,7 +23,7 @@ dfhack_plugin(tilesieve tilesieve.cpp) # dfhack_plugin(zoom zoom.cpp) if(UNIX) - dfhack_plugin(ref-index ref-index.cpp) + dfhack_plugin(ref-index ref-index.cpp LINK_LIBRARIES lua) endif() add_subdirectory(check-structures-sanity) diff --git a/plugins/devel/buildprobe.cpp b/plugins/devel/buildprobe.cpp index 333f1d3bc4..3b4df00e28 100644 --- a/plugins/devel/buildprobe.cpp +++ b/plugins/devel/buildprobe.cpp @@ -55,7 +55,7 @@ command_result readFlag (color_ostream &out, vector & parameters) MapExtras::MapCache * MCache = new MapExtras::MapCache(); t_occupancy oc = MCache->occupancyAt(cursor); - out.print("Current Value: %d\n", oc.bits.building); + out.print("Current Value: {}\n", ENUM_AS_STR(oc.bits.building)); return CR_OK; } diff --git a/plugins/devel/check-structures-sanity/dispatch.cpp b/plugins/devel/check-structures-sanity/dispatch.cpp index 0c0b3453b4..200db9f326 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -3,6 +3,7 @@ #include #include +#include "df/global_objects.h" #include "df/large_integer.h" Checker::Checker(color_ostream & out) : @@ -118,7 +119,7 @@ void Checker::queue_globals() // offset is the position of the DFHack pointer to this global. auto ptr = *reinterpret_cast(field->offset); - QueueItem item(stl_sprintf("df.global.%s", field->name), ptr); + QueueItem item(fmt::format("df.global.{}", field->name), ptr); CheckedStructure cs(field); if (!ptr) @@ -302,14 +303,12 @@ void Checker::dispatch_primitive(const QueueItem & item, const CheckedStructure FAIL("invalid value for bool: " << int(val)); } } - else if (dynamic_cast(cs.identity)) + else if (auto num_identity = dynamic_cast(cs.identity)) { - check_possible_pointer(item, cs); + if (num_identity->isInteger()) + check_possible_pointer(item, cs); // TODO check ints? - } - else if (dynamic_cast(cs.identity)) - { // TODO check floats? } else @@ -335,7 +334,7 @@ void Checker::dispatch_pointer(const QueueItem & item, const CheckedStructure & } QueueItem target_item(item.path, target_ptr); - auto target = static_cast(cs.identity)->getTarget(); + auto target = static_cast(cs.identity)->getTarget(); if (!target) { check_unknown_pointer(target_item); @@ -886,7 +885,7 @@ void Checker::check_stl_string(const QueueItem & item) else if (is_gcc && length > 0 && !is_valid_dereference(QueueItem(item, "?start?", reinterpret_cast(string->start)), 1)) { // nullptr is NOT okay here - FAIL("invalid string pointer " << stl_sprintf("0x%" PRIxPTR, string->start)); + FAIL("invalid string pointer " << fmt::format("{:#x}", string->start)); return; } else if (is_local && length >= 16) diff --git a/plugins/devel/check-structures-sanity/main.cpp b/plugins/devel/check-structures-sanity/main.cpp index 5bc7c09d6d..6110afe1c0 100644 --- a/plugins/devel/check-structures-sanity/main.cpp +++ b/plugins/devel/check-structures-sanity/main.cpp @@ -77,7 +77,7 @@ static command_result command(color_ostream & out, std::vector & pa } \ catch (std::exception & ex) \ { \ - out.printerr("check-structures-sanity: argument to -%s: %s\n", #name, ex.what()); \ + out.printerr("check-structures-sanity: argument to -{}: {}\n", #name, ex.what()); \ return CR_WRONG_USAGE; \ } \ } diff --git a/plugins/devel/check-structures-sanity/types.cpp b/plugins/devel/check-structures-sanity/types.cpp index 647fa6141e..84257a6ef7 100644 --- a/plugins/devel/check-structures-sanity/types.cpp +++ b/plugins/devel/check-structures-sanity/types.cpp @@ -10,7 +10,7 @@ QueueItem::QueueItem(const QueueItem & parent, const std::string & member, const { } QueueItem::QueueItem(const QueueItem & parent, size_t index, const void *ptr) : - QueueItem(parent.path + stl_sprintf("[%zu]", index), ptr) + QueueItem(parent.path + fmt::format("[{}]", index), ptr) { } @@ -156,24 +156,24 @@ bool CheckedStructure::has_type_at_offset(const CheckedStructure & type, size_t const type_identity *Checker::wrap_in_stl_ptr_vector(const type_identity *base) { - static std::map> wrappers; + static std::map>>> wrappers; auto it = wrappers.find(base); if (it != wrappers.end()) { return it->second.get(); } - return (wrappers[base] = std::make_unique(base, nullptr)).get(); + return (wrappers[base] = std::make_unique>>(base, nullptr)).get(); } const type_identity *Checker::wrap_in_pointer(const type_identity *base) { - static std::map> wrappers; + static std::map> wrappers; auto it = wrappers.find(base); if (it != wrappers.end()) { return it->second.get(); } - return (wrappers[base] = std::make_unique(base)).get(); + return (wrappers[base] = std::make_unique(base)).get(); } std::map> known_types_by_size; diff --git a/plugins/devel/check-structures-sanity/validate.cpp b/plugins/devel/check-structures-sanity/validate.cpp index 24d5327f92..37e739bdb4 100644 --- a/plugins/devel/check-structures-sanity/validate.cpp +++ b/plugins/devel/check-structures-sanity/validate.cpp @@ -1,5 +1,7 @@ #include "check-structures-sanity.h" +#include "df/global_objects.h" + #ifdef WIN32 #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0501 @@ -35,10 +37,10 @@ bool Checker::is_valid_dereference(const QueueItem & item, const CheckedStructur // assumes MALLOC_PERTURB_=45 #ifdef DFHACK64 #define UNINIT_PTR 0xd2d2d2d2d2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%016zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #else #define UNINIT_PTR 0xd2d2d2d2 -#define FAIL_PTR(message) FAIL(stl_sprintf("0x%08zx: ", reinterpret_cast(base)) << message) +#define FAIL_PTR(message) FAIL(fmt::format("{:#016x} ", reinterpret_cast(base)) << message) #endif if (uintptr_t(base) == UNINIT_PTR) { diff --git a/plugins/devel/color-dfhack-text.cpp b/plugins/devel/color-dfhack-text.cpp index a19a475bb3..a17ad9d072 100644 --- a/plugins/devel/color-dfhack-text.cpp +++ b/plugins/devel/color-dfhack-text.cpp @@ -114,7 +114,7 @@ command_result color(color_ostream &out, std::vector ¶ms) } else if (p != "enable") { - out.printerr("Unrecognized option: %s\n", p.c_str()); + out.printerr("Unrecognized option: {}\n", p); return CR_WRONG_USAGE; } } diff --git a/plugins/devel/counters.cpp b/plugins/devel/counters.cpp index d3672d8d48..05c2024acc 100644 --- a/plugins/devel/counters.cpp +++ b/plugins/devel/counters.cpp @@ -22,7 +22,7 @@ command_result df_counters (color_ostream &out, vector & parameters) for (size_t i = 0; i < counters.size(); i++) { auto counter = counters[i]; - out.print("%i (%s): %i\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id).c_str(), counter->value); + out.print("{} ({}) : {}\n", (int)counter->id, ENUM_KEY_STR(misc_trait_type, counter->id), counter->value); } return CR_OK; diff --git a/plugins/devel/dumpmats.cpp b/plugins/devel/dumpmats.cpp index b910b6546b..b7bc7d4350 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -1,16 +1,17 @@ // Dump all hardcoded materials #include "Console.h" +#include "DataDefs.h" #include "Export.h" #include "PluginManager.h" -#include "DataDefs.h" -#include "df/world.h" -#include "df/material.h" #include "df/builtin_mats.h" -#include "df/matter_state.h" #include "df/descriptor_color.h" +#include "df/global_objects.h" #include "df/item_type.h" +#include "df/material.h" +#include "df/matter_state.h" #include "df/strain_type.h" +#include "df/world.h" using std::string; using std::vector; @@ -32,7 +33,7 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) df::material *mat = world->raws.mat_table.builtin[mat_num]; if (!mat) continue; - out.print("\n[MATERIAL:%s] - reconstructed from data extracted from memory\n", mat->id.c_str()); + out.print("\n[MATERIAL:{}] - reconstructed from data extracted from memory\n", mat->id); int32_t def_color[6] = {-1,-1,-1,-1,-1,-1}; string def_name[6]; @@ -52,10 +53,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_color[matter_state::Liquid] = solid_color; def_color[matter_state::Gas] = solid_color; - out.print("\t[STATE_COLOR:ALL:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL:{}]\n", world->raws.descriptors.colors[solid_color]->id); } else - out.print("\t[STATE_COLOR:ALL_SOLID:%s]\n", world->raws.descriptors.colors[solid_color]->id.c_str()); + out.print("\t[STATE_COLOR:ALL_SOLID:{}]\n", world->raws.descriptors.colors[solid_color]->id); } string solid_name = mat->state_name[matter_state::Solid]; @@ -82,10 +83,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) def_name[matter_state::Gas] = solid_name; def_adj[matter_state::Liquid] = solid_name; def_adj[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME_ADJ:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME_ADJ:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME_ADJ:ALL_SOLID:{}]\n", solid_name); } } else @@ -104,10 +105,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_name[matter_state::Liquid] = solid_name; def_name[matter_state::Gas] = solid_name; - out.print("\t[STATE_NAME:ALL:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL:{}]\n", solid_name); } else - out.print("\t[STATE_NAME:ALL_SOLID:%s]\n", solid_name.c_str()); + out.print("\t[STATE_NAME:ALL_SOLID:{}]\n", solid_name); } if (solid_adj == mat->state_adj[matter_state::Powder] || solid_adj == mat->state_adj[matter_state::Paste] || @@ -123,10 +124,10 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) { def_adj[matter_state::Liquid] = solid_adj; def_adj[matter_state::Gas] = solid_adj; - out.print("\t[STATE_ADJ:ALL:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL:{}]\n", solid_adj); } else - out.print("\t[STATE_ADJ:ALL_SOLID:%s]\n", solid_adj.c_str()); + out.print("\t[STATE_ADJ:ALL_SOLID:{}]\n", solid_adj); } } const char *state_names[6] = {"SOLID", "LIQUID", "GAS", "SOLID_POWDER", "SOLID_PASTE", "SOLID_PRESSED"}; @@ -134,110 +135,110 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters) FOR_ENUM_ITEMS(matter_state, state) { if (mat->state_color[state] != -1 && mat->state_color[state] != def_color[state]) - out.print("\t[STATE_COLOR:%s:%s]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id.c_str()); + out.print("\t[STATE_COLOR:{}:{}]\n", state_names[state], world->raws.descriptors.colors[mat->state_color[state]]->id); if (mat->state_name[state] == mat->state_adj[state]) { if ((mat->state_name[state].size() && mat->state_name[state] != def_name[state]) || (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state])) - out.print("\t[STATE_NAME_ADJ:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME_ADJ:{}:{}]\n", state_names[state], mat->state_name[state]); } else { if (mat->state_name[state].size() && mat->state_name[state] != def_name[state]) - out.print("\t[STATE_NAME:%s:%s]\n", state_names[state], mat->state_name[state].c_str()); + out.print("\t[STATE_NAME:{}:{}]\n", state_names[state], mat->state_name[state]); if (mat->state_adj[state].size() && mat->state_adj[state] != def_adj[state]) - out.print("\t[STATE_ADJ:%s:%s]\n", state_names[state], mat->state_adj[state].c_str()); + out.print("\t[STATE_ADJ:{}:{}]\n", state_names[state], mat->state_adj[state]); } } if (mat->basic_color[0] != 7 || mat->basic_color[1] != 0) - out.print("\t[BASIC_COLOR:%i:%i]\n", mat->basic_color[0], mat->basic_color[1]); + out.print("\t[BASIC_COLOR:{}:{}]\n", mat->basic_color[0], mat->basic_color[1]); if (mat->build_color[0] != 7 || mat->build_color[1] != 7 || mat->build_color[2] != 0) - out.print("\t[BUILD_COLOR:%i:%i:%i]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); + out.print("\t[BUILD_COLOR:{}:{}:{}]\n", mat->build_color[0], mat->build_color[1], mat->build_color[2]); if (mat->tile_color[0] != 7 || mat->tile_color[1] != 7 || mat->tile_color[2] != 0) - out.print("\t[TILE_COLOR:%i:%i:%i]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); + out.print("\t[TILE_COLOR:{}:{}:{}]\n", mat->tile_color[0], mat->tile_color[1], mat->tile_color[2]); if (mat->tile != 0xdb) - out.print("\t[TILE:%i]\n", mat->tile); + out.print("\t[TILE:{}]\n", mat->tile); if (mat->item_symbol != 0x07) - out.print("\t[ITEM_SYMBOL:%i]\n", mat->item_symbol); + out.print("\t[ITEM_SYMBOL:{}]\n", mat->item_symbol); if (mat->material_value != 1) - out.print("\t[MATERIAL_VALUE:%i]\n", mat->material_value); + out.print("\t[MATERIAL_VALUE:{}]\n", mat->material_value); if (mat->gem_name1.size()) - out.print("\t[IS_GEM:%s:%s]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); + out.print("\t[IS_GEM:{}:{}]\n", mat->gem_name1.c_str(), mat->gem_name2.c_str()); if (mat->stone_name.size()) - out.print("\t[STONE_NAME:%s]\n", mat->stone_name.c_str()); + out.print("\t[STONE_NAME:{}]\n", mat->stone_name.c_str()); if (mat->heat.spec_heat != 60001) - out.print("\t[SPEC_HEAT:%i]\n", mat->heat.spec_heat); + out.print("\t[SPEC_HEAT:{}]\n", mat->heat.spec_heat); if (mat->heat.heatdam_point != 60001) - out.print("\t[HEATDAM_POINT:%i]\n", mat->heat.heatdam_point); + out.print("\t[HEATDAM_POINT:{}]\n", mat->heat.heatdam_point); if (mat->heat.colddam_point != 60001) - out.print("\t[COLDDAM_POINT:%i]\n", mat->heat.colddam_point); + out.print("\t[COLDDAM_POINT:{}]\n", mat->heat.colddam_point); if (mat->heat.ignite_point != 60001) - out.print("\t[IGNITE_POINT:%i]\n", mat->heat.ignite_point); + out.print("\t[IGNITE_POINT:{}]\n", mat->heat.ignite_point); if (mat->heat.melting_point != 60001) - out.print("\t[MELTING_POINT:%i]\n", mat->heat.melting_point); + out.print("\t[MELTING_POINT:{}]\n", mat->heat.melting_point); if (mat->heat.boiling_point != 60001) - out.print("\t[BOILING_POINT:%i]\n", mat->heat.boiling_point); + out.print("\t[BOILING_POINT:{}]\n", mat->heat.boiling_point); if (mat->heat.mat_fixed_temp != 60001) - out.print("\t[MAT_FIXED_TEMP:%i]\n", mat->heat.mat_fixed_temp); + out.print("\t[MAT_FIXED_TEMP:{}]\n", mat->heat.mat_fixed_temp); if (uint32_t(mat->solid_density) != 0xFBBC7818) - out.print("\t[SOLID_DENSITY:%i]\n", mat->solid_density); + out.print("\t[SOLID_DENSITY:{}]\n", mat->solid_density); if (uint32_t(mat->liquid_density) != 0xFBBC7818) - out.print("\t[LIQUID_DENSITY:%i]\n", mat->liquid_density); + out.print("\t[LIQUID_DENSITY:{}]\n", mat->liquid_density); if (uint32_t(mat->molar_mass) != 0xFBBC7818) - out.print("\t[MOLAR_MASS:%i]\n", mat->molar_mass); + out.print("\t[MOLAR_MASS:{}]\n", mat->molar_mass); FOR_ENUM_ITEMS(strain_type, strain) { auto name = ENUM_KEY_STR(strain_type,strain); if (mat->strength.yield[strain] != 10000) - out.print("\t[%s_YIELD:%i]\n", name.c_str(), mat->strength.yield[strain]); + out.print("\t[{}_YIELD:{}]\n", name, mat->strength.yield[strain]); if (mat->strength.fracture[strain] != 10000) - out.print("\t[%s_FRACTURE:%i]\n", name.c_str(), mat->strength.fracture[strain]); + out.print("\t[{}_FRACTURE:{}]\n", name, mat->strength.fracture[strain]); if (mat->strength.strain_at_yield[strain] != 0) - out.print("\t[%s_STRAIN_AT_YIELD:%i]\n", name.c_str(), mat->strength.strain_at_yield[strain]); + out.print("\t[{}_STRAIN_AT_YIELD:{}]\n", name, mat->strength.strain_at_yield[strain]); } if (mat->strength.max_edge != 0) - out.print("\t[MAX_EDGE:%i]\n", mat->strength.max_edge); + out.print("\t[MAX_EDGE:{}]\n", mat->strength.max_edge); if (mat->strength.absorption != 0) - out.print("\t[ABSORPTION:%i]\n", mat->strength.absorption); + out.print("\t[ABSORPTION:{}]\n", mat->strength.absorption); FOR_ENUM_ITEMS(material_flags, i) { if (mat->flags.is_set(i)) - out.print("\t[%s]\n", ENUM_KEY_STR(material_flags, i).c_str()); + out.print("\t[{}]\n", ENUM_KEY_STR(material_flags, i)); } if (mat->extract_storage != item_type::BARREL) - out.print("\t[EXTRACT_STORAGE:%s]\n", ENUM_KEY_STR(item_type, mat->extract_storage).c_str()); + out.print("\t[EXTRACT_STORAGE:{}]\n", ENUM_KEY_STR(item_type, mat->extract_storage)); if (mat->butcher_special_type != item_type::NONE || mat->butcher_special_subtype != -1) - out.print("\t[BUTCHER_SPECIAL:%s:%s]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type).c_str(), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); + out.print("\t[BUTCHER_SPECIAL:{}:{}]\n", ENUM_KEY_STR(item_type, mat->butcher_special_type), (mat->butcher_special_subtype == -1) ? "NONE" : "?"); if (mat->meat_name[0].size() || mat->meat_name[1].size() || mat->meat_name[2].size()) - out.print("\t[MEAT_NAME:%s:%s:%s]\n", mat->meat_name[0].c_str(), mat->meat_name[1].c_str(), mat->meat_name[2].c_str()); + out.print("\t[MEAT_NAME:{}:{}:{}]\n", mat->meat_name[0], mat->meat_name[1], mat->meat_name[2]); if (mat->block_name[0].size() || mat->block_name[1].size()) - out.print("\t[BLOCK_NAME:%s:%s]\n", mat->block_name[0].c_str(), mat->block_name[1].c_str()); + out.print("\t[BLOCK_NAME:{}:{}]\n", mat->block_name[0], mat->block_name[1]); for (std::string *s : mat->reaction_class) - out.print("\t[REACTION_CLASS:%s]\n", s->c_str()); + out.print("\t[REACTION_CLASS:{}]\n", *s); for (size_t i = 0; i < mat->reaction_product.id.size(); i++) { if ((*mat->reaction_product.str[0][i] == "NONE") && (*mat->reaction_product.str[1][i] == "NONE")) - out.print("\t[MATERIAL_REACTION_PRODUCT:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[MATERIAL_REACTION_PRODUCT:{}:{}:{}{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], mat->reaction_product.str[4][i]->size() ? ":" : "", *mat->reaction_product.str[4][i]); else - out.print("\t[ITEM_REACTION_PRODUCT:%s:%s:%s:%s:%s%s%s]\n", mat->reaction_product.id[i]->c_str(), mat->reaction_product.str[0][i]->c_str(), mat->reaction_product.str[1][i]->c_str(), mat->reaction_product.str[2][i]->c_str(), mat->reaction_product.str[3][i]->c_str(), mat->reaction_product.str[4][i]->size() ? ":" : "", mat->reaction_product.str[4][i]->c_str()); + out.print("\t[ITEM_REACTION_PRODUCT:{}:{}:{}:{}:{}{}]\n", *mat->reaction_product.id[i], *mat->reaction_product.str[0][i], *mat->reaction_product.str[1][i], *mat->reaction_product.str[2][i], *mat->reaction_product.str[3][i], *mat->reaction_product.str[4][i]); } if (mat->hardens_with_water.mat_type != -1) - out.print("\t[HARDENS_WITH_WATER:%s:%s%s%s]\n", mat->hardens_with_water.str[0].c_str(), mat->hardens_with_water.str[1].c_str(), mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2].c_str()); + out.print("\t[HARDENS_WITH_WATER:{}:{}{}{}]\n", mat->hardens_with_water.str[0], mat->hardens_with_water.str[1], mat->hardens_with_water.str[2].size() ? ":" : "", mat->hardens_with_water.str[2]); if (mat->powder_dye != -1) - out.print("\t[POWDER_DYE:%s]\n", world->raws.descriptors.colors[mat->powder_dye]->id.c_str()); + out.print("\t[POWDER_DYE:{}]\n", world->raws.descriptors.colors[mat->powder_dye]->id); if (mat->soap_level != -0) - out.print("\t[SOAP_LEVEL:%o]\n", mat->soap_level); + out.print("\t[SOAP_LEVEL:{}]\n", mat->soap_level); for (size_t i = 0; i < mat->syndrome.syndrome.size(); i++) out.print("\t[SYNDROME] ...\n"); diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index ceaec29921..c9715a6875 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -1,4 +1,7 @@ +#include +#include + #include "PluginManager.h" #include "DataDefs.h" @@ -8,16 +11,15 @@ #include "df/caste_body_info.h" #include "df/construction.h" #include "df/coord.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/item_actual.h" #include "df/job.h" #include "df/unit.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" -#include -#include - using namespace DFHack; using namespace std; @@ -108,7 +110,7 @@ command_result eventExample(color_ostream& out, vector& parameters) { //static int timerCount=0; //static int timerDenom=0; void jobInitiated(color_ostream& out, void* job_) { - out.print("Job initiated! %p\n", job_); + out.print("Job initiated! {}\n", static_cast(job_)); /* df::job* job = (df::job*)job_; out.print(" completion_timer = %d\n", job->completion_timer); @@ -119,37 +121,37 @@ void jobInitiated(color_ostream& out, void* job_) { } void jobCompleted(color_ostream& out, void* job) { - out.print("Job completed! %p\n", job); + out.print("Job completed! {}\n", static_cast(job)); } void timePassed(color_ostream& out, void* ptr) { - out.print("Time: %zi\n", (intptr_t)(ptr)); + out.print("Time: {}\n", (intptr_t)(ptr)); } void unitDeath(color_ostream& out, void* ptr) { - out.print("Death: %zi\n", (intptr_t)(ptr)); + out.print("Death: {}\n", (intptr_t)(ptr)); } void itemCreate(color_ostream& out, void* ptr) { int32_t item_index = df::item::binsearch_index(df::global::world->items.all, (intptr_t)ptr); if ( item_index == -1 ) { - out.print("%s, %d: Error.\n", __FILE__, __LINE__); + out.print("{}: Error.\n", __FILE__, __LINE__); } df::item* item = df::global::world->items.all[item_index]; df::item_type type = item->getType(); df::coord pos = item->pos; - out.print("Item created: %zi, %s, at (%d,%d,%d)\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); + out.print("Item created: {}, {}, at ({},{},{})\n", (intptr_t)(ptr), ENUM_KEY_STR(item_type, type).c_str(), pos.x, pos.y, pos.z); } void building(color_ostream& out, void* ptr) { - out.print("Building created/destroyed: %zi\n", (intptr_t)ptr); + out.print("Building created/destroyed: {}\n", (intptr_t)ptr); } void construction(color_ostream& out, void* ptr) { - out.print("Construction created/destroyed: %p\n", ptr); + out.print("Construction created/destroyed: {}\n", ptr); df::construction* constr = (df::construction*)ptr; df::coord pos = constr->pos; - out.print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + out.print(" ({},{},{})\n", pos.x, pos.y, pos.z); if ( df::construction::find(pos) == NULL ) out.print(" construction destroyed\n"); else @@ -159,19 +161,19 @@ void construction(color_ostream& out, void* ptr) { void syndrome(color_ostream& out, void* ptr) { EventManager::SyndromeData* data = (EventManager::SyndromeData*)ptr; - out.print("Syndrome started: unit %d, syndrome %d.\n", data->unitId, data->syndromeIndex); + out.print("Syndrome started: unit {}, syndrome {}.\n", data->unitId, data->syndromeIndex); } void invasion(color_ostream& out, void* ptr) { - out.print("New invasion! %zi\n", (intptr_t)ptr); + out.print("New invasion! {}\n", (intptr_t)ptr); } void unitAttack(color_ostream& out, void* ptr) { EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr; - out.print("unit %d attacks unit %d\n", data->attacker, data->defender); + out.print("unit {} attacks unit {}\n", data->attacker, data->defender); df::unit* defender = df::unit::find(data->defender); if (!defender) { - out.printerr("defender %d does not exist\n", data->defender); + out.printerr("defender {} does not exist\n", data->defender); return; } int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound); @@ -186,6 +188,6 @@ void unitAttack(color_ostream& out, void* ptr) { for ( auto a = parts.begin(); a != parts.end(); a++ ) { int32_t body_part_id = (*a); df::body_part_raw* part = defender->body.body_plan->body_parts[body_part_id]; - out.print(" %s\n", part->name_singular[0]->c_str()); + out.print(" {}\n", *part->name_singular[0]); } } diff --git a/plugins/devel/frozen.cpp b/plugins/devel/frozen.cpp index 6c4264783e..e97a7ebf0a 100644 --- a/plugins/devel/frozen.cpp +++ b/plugins/devel/frozen.cpp @@ -6,6 +6,7 @@ #include "modules/Maps.h" #include "df/block_square_event_frozen_liquidst.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" @@ -55,7 +56,7 @@ command_result df_frozenlava (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Magma); if (tiles) - out.print("Changed %i tiles of ice into frozen lava.\n", tiles); + out.print("Changed {} tiles of ice into frozen lava.\n", tiles); return CR_OK; } @@ -72,7 +73,7 @@ command_result df_frozenwater (color_ostream &out, vector & parameters) int tiles = changeLiquid(tile_liquid::Water); if (tiles) - out.print("Changed %i tiles of ice into frozen water.\n", tiles); + out.print("Changed {} tiles of ice into frozen water.\n", tiles); return CR_OK; } diff --git a/plugins/devel/kittens.cpp b/plugins/devel/kittens.cpp index 69430e69da..c328492fb5 100644 --- a/plugins/devel/kittens.cpp +++ b/plugins/devel/kittens.cpp @@ -53,6 +53,7 @@ uint64_t timeLast = 0; command_result kittens (color_ostream &out, vector & parameters); command_result ktimer (color_ostream &out, vector & parameters); +command_result throwtest (color_ostream &out, vector & parameters); command_result trackmenu (color_ostream &out, vector & parameters); command_result trackpos (color_ostream &out, vector & parameters); command_result trackstate (color_ostream &out, vector & parameters); @@ -68,6 +69,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector main.mode) { last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); } } if(trackpos_flg) @@ -134,14 +136,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) last_designation[0] = desig_x; last_designation[1] = desig_y; last_designation[2] = desig_z; - out.print("Designation: %d %d %d\n",desig_x, desig_y, desig_z); + out.print("Designation: {} {} {}\n", desig_x, desig_y, desig_z); } df::coord mousePos = Gui::getMousePos(); if(mousePos.x != last_mouse[0] || mousePos.y != last_mouse[1]) { last_mouse[0] = mousePos.x; last_mouse[1] = mousePos.y; - out.print("Mouse: %d %d\n",mousePos.x, mousePos.y); + out.print("Mouse: {} {}\n", mousePos.x, mousePos.y); } } return CR_OK; @@ -158,7 +160,7 @@ command_result trackmenu (color_ostream &out, vector & parameters) { is_enabled = true; last_menu = plotinfo->main.mode; - out.print("Menu: %d\n",last_menu); + out.print("Menu: {}\n", ENUM_AS_STR(last_menu)); trackmenu_flg = true; return CR_OK; } @@ -182,10 +184,10 @@ command_result colormods (color_ostream &out, vector & parameters) for(df::creature_raw* rawlion : vec) { df::caste_raw * caste = rawlion->caste[0]; - out.print("%s\nCaste addr %p\n",rawlion->creature_id.c_str(), &caste->color_modifiers); + out.print("{}\nCaste addr {}\n",rawlion->creature_id, static_cast(&caste->color_modifiers)); for(size_t j = 0; j < caste->color_modifiers.size();j++) { - out.print("mod %zd: %p\n", j, caste->color_modifiers[j]); + out.print("mod {}: {}\n", j, static_cast(caste->color_modifiers[j])); } } return CR_OK; @@ -203,7 +205,7 @@ command_result ktimer (color_ostream &out, vector & parameters) uint64_t timeend = GetTimeMs64(); timeLast = timeend; timering = true; - out.print("Time to suspend = %d ms\n", int(timeend - timestart)); + out.print("Time to suspend = {} ms\n", int(timeend - timestart)); } is_enabled = true; return CR_OK; @@ -300,9 +302,9 @@ struct Connected : public ClearMem { return this; } ~Connected() { - INFO(command,*out).print("Connected %d had %d count. " - "It was caller %d times. " - "It was callee %d times.\n", + INFO(command,*out).print("Connected {} had {} count. " + "It was caller {} times. " + "It was callee {} times.\n", id, count, caller, callee.load()); } }; @@ -377,6 +379,13 @@ command_result sharedsignal (color_ostream &out, vector & parameters) return CR_OK; } +command_result throwtest (color_ostream &out, vector & parameters) +{ + if (!parameters.empty() && parameters[0] == "nonstd") + throw 42; + throw std::runtime_error("throwtest: test exception"); +} + command_result kittens (color_ostream &out, vector & parameters) { if (parameters.size() >= 1) diff --git a/plugins/devel/memutils.cpp b/plugins/devel/memutils.cpp index c2a7387fdd..b7ce98ea73 100644 --- a/plugins/devel/memutils.cpp +++ b/plugins/devel/memutils.cpp @@ -52,7 +52,7 @@ namespace memutils { lua_pushstring(state, expr); if (!Lua::SafeCall(*out, state, 1, 1)) { - out->printerr("Failed to evaluate %s\n", expr); + out->printerr("Failed to evaluate {}\n", expr); return NULL; } @@ -60,7 +60,7 @@ namespace memutils { lua_swap(state); if (!Lua::SafeCall(*out, state, 1, 1) || !lua_isinteger(state, -1)) { - out->printerr("Failed to get address: %s\n", expr); + out->printerr("Failed to get address: {}\n", expr); return NULL; } diff --git a/plugins/devel/memview.cpp b/plugins/devel/memview.cpp index a9fba897e1..01397ef7d2 100644 --- a/plugins/devel/memview.cpp +++ b/plugins/devel/memview.cpp @@ -75,7 +75,7 @@ void outputHex(uint8_t *buf,uint8_t *lbuf,size_t len,size_t start,color_ostream for(size_t i=0;i8X} ",i+start); for(size_t j=0;(j2X}",static_cast(buf[j+i])); //if modfied show a star else - con.print(" %02X",buf[j+i]); + con.print(" {:0>2X}",static_cast(buf[j+i])); } con.reset_color(); con.print(" | "); for(size_t j=0;(j31)&&(buf[j+i]<128)) //only printable ascii - con.print("%c",buf[j+i]); + con.print("{}",static_cast(buf[j+i])); else con.print("."); con.print("\n"); @@ -187,7 +187,7 @@ command_result memview (color_ostream &out, vector & parameters) isValid=true; if(!isValid) { - out.printerr("Invalid address: %p\n",memdata.addr); + out.printerr("Invalid address: {}\n",memdata.addr); mymutex->unlock(); return CR_OK; } diff --git a/plugins/devel/onceExample.cpp b/plugins/devel/onceExample.cpp index ebc03e32b7..73361c53e7 100644 --- a/plugins/devel/onceExample.cpp +++ b/plugins/devel/onceExample.cpp @@ -25,7 +25,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector & parameters) { - out.print("Already done = %d.\n", DFHack::Once::alreadyDone("onceExample_1")); + out.print("Already done = {}.\n", DFHack::Once::alreadyDone("onceExample_1")); if ( DFHack::Once::doOnce("onceExample_1") ) { out.print("Printing this message once!\n"); } diff --git a/plugins/devel/stepBetween.cpp b/plugins/devel/stepBetween.cpp index 0eca4ea8c9..9acbce904b 100644 --- a/plugins/devel/stepBetween.cpp +++ b/plugins/devel/stepBetween.cpp @@ -79,7 +79,7 @@ df::coord prev; command_result stepBetween (color_ostream &out, std::vector & parameters) { df::coord bob = Gui::getCursorPos(); - out.print("(%d,%d,%d), (%d,%d,%d): canStepBetween = %d, canWalkBetween = %d\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); + out.print("({},{},{}), ({},{},{}): canStepBetween = {}, canWalkBetween = {}\n", prev.x, prev.y, prev.z, bob.x, bob.y, bob.z, Maps::canStepBetween(prev, bob), Maps::canWalkBetween(prev,bob)); prev = bob; return CR_OK; diff --git a/plugins/devel/stockcheck.cpp b/plugins/devel/stockcheck.cpp index acd2b512f1..1e8a55d1a0 100644 --- a/plugins/devel/stockcheck.cpp +++ b/plugins/devel/stockcheck.cpp @@ -79,7 +79,7 @@ struct StockpileInfo { DFCoord cursor (x,y,z); uint32_t tileX = x % 16; uint32_t tileY = y % 16; - MapExtras::Block * b = mc.BlockAt(cursor/16); + MapExtras::Block * b = mc.BlockAtTile(cursor); if(b && b->is_valid()) { auto &block = *b->getRaw(); diff --git a/plugins/devel/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc30..bad22f3e58 100644 --- a/plugins/devel/stripcaged.cpp +++ b/plugins/devel/stripcaged.cpp @@ -21,7 +21,6 @@ using namespace std; #include #include "df/world.h" -#include "df/world_raws.h" #include "df/building_def.h" #include "df/unit_inventory_item.h" #include @@ -35,7 +34,6 @@ using std::string; using namespace DFHack; using namespace df::enums; using df::global::world; -using df::global::cursor; using df::global::plotinfo; using namespace DFHack::Gui; diff --git a/plugins/devel/tiles.cpp b/plugins/devel/tiles.cpp index e087962876..85023fc56e 100644 --- a/plugins/devel/tiles.cpp +++ b/plugins/devel/tiles.cpp @@ -111,8 +111,8 @@ class BlockBrush : public Brush coord_vec points(MapCache & mc, DFHack::DFCoord start) { coord_vec v; - DFHack::DFCoord blockc = start % 16; - DFHack::DFCoord iterc = blockc * 16; + DFHack::DFCoord blockc = Maps::getTileBlockCoord(start); + DFHack::DFCoord iterc = Maps::getBlockOrigin(blockc); if( !mc.testCoord(start) ) return v; @@ -445,7 +445,7 @@ command_result df_tiles (Core * c, vector & parameters) mcache.setTemp2At(*iter,10015); mcache.setDesignationAt(*iter,a); - Block * b = mcache.BlockAt((*iter)/16); + Block * b = mcache.BlockAtTile(*iter); DFHack::t_blockflags bf = b->BlockFlags(); bf.bits.liquid_1 = true; bf.bits.liquid_2 = true; @@ -506,7 +506,7 @@ command_result df_tiles (Core * c, vector & parameters) } mcache.setDesignationAt(current,des); } - seen_blocks.insert(mcache.BlockAt((*iter) / 16)); + seen_blocks.insert(mcache.BlockAtTile(*iter)); iter++; } set ::iterator biter = seen_blocks.begin(); diff --git a/plugins/devel/tilesieve.cpp b/plugins/devel/tilesieve.cpp index 6b36b8dd0d..2360a0574a 100644 --- a/plugins/devel/tilesieve.cpp +++ b/plugins/devel/tilesieve.cpp @@ -1,16 +1,18 @@ // This is a generic plugin that does nothing useful apart from acting as an example... of a plugin that does nothing :D -// some headers required for a plugin. Nothing special, just the basics. -#include -#include -#include #include -// DF data structure definition headers + +#include "Console.h" #include "DataDefs.h" +#include "Export.h" +#include "PluginManager.h" +#include "TileTypes.h" + #include "modules/Maps.h" + +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" -#include "TileTypes.h" using namespace DFHack; using namespace df::enums; @@ -73,7 +75,7 @@ command_result tilesieve(color_ostream &out, std::vector & params) if(seen.count(tt)) continue; seen.insert(tt); - out.print("Found tile %x @ %d %d %d\n", tt, block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); + out.print("Found tile {} @ {} {} {}\n", ENUM_AS_STR(tt), block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); } } return CR_OK; diff --git a/plugins/devel/vectors.cpp b/plugins/devel/vectors.cpp index 279fe8a0d5..ac72de3074 100644 --- a/plugins/devel/vectors.cpp +++ b/plugins/devel/vectors.cpp @@ -134,7 +134,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, uintptr_t length = (intptr_t)vec->end - (intptr_t)vec->start; uintptr_t offset = pos - start; - con.print("%8s offset 0x%06zx, addr 0x%01zx, start 0x%01zx, length %zi", + con.print("{:8} offset 0x{:06x}, addr 0x{:01x}, start 0x{:01x}, length {:}", msg, offset, pos, intptr_t(vec->start), length); if (length >= 4 && length % 4 == 0) { @@ -146,7 +146,7 @@ static void printVec(color_ostream &con, const char* msg, t_vecTriplet *vec, } std::string classname; if (Core::getInstance().vinfo->getVTableName(ptr, classname)) - con.print(", 1st item: %s", classname.c_str()); + con.print(", 1st item: {}", classname); } con.print("\n"); } @@ -197,10 +197,10 @@ command_result df_vectors (color_ostream &con, vector & parameters) // Found the range containing the start if (!range.isInRange((void *)end)) { - con.print("Scanning %zi bytes would read past end of memory " + con.print("Scanning {} bytes would read past end of memory " "range.\n", bytes); size_t diff = end - (intptr_t)range.end; - con.print("Cutting bytes down by %zi.\n", diff); + con.print("Cutting bytes down by {}.\n", diff); end = (uintptr_t) range.end; } diff --git a/plugins/devel/zoom.cpp b/plugins/devel/zoom.cpp index 18e8d62e32..d37ed3418a 100644 --- a/plugins/devel/zoom.cpp +++ b/plugins/devel/zoom.cpp @@ -42,7 +42,7 @@ command_result df_zoom (color_ostream &out, std::vector & paramete return CR_WRONG_USAGE; if (zcmap.find(parameters[0]) == zcmap.end()) { - out.printerr("Unrecognized zoom command: %s\n", parameters[0].c_str()); + out.printerr("Unrecognized zoom command: {}\n", parameters[0]); out.print("Valid commands:"); for (auto it = zcmap.begin(); it != zcmap.end(); ++it) { diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index c3c79122fc..2bb2ea6fee 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -20,9 +20,11 @@ #include "modules/World.h" #include "df/building.h" +#include "df/builtin_mats.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/map_block.h" +#include "df/material.h" #include "df/plotinfost.h" #include "df/reaction_product_itemst.h" #include "df/tile_designation.h" @@ -46,9 +48,6 @@ namespace DFHack { DBG_DECLARE(dignow, channels, DebugCategory::LINFO); } -#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16 -#define COORDARGS(id) id.x, id.y, id.z - using namespace DFHack; struct designation{ @@ -201,47 +200,15 @@ struct dig_now_options { static struct_identity _identity; }; static const struct_field_info dig_now_options_fields[] = { - { struct_field_info::PRIMITIVE, "help", offsetof(dig_now_options, help), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "start", offsetof(dig_now_options, start), &df::coord::_identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "end", offsetof(dig_now_options, end), &df::coord::_identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "boulder_percents", offsetof(dig_now_options, boulder_percents), &boulder_percent_options::_identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "dump_pos", offsetof(dig_now_options, dump_pos), &df::coord::_identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "help", offsetof(dig_now_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "start", offsetof(dig_now_options, start), df::identity_traits::get(), 0, 0 }, + { struct_field_info::SUBSTRUCT, "end", offsetof(dig_now_options, end), df::identity_traits::get(), 0, 0 }, + { struct_field_info::SUBSTRUCT, "boulder_percents", offsetof(dig_now_options, boulder_percents), &boulder_percent_options::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "dump_pos", offsetof(dig_now_options, dump_pos), df::identity_traits::get(), 0, 0 }, { struct_field_info::END } }; struct_identity dig_now_options::_identity(sizeof(dig_now_options), &df::allocator_fn, NULL, "dig_now_options", NULL, dig_now_options_fields); -// propagate light, outside, and subterranean flags to open tiles below this one -static void propagate_vertical_flags(MapExtras::MapCache &map, - const DFCoord &pos) { - df::tile_designation td = map.designationAt(pos); - - if (!map.ensureBlockAt(DFCoord(pos.x, pos.y, pos.z+1))) { - // only the sky above - td.bits.light = true; - td.bits.outside = true; - td.bits.subterranean = false; - } - - int32_t zlevel = pos.z; - df::tiletype_shape shape = - tileShape(map.tiletypeAt(DFCoord(pos.x, pos.y, zlevel))); - while ((shape == df::tiletype_shape::EMPTY - || shape == df::tiletype_shape::RAMP_TOP) - && map.ensureBlockAt(DFCoord(pos.x, pos.y, --zlevel))) { - DFCoord pos_below(pos.x, pos.y, zlevel); - df::tile_designation td_below = map.designationAt(pos_below); - if (td_below.bits.light == td.bits.light - && td_below.bits.outside == td.bits.outside - && td_below.bits.subterranean == td.bits.subterranean) - break; - td_below.bits.light = td.bits.light; - td_below.bits.outside = td.bits.outside; - td_below.bits.subterranean = td.bits.subterranean; - map.setDesignationAt(pos_below, td_below); - shape = tileShape(map.tiletypeAt(pos_below)); - } -} - static bool can_dig_default(df::tiletype tt) { df::tiletype_shape shape = tileShape(tt); return shape == df::tiletype_shape::WALL || @@ -391,7 +358,7 @@ struct dug_tile_info { DFCoord pos; df::tiletype_material tmat; df::item_type itype; - int32_t imat; // mat idx of boulder/gem potentially generated at this pos + t_matpair imat; // matpair of boulder/gem potentially generated at this pos dug_tile_info(MapExtras::MapCache &map, const DFCoord &pos) { this->pos = pos; @@ -403,32 +370,32 @@ struct dug_tile_info { imat = -1; df::tiletype_shape shape = tileShape(tt); - if (shape == df::tiletype_shape::WALL || shape == df::tiletype_shape::FORTIFICATION) { - switch (tmat) { - case df::tiletype_material::STONE: - case df::tiletype_material::MINERAL: - case df::tiletype_material::FEATURE: - imat = map.baseMaterialAt(pos).mat_index; - break; - case df::tiletype_material::LAVA_STONE: - { - MaterialInfo mi; - if (mi.findInorganic("OBSIDIAN")) - imat = mi.index; - return; // itype should always be BOULDER, regardless of vein - } - default: - break; - } - } - - switch (map.BlockAtTile(pos)->veinTypeAt(pos)) { - case df::inclusion_type::CLUSTER_ONE: - case df::inclusion_type::CLUSTER_SMALL: - itype = df::item_type::ROUGH; + if (shape != df::tiletype_shape::WALL && shape != df::tiletype_shape::FORTIFICATION) + return; + + switch (tmat) { + case df::tiletype_material::STONE: + case df::tiletype_material::MINERAL: + case df::tiletype_material::FEATURE: + case df::tiletype_material::LAVA_STONE: + imat = map.baseMaterialAt(pos); break; - default: + case df::tiletype_material::FROZEN_LIQUID: + // assume frozen water + // we can't use baseMaterialAt here because it will return the underlying river bed material + imat = t_matpair(df::builtin_mats::WATER, -1); break; + default: + return; + } + + MaterialInfo mi; + mi.decode(imat); + if (mi.type == -1 || !mi.material) + return; + + if (mi.material->isGem()) { + itype = df::item_type::ROUGH; } } }; @@ -448,12 +415,10 @@ static bool is_diggable(MapExtras::MapCache &map, const DFCoord &pos, break; } - if (mat == df::tiletype_material::FEATURE) { - // adamantine is the only is diggable feature - t_feature feature; - return map.BlockAtTile(pos)->GetLocalFeature(&feature) - && feature.type == feature_type::deep_special_tube; - } + MaterialInfo mi; + mi.decode(map.baseMaterialAt(pos)); + if (mi.material != nullptr && mi.material->flags.is_set(df::material_flags::UNDIGGABLE)) + return false; return true; } @@ -486,7 +451,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, DFCoord pos_below(pos.x, pos.y, pos.z-1); if (can_dig_channel(tt) && map.ensureBlockAt(pos_below) && is_diggable(map, pos_below, map.tiletypeAt(pos_below))) { - TRACE(channels).print("dig_tile: channeling at (" COORD ") [can_dig_channel: true]\n",COORDARGS(pos_below)); + TRACE(channels).print("dig_tile: channeling at ({}) [can_dig_channel: true]\n", pos_below); target_type = df::tiletype::OpenSpace; DFCoord pos_above(pos.x, pos.y, pos.z+1); if (map.ensureBlockAt(pos_above)) { @@ -499,11 +464,11 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, dig_tile(out, map, pos_below, td_below, dug_tiles); } clean_ramps(map, pos); - propagate_vertical_flags(map, pos); + map.propagateVerticalFlags(pos); return true; } } else { - DEBUG(channels).print("dig_tile: failed to channel at (" COORD ") [can_dig_channel: false]\n", COORDARGS(pos_below)); + DEBUG(channels).print("dig_tile: failed to channel at ({}) [can_dig_channel: false]\n", pos_below); } break; } @@ -542,7 +507,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, map.setTiletypeAt(pos_above, get_target_type(tt, df::tiletype_shape::RAMP_TOP)); remove_ramp_top(map, DFCoord(pos.x, pos.y, pos.z+2)); - propagate_vertical_flags(map, DFCoord(pos.x, pos.y, pos.z + 1)); + map.propagateVerticalFlags(DFCoord(pos.x, pos.y, pos.z + 1)); } } break; @@ -550,8 +515,8 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, case df::tile_dig_designation::No: default: out.printerr( - "unhandled dig designation for tile (%d, %d, %d): %d\n", - pos.x, pos.y, pos.z, designation); + "unhandled dig designation for tile ({}, {}, {}): {}\n", + pos.x, pos.y, pos.z, ENUM_AS_STR(designation)); } // fail if unhandled or no change to tile @@ -559,7 +524,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, return false; dug_tiles.emplace_back(map, pos); - TRACE(general).print("dig_tile: digging the designation tile at (" COORD ")\n",COORDARGS(pos)); + TRACE(general).print("dig_tile: digging the designation tile at ({})\n",pos); dig_type(map, pos, target_type); clean_ramps(map, pos); @@ -737,7 +702,7 @@ static bool produces_item(const boulder_percent_options &options, return rng.random(100) < probability; } -typedef std::map, std::vector> +typedef std::map, std::vector> item_coords_t; static void do_dig(color_ostream &out, std::vector &dug_coords, @@ -876,8 +841,8 @@ static void create_boulders(color_ostream &out, prod->item_type = entry.first.first; prod->item_subtype = -1; - prod->mat_type = 0; - prod->mat_index = entry.first.second; + prod->mat_type = entry.first.second.mat_type; + prod->mat_index = entry.first.second.mat_index; prod->probability = 100; prod->product_dimension = 1; @@ -898,10 +863,8 @@ static void create_boulders(color_ostream &out, if (num_items != coords.size()) { MaterialInfo material; material.decode(prod->mat_type, prod->mat_index); - out.printerr("unexpected number of %s %s produced: expected %zd," - " got %zd.\n", - material.toString().c_str(), - ENUM_KEY_STR(item_type, prod->item_type).c_str(), + out.printerr("unexpected number of {} {} produced: expected {}, got {}.\n", + material.toString(), ENUM_KEY_STR(item_type, prod->item_type), coords.size(), num_items); num_items = std::min(num_items, entry.second.size()); } @@ -911,7 +874,7 @@ static void create_boulders(color_ostream &out, dump_pos : simulate_fall(coords[i]); if (!Maps::ensureTileBlock(pos)) { out.printerr( - "unable to place boulder generated at (%d, %d, %d)\n", + "unable to place boulder generated at ({}, {}, {})\n", coords[i].x, coords[i].y, coords[i].z); continue; } @@ -956,7 +919,7 @@ static void post_process_dug_tiles(color_ostream &out, continue; if (!Maps::ensureTileBlock(resting_pos)) { - out.printerr("No valid tile beneath (%d, %d, %d); can't move" + out.printerr("No valid tile beneath ({},{},{}) can't move" " units and items to floor", pos.x, pos.y, pos.z); continue; diff --git a/plugins/dig.cpp b/plugins/dig.cpp index bc9089a654..55f33663ae 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -68,7 +68,7 @@ static bool is_painting_warm = false; static bool is_painting_damp = false; DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/damp_dig_map.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "damp_dig_map.png", 32, 32, true); commands.push_back(PluginCommand( "digv", @@ -108,7 +108,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector getassignment(pos), is_warm(pos)); if (warm_mask->getassignment(pos) && is_warm(pos)) { - DEBUG(log,out).print("revealing warm dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing warm dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } if (auto damp_mask = World::getPersistentTilemask(damp_config, block)) { - TRACE(log,out).print("testing tile at (%d,%d,%d); mask:%d, damp:%d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("testing tile at ({},{},{}); mask:{}, damp:{}\n", pos.x, pos.y, pos.z, damp_mask->getassignment(pos), is_damp(pos)); if (damp_mask->getassignment(pos) && is_damp(pos)) { - DEBUG(log,out).print("revealing damp dig tile at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + DEBUG(log,out).print("revealing damp dig tile at ({},{},{})\n", pos.x, pos.y, pos.z); block->designation[pos.x&15][pos.y&15].bits.hidden = false; } } @@ -418,7 +418,7 @@ static void unhide_surrounding_tagged_tiles(color_ostream& out, void* job_ptr) { return; const auto & pos = job->pos; - TRACE(log,out).print("handing dig job at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + TRACE(log,out).print("handing dig job at ({},{},{})\n", pos.x, pos.y, pos.z); process_taken_dig_job(out, pos); @@ -496,7 +496,7 @@ bool dig (MapExtras::MapCache & MCache, ) { DFCoord at (x,y,z); - auto b = MCache.BlockAt(at/16); + auto b = MCache.BlockAtTile(at); if(!b || !b->is_valid()) return false; if(x == 0 || x == x_max * 16 - 1) @@ -626,7 +626,7 @@ int32_t parse_priority(color_ostream &out, vector ¶meters) } else { - out.printerr("invalid priority specified; reverting to %i\n", default_priority); + out.printerr("invalid priority specified; reverting to {}\n", default_priority); break; } } @@ -1470,7 +1470,7 @@ command_result digv (color_ostream &out, vector & parameters) con.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, des.whole); + con.print("{} tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", xy, ENUM_AS_STR(tt), veinmat, des.whole); stack flood; flood.push(xy); @@ -1654,7 +1654,7 @@ command_result digl (color_ostream &out, vector & parameters) con.printerr("This is a vein. Use digv instead!\n"); return CR_FAILURE; } - con.print("%d/%d/%d tiletype: %d, basemat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, basemat, des.whole); + con.print("{}/{}/{}/ tiletype: {}, basemat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), basemat, des.whole); stack flood; flood.push(xy); @@ -1841,7 +1841,7 @@ command_result digtype (color_ostream &out, vector & parameters) automine = false; else { - out.printerr("Invalid parameter: '%s'.\n", parameter.c_str()); + out.printerr("Invalid parameter: '{}'.\n", parameter); return CR_FAILURE; } } @@ -1873,7 +1873,7 @@ command_result digtype (color_ostream &out, vector & parameters) out.printerr("This tile is not a vein.\n"); return CR_FAILURE; } - out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole); + out.print("({},{},{}) tiletype: {}, veinmat: {}, designation: 0x{:x} ... DIGGING!\n", cx,cy,cz, ENUM_AS_STR(tt), veinmat, baseDes.whole); if ( targetDigType != -1 ) { @@ -1910,7 +1910,7 @@ command_result digtype (color_ostream &out, vector & parameters) //designate it for digging if ( !mCache->testCoord(current) ) { - out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z); + out.printerr("testCoord failed at ({},{},{})\n", x, y, z); return CR_FAILURE; } @@ -2158,7 +2158,7 @@ static bool blink(int delay) { } static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = false) { - TRACE(log).print("entering paintScreenDampWarm aquifer_mode=%d, show_damp=%d\n", aquifer_mode, show_damp); + TRACE(log).print("entering paintScreenDampWarm aquifer_mode={}, show_damp={}\n", aquifer_mode, show_damp); static Screen::Pen empty_pen; @@ -2220,7 +2220,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals bump_layers(*pen, x, y); } } else { - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", pos.x, pos.y, pos.z, x, y); auto des = Maps::getTileDesignation(pos); @@ -2231,7 +2231,7 @@ static void paintScreenWarmDamp(bool aquifer_mode = false, bool show_damp = fals Screen::Pen cur_tile = Screen::readTile(x, y, true); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } @@ -2388,6 +2388,10 @@ static bool is_designated_for_track_carving(const designation &designation) { return occ.bits.carve_track_east || occ.bits.carve_track_north || occ.bits.carve_track_south || occ.bits.carve_track_west; } +static bool is_designated_for_digging(const designation &designation) { + return designation.td.bits.dig != df::tile_dig_designation::No; +} + static char get_track_char(const designation &designation) { const df::tile_occupancy &occ = designation.to; if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) @@ -2444,8 +2448,8 @@ static char get_tile_char(const df::coord &pos, char desig_char, bool draw_prior } } -static void paintScreenCarve() { - TRACE(log).print("entering paintScreenCarve\n"); +static void paintScreenDesignated() { + TRACE(log).print("entering paintScreenDesignated\n"); if (Screen::inGraphicsMode() || blink(500)) return; @@ -2461,12 +2465,7 @@ static void paintScreenCarve() { if (!Maps::isValidTilePos(map_pos)) continue; - if (!Maps::isTileVisible(map_pos)) { - TRACE(log).print("skipping hidden tile\n"); - continue; - } - - TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", + TRACE(log).print("scanning map tile at ({},{},{}) screen offset ({},{})\n", map_pos.x, map_pos.y, map_pos.z, x, y); Screen::Pen cur_tile; @@ -2486,8 +2485,14 @@ static void paintScreenCarve() { else if (is_designated_for_track_carving(des)) { cur_tile.ch = get_tile_char(map_pos, get_track_char(des), draw_priority); // directional track } + else if (is_designated_for_digging(des)) { + static char empty_char = (char)0x00; + cur_tile.ch = get_tile_char(map_pos, empty_char, draw_priority); + if (cur_tile.ch == empty_char) + continue; + } else { - TRACE(log).print("skipping tile with no carving designation\n"); + TRACE(log).print("skipping tile with no designation\n"); continue; } @@ -2515,6 +2520,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(toggleCurLevelWarmDig), DFHACK_LUA_FUNCTION(toggleCurLevelDampDig), DFHACK_LUA_FUNCTION(paintScreenWarmDamp), - DFHACK_LUA_FUNCTION(paintScreenCarve), + DFHACK_LUA_FUNCTION(paintScreenDesignated), DFHACK_LUA_END }; diff --git a/plugins/digFlood.cpp b/plugins/digFlood.cpp index 045e6c61d6..c1d0eccbc5 100644 --- a/plugins/digFlood.cpp +++ b/plugins/digFlood.cpp @@ -181,7 +181,7 @@ command_result digFlood (color_ostream &out, std::vector & paramet } } - out.print("Could not find material \"%s\".\n", parameters[a].c_str()); + out.print("Could not find material \"{}\".\n", parameters[a]); return CR_WRONG_USAGE; loop: continue; diff --git a/plugins/diggingInvaders/assignJob.cpp b/plugins/diggingInvaders/assignJob.cpp index eb74433b86..1bd5c97e4d 100644 --- a/plugins/diggingInvaders/assignJob.cpp +++ b/plugins/diggingInvaders/assignJob.cpp @@ -231,13 +231,13 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapmat_type = material.type; @@ -255,7 +255,7 @@ int32_t assignJob(color_ostream& out, Edge firstImportantEdge, unordered_mapsite_id), NULL); if ( out_items.size() != 1 ) { - out.print("%s, %d: wrong size: %zu.\n", __FILE__, __LINE__, out_items.size()); + out.print("{}, {}: wrong size: {}.\n", __FILE__, __LINE__, out_items.size()); return -1; } out_items[0]->moveToGround(firstInvader->pos.x, firstInvader->pos.y, firstInvader->pos.z); diff --git a/plugins/diggingInvaders/diggingInvaders.cpp b/plugins/diggingInvaders/diggingInvaders.cpp index d883ef4dba..fff7f033ea 100644 --- a/plugins/diggingInvaders/diggingInvaders.cpp +++ b/plugins/diggingInvaders/diggingInvaders.cpp @@ -276,13 +276,13 @@ command_result diggingInvadersCommand(color_ostream& out, std::vectorjob.current_job && lastDigger->job.current_job->id == lastInvasionJob ) { return; } - //out.print("%s,%d: lastDigger = %d, last job = %d, last digger's job = %d\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); + //out.print("{},{}: lastDigger = {}, last job = {}, last digger's job = {}\n", __FILE__, __LINE__, lastInvasionDigger, lastInvasionJob, !lastDigger ? -1 : (!lastDigger->job.current_job ? -1 : lastDigger->job.current_job->id)); lastInvasionDigger = lastInvasionJob = -1; clearDijkstra(); @@ -382,7 +382,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { } else if ( unit->flags1.bits.active_invader ) { df::creature_raw* raw = df::creature_raw::find(unit->race); if ( raw == NULL ) { - out.print("%s,%d: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); + out.print("{},{}: WTF? Couldn't find creature raw.\n", __FILE__, __LINE__); continue; } /* @@ -464,7 +464,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { fringe.erase(fringe.begin()); //out.print("line %d: fringe size = %d, localPtsFound = %d / %d, closedSetSize = %d, pt = %d,%d,%d\n", __LINE__, fringe.size(), localPtsFound, localPts.size(), closedSet.size(), pt.x,pt.y,pt.z); if ( closedSet.find(pt) != closedSet.end() ) { - out.print("%s, line %d: Double closure! Bad!\n", __FILE__, __LINE__); + out.print("{},{}: Double closure! Bad!\n", __FILE__, __LINE__); break; } closedSet.insert(pt); @@ -511,7 +511,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { delete myEdges; } // clock_t time = clock() - t0; - //out.print("tickTime = %d, time = %d, totalEdgeTime = %d, total points = %d, total edges = %d, time per point = %.3f, time per edge = %.3f, clocks/sec = %d\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); + //out.print("tickTime = {}, time = {}, totalEdgeTime = {}, total points = {}, total edges = {}, time per point = {:.3f}, time per edge = {:.3f}, clocks/sec = {}\n", (int32_t)tickTime, time, totalEdgeTime, closedSet.size(), edgeCount, (float)time / closedSet.size(), (float)time / edgeCount, CLOCKS_PER_SEC); fringe.clear(); if ( !foundTarget ) @@ -590,7 +590,7 @@ void findAndAssignInvasionJob(color_ostream& out, void* tickTime) { //cancel it job->flags.bits.item_lost = 1; - out.print("%s,%d: cancelling job %d.\n", __FILE__,__LINE__, job->id); + out.print("{},{}: cancelling job {}.\n", __FILE__,__LINE__, job->id); //invaderJobs.remove(job->id); } invaderJobs.erase(lastInvasionJob); diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index d4fddb4846..35f0238123 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -46,7 +46,6 @@ #include "df/unit_preference.h" #include "df/unit_soul.h" #include "df/viewscreen_unitst.h" -#include "df/world_raws.h" using std::deque; @@ -1184,7 +1183,7 @@ struct preference_map label = ""; typedef df::unit_preference::T_type T_type; - df::world_raws &raws = world->raws; + auto &raws = world->raws; switch (pref.type) { case (T_type::LikeCreature): diff --git a/plugins/dwarfvet.cpp b/plugins/dwarfvet.cpp index 6f3e32d00f..7847dfa8e8 100644 --- a/plugins/dwarfvet.cpp +++ b/plugins/dwarfvet.cpp @@ -69,17 +69,17 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -140,7 +140,7 @@ static void dwarfvet_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); Lua::CallLuaModuleFunction(out, "plugins.dwarfvet", "checkup"); } diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp new file mode 100644 index 0000000000..cdc0c349d2 --- /dev/null +++ b/plugins/edgescroll.cpp @@ -0,0 +1,232 @@ +#include "ColorText.h" +#include "MemAccess.h" +#include "PluginManager.h" + +#include "modules/Gui.h" +#include "modules/DFSDL.h" + +#include "df/enabler.h" +#include "df/gamest.h" +#include "df/graphic.h" +#include "df/graphic_viewportst.h" +#include "df/renderer_2d.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/viewscreen_worldst.h" +#include "df/viewscreen_new_regionst.h" +#include "df/world.h" +#include "df/world_data.h" +#include "df/world_generatorst.h" + +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("edgescroll"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(enabler); +REQUIRE_GLOBAL(game); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(gps); + +// Cooldown between edge scroll actions +constexpr uint32_t cooldown_ms = 100; +// Number of pixels from border to trigger edgescroll +constexpr int border_range = 5; + +// Controls how much edge scroll moves +constexpr int map_scroll_pixels = 100; +constexpr int world_scroll_tiles = 3; +constexpr int world_scroll_tiles_zoomed = 6; + +DFhackCExport command_result plugin_init([[maybe_unused]]color_ostream &out, [[maybe_unused]] std::vector &commands) { + return CR_OK; +} + +DFhackCExport command_result plugin_enable([[maybe_unused]]color_ostream &out, bool enable) { + is_enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out) { + return CR_OK; +} + +static std::atomic_bool callback_queued = false; + +struct scroll_state { + int8_t xdiff = 0; + int8_t ydiff = 0; +}; + +static scroll_state state; +static scroll_state queued; + +static void render_thread_cb() { + queued = {}; + // Ignore the mouse if outside the window + if (!enabler->mouse_focus) { + callback_queued.store(false); + return; + } + + // Calculate the render rect in window coordinates + auto* renderer = virtual_cast(enabler->renderer); + int origin_x, origin_y; + int end_x, end_y; + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, (float)renderer->origin_x, + (float)renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, + (float)renderer->cur_w - (float)renderer->origin_x, + (float)(renderer->cur_h - renderer->origin_y), &end_x, &end_y); + + // Get the mouse location in window coordinates + int mx, my; + DFSDL::DFSDL_GetMouseState(&mx, &my); + + if (mx <= origin_x + border_range) { + queued.xdiff--; + } else if (mx >= end_x - border_range) { + queued.xdiff++; + } + if (my <= origin_y + border_range) { + queued.ydiff--; + } else if (my >= end_y - border_range) { + queued.ydiff++; + } + + callback_queued.store(false); +} + +static bool update_mouse_pos() { + if (callback_queued.load()) + return false; // Queued callback not complete, check back later + + // Queued callback complete, save the results and enqueue again + state = queued; + queued = {}; + DFHack::runOnRenderThread(render_thread_cb); + callback_queued.store(true); + return true; +} + +// Apply scroll whilst maintaining boundaries +template +static void apply_scroll(T* out, T diff, T min, T max) { + *out = std::min(std::max(*out + diff, min), max); +} + +// Scroll main fortress/adventure world views +static void scroll_dwarfmode(int xdiff, int ydiff) { + using df::global::window_x; + using df::global::window_y; + using df::global::game; + // Scale the movement by pixels, to keep scroll speeds visually consistent + int tilesize = gps->viewport_zoom_factor / 4; + int width = gps->main_viewport->dim_x; + int height = gps->main_viewport->dim_y; + + // Ensure the map doesn't go fully off-screen + int min_x = -width / 2; + int min_y = -height / 2; + int max_x = world->map.x_count - (width / 2); + int max_y = world->map.y_count - (height / 2); + apply_scroll(window_x, xdiff * std::max(1, map_scroll_pixels / tilesize), min_x, max_x); + apply_scroll(window_y, ydiff * std::max(1, map_scroll_pixels / tilesize), min_y, max_y); + + // Force a minimap update + game->minimap.update = 1; + game->minimap.mustmake = 1; +} + +template +static void scroll_world_internal(T* screen, int xdiff, int ydiff) { + if constexpr(std::is_same_v) { + if (screen->zoomed_in) { + int max_x = (world->world_data->world_width * 16)-1; + int max_y = (world->world_data->world_height * 16)-1; + apply_scroll(&screen->zoom_cent_x, xdiff * world_scroll_tiles_zoomed, 0, max_x); + apply_scroll(&screen->zoom_cent_y, ydiff * world_scroll_tiles_zoomed, 0, max_y); + return; + } + } + + int32_t *x, *y; + if constexpr(std::is_same_v) { + x = &world->worldgen_status.cursor_x; + y = &world->worldgen_status.cursor_y; + } else { + x = &screen->region_cent_x; + y = &screen->region_cent_y; + } + int max_x = world->world_data->world_width-1; + int max_y = world->world_data->world_height-1; + apply_scroll(x, xdiff * world_scroll_tiles, 0, max_x); + apply_scroll(y, ydiff * world_scroll_tiles, 0, max_y); +} + +template +struct overloads : Ts... { using Ts::operator()...; }; + +using world_map = std::variant; +static std::optional get_map() { + df::viewscreen* screen = Gui::getCurViewscreen(true); + screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if(!screen) + return {}; + + if (auto start_site = virtual_cast(screen)) + return start_site; + if (auto world_map = virtual_cast(screen)) + return world_map; + if (auto worldgen_map = virtual_cast(screen)) { + if (!world || world->worldgen_status.state <= df::world_generatorst::Initializing) + return {}; // Map isn't displayed yet + return worldgen_map; + } + return {}; +} + +static void scroll_world(world_map screen, int xdiff, int ydiff) { + const auto visitor = overloads { + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world_internal(s, xdiff, ydiff);}, + }; + std::visit(visitor, screen); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + // Apply a cooldown to any potential edgescrolls + auto& core = Core::getInstance(); + static uint32_t last_action = 0; + uint32_t now = core.p->getTickCount(); + if (now < last_action + cooldown_ms) + return CR_OK; + + // Update mouse_x/y from values read in render thread callback + if (!update_mouse_pos()) + return CR_OK; // No new input to process + + if (state.xdiff == 0 && state.ydiff == 0) + return CR_OK; // No work to do + + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; + + // Dispatch scrolling to active scrollables + if (worldmap.has_value()) + scroll_world(worldmap.value(), state.xdiff, state.ydiff); + else if (gps->main_viewport->flag.bits.active) + scroll_dwarfmode(state.xdiff, state.ydiff); + + // Update cooldown + last_action = now; + + return CR_OK; +} diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp index ee418363e7..5b9d11f78a 100644 --- a/plugins/embark-assistant/embark-assistant.cpp +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "modules/Gui.h" +#include "modules/Hotkey.h" #include "modules/Screen.h" #include "../uicommon.h" @@ -18,7 +19,6 @@ #include "df/world.h" #include "df/world_data.h" #include "df/world_geo_biome.h" -#include "df/world_raws.h" #include "defs.h" #include "embark-assistant.h" @@ -162,7 +162,7 @@ struct start_site_hook : df::viewscreen_choose_start_sitest { { if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) { - Core::getInstance().setHotkeyCmd("embark-assistant"); + Core::getInstance().getHotkeyManager()->setHotkeyCommand("embark-assistant"); return; } INTERPOSE_NEXT(feed)(input); diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index b4c55dd823..8e695bd748 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -14,9 +14,7 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region_type.h" -#include "df/world_raws.h" #include "embark-assistant.h" #include "finder_ui.h" @@ -200,7 +198,7 @@ namespace embark_assist { size_t civ = 0; if (!infile) { - out.printerr("No profile file found at %s\n", profile_file_name); + out.printerr("No profile file found at {}\n", profile_file_name); return; } @@ -211,7 +209,7 @@ namespace embark_assist { while (true) { if (!fgets(line, count, infile) || line[0] != '[') { - out.printerr("Failed to find token start '[' at line %i\n", static_cast(i)); + out.printerr("Failed to find token start '[' at line {}\n", static_cast(i)); fclose(infile); return; } @@ -220,7 +218,7 @@ namespace embark_assist { if (line[k] == ':') { for (int l = 1; l < k; l++) { if (state->finder_list[static_cast(i) + civ].text.c_str()[l - 1] != line[l]) { - out.printerr("Token mismatch of %s vs %s\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); + out.printerr("Token mismatch of {} vs {}\n", line, state->finder_list[static_cast(i) + civ].text.c_str()); fclose(infile); return; } @@ -244,7 +242,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value extraction failure from %s\n", line); + out.printerr("Value extraction failure from {}\n", line); fclose(infile); return; } @@ -254,7 +252,7 @@ namespace embark_assist { } if (!found) { - out.printerr("Value delimiter not found in %s\n", line); + out.printerr("Value delimiter not found in {}\n", line); fclose(infile); return; } diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index b540ae80b4..61da64959b 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -10,7 +10,6 @@ #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" #include "df/world_data.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_type.h" @@ -1451,21 +1450,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0 && !tile->neighboring_savagery[i]) { - if (trace) out.print("matcher::world_tile_match: Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -1481,21 +1480,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] < embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0 && !tile->neighboring_evilness[i]) { - if (trace) out.print("matcher::world_tile_match: Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Evil Absent ({}, {})\n", x, y); return false; } break; @@ -1512,14 +1511,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Most_Light: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light ({}, {})\n", x, y); return false; } break; @@ -1527,7 +1526,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Light ({}, {})\n", x, y); return false; } break; @@ -1535,21 +1534,21 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_At_Least_Light: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || (tile->aquifer == embark_assist::defs::None_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_At_Least_Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Light: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::At_Least_Light: if (tile->aquifer == embark_assist::defs::None_Aquifer_Bit) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Least_Light ({}, {})\n", x, y); return false; } break; @@ -1557,7 +1556,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::None_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1565,7 +1564,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::At_Most_Light_Plus_Heavy: if (tile->aquifer == embark_assist::defs::Heavy_Aquifer_Bit || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer At_Most_Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1573,7 +1572,7 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::Light_Plus_Heavy: if (!(tile->aquifer & embark_assist::defs::Light_Aquifer_Bit) || !(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Light_Plus_Heavy ({}, {})\n", x, y); return false; } break; @@ -1581,14 +1580,14 @@ namespace embark_assist { case embark_assist::defs::aquifer_ranges::None_Light_Heavy: if (tile->aquifer != (embark_assist::defs::None_Aquifer_Bit | embark_assist::defs::Light_Aquifer_Bit | embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer None_Light_Heavy ({}, {})\n", x, y); return false; } break; case embark_assist::defs::aquifer_ranges::Heavy: if (!(tile->aquifer & embark_assist::defs::Heavy_Aquifer_Bit)) { - if (trace) out.print("matcher::world_tile_match: Aquifer Heavy (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Aquifer Heavy ({}, {})\n", x, y); return false; } break; @@ -1598,42 +1597,42 @@ namespace embark_assist { switch (tile->max_river_size) { case embark_assist::defs::river_sizes::None: if (finder->min_river > embark_assist::defs::river_ranges::None) { - if (trace) out.print("matcher::world_tile_match: River_Size None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Brook: if (finder->min_river > embark_assist::defs::river_ranges::Brook) { - if (trace) out.print("matcher::world_tile_match: River_Size Brook (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Brook ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Stream: if (finder->min_river > embark_assist::defs::river_ranges::Stream) { - if (trace) out.print("matcher::world_tile_match: River_Size Stream (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Stream ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Minor: if (finder->min_river > embark_assist::defs::river_ranges::Minor) { - if (trace) out.print("matcher::world_tile_match: River_Size Mino (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Mino ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Medium: if (finder->min_river > embark_assist::defs::river_ranges::Medium) { - if (trace) out.print("matcher::world_tile_match: River_Size Medium (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Medium ({}, {})\n", x, y); return false; } break; case embark_assist::defs::river_sizes::Major: if (finder->max_river != embark_assist::defs::river_ranges::NA) { - if (trace) out.print("matcher::world_tile_match: River_Size Major (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: River_Size Major ({}, {})\n", x, y); return false; } break; @@ -1641,13 +1640,13 @@ namespace embark_assist { // Waterfall if (finder->min_waterfall > tile->max_waterfall) { // N/A = -1 is always smaller - if (trace) out.print("matcher::world_tile_match: Waterfall (%i, %i), finder: %i, tile: %i\n", x, y, finder->min_waterfall, tile->max_waterfall); + if (trace) out.print("matcher::world_tile_match: Waterfall ({}, {}), finder: {}, tile: {}\n", x, y, finder->min_waterfall, tile->max_waterfall); return false; } if (finder->min_waterfall == 0 && // Absent embark_size == 256 && tile->max_waterfall > 0) { - if (trace) out.print("matcher::world_tile_match: Waterfall 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Waterfall 2 ({}, {})\n", x, y); return false; } @@ -1661,14 +1660,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->clay_count == 0 && !tile->neighboring_clay) { - if (trace) out.print("matcher::world_tile_match: Clay Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->clay_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Clay Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Clay Absent ({}, {})\n", x, y); return false; } break; @@ -1682,14 +1681,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->sand_count == 0 && !tile->neighboring_sand) { - if (trace) out.print("matcher::world_tile_match: Sand Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->sand_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Sand Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Sand Absent ({}, {})\n", x, y); return false; } break; @@ -1702,14 +1701,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Flux Absent ({}, {})\n", x, y); return false; } break; @@ -1722,14 +1721,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count > 256 - embark_size) { - if (trace) out.print("matcher::world_tile_match: Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Coal Absent ({}, {})\n", x, y); return false; } break; @@ -1743,28 +1742,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: Soil Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: Soil Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil Min Very Deep ({}, {})\n", x, y); return false; } break; @@ -1780,28 +1779,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::None: if (tile->min_region_soil > 0) { - if (trace) out.print("matcher::world_tile_match: Soil_Max None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max None ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->min_region_soil > 1) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->min_region_soil > 2) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->min_region_soil > 3) { - if (trace) out.print("matcher::world_tile_match: Soil_Max Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Soil_Max Deep ({}, {})\n", x, y); return false; } break; @@ -1841,14 +1840,14 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Permanent: if (min_max_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Least_Partial: if (min_min_temperature > 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At_Lest_Partial ({}, {})\n", x, y); return false; } break; @@ -1856,21 +1855,21 @@ namespace embark_assist { case embark_assist::defs::freezing_ranges::Partial: if (min_min_temperature > 0 || max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::At_Most_Partial: if (max_max_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing At Most_Partial ({}, {})\n", x, y); return false; } break; case embark_assist::defs::freezing_ranges::Never: if (max_min_temperature <= 0) { - if (trace) out.print("matcher::world_tile_match: Freezing Never (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Freezing Never ({}, {})\n", x, y); return false; } break; @@ -1885,28 +1884,28 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->max_tree_level < embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->max_tree_level < embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Woodland ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Heavily_Forested: if (tile->max_tree_level < embark_assist::defs::tree_levels::Heavily_Forested) { - if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Min_Trees Heavily_Forested ({}, {})\n", x, y); return false; } break; @@ -1919,7 +1918,7 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::None: if (tile->min_tree_level > embark_assist::defs::tree_levels::None) { - if (trace) out.print("matcher::world_tile_match: Max_Trees None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees None ({}, {})\n", x, y); return false; } break; @@ -1927,21 +1926,21 @@ namespace embark_assist { case embark_assist::defs::tree_ranges::Very_Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Very_Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Very_Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Scarce: if (tile->min_tree_level > embark_assist::defs::tree_levels::Scarce) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Scarce ({}, {})\n", x, y); return false; } break; case embark_assist::defs::tree_ranges::Woodland: if (tile->min_tree_level > embark_assist::defs::tree_levels::Woodland) { - if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Max_Trees Woodland ({}, {})\n", x, y); return false; } break; @@ -1954,14 +1953,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -1974,35 +1973,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2015,42 +2014,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Reanimation ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: Reanimation None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Reanimation None ({}, {})\n", x, y); return false; } break; @@ -2064,7 +2063,7 @@ namespace embark_assist { // Region Type 1 if (finder->region_type_1 != -1) { if (!tile->neighboring_region_types[finder->region_type_1]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2072,7 +2071,7 @@ namespace embark_assist { // Region Type 2 if (finder->region_type_2 != -1) { if (!tile->neighboring_region_types[finder->region_type_2]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2080,7 +2079,7 @@ namespace embark_assist { // Region Type 3 if (finder->region_type_3 != -1) { if (!tile->neighboring_region_types[finder->region_type_3]) { - if (trace) out.print("matcher::world_tile_match: Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2088,7 +2087,7 @@ namespace embark_assist { // Biome 1 if (finder->biome_1 != -1) { if (!tile->neighboring_biomes[finder->biome_1]) { - if (trace) out.print("matcher::world_tile_match: Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_1 ({}, {})\n", x, y); return false; } } @@ -2096,7 +2095,7 @@ namespace embark_assist { // Biome 2 if (finder->biome_2 != -1) { if (!tile->neighboring_biomes[finder->biome_2]) { - if (trace) out.print("matcher::world_tile_match: Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_2 ({}, {})\n", x, y); return false; } } @@ -2104,7 +2103,7 @@ namespace embark_assist { // Biome 3 if (finder->biome_3 != -1) { if (!tile->neighboring_biomes[finder->biome_3]) { - if (trace) out.print("matcher::world_tile_match: Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Biome_3 ({}, {})\n", x, y); return false; } } @@ -2147,30 +2146,30 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } // Necro Neighbors if (finder->min_necro_neighbors > tile->necro_neighbors) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 1 ({}, {})\n", x, y); return false; } if (finder->max_necro_neighbors < tile->necro_neighbors && finder->max_necro_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Necro_Neighbors 2 ({}, {})\n", x, y); return false; } // Civ Neighbors if (finder->min_civ_neighbors > (int16_t)tile->neighbors.size()) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 (%i, %i), %i, %i\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 1 ({}, {}), {}, {}\n", x, y, finder->min_civ_neighbors, (int)tile->neighbors.size()); return false; } if (finder->max_civ_neighbors < (int8_t)tile->neighbors.size() && finder->max_civ_neighbors != -1) { - if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Civ_Neighbors 2 ({}, {})\n", x, y); return false; } @@ -2192,7 +2191,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Present ({}, {})\n", x, y); return false; } @@ -2202,7 +2201,7 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Absent: for (uint16_t k = 0; k < tile->neighbors.size(); k++) { if (finder->neighbors[i].entity_raw == tile->neighbors[k]) { - if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: Specific Neighbors Absent ({}, {})\n", x, y); return false; } } @@ -2224,21 +2223,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->savagery_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->savagery_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Savagery Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Savagery Absent ({}, {})\n", x, y); return false; } break; @@ -2254,21 +2253,21 @@ namespace embark_assist { case embark_assist::defs::evil_savagery_values::All: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil All (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil All ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Present: if (tile->evilness_count[i] == 0) { - if (trace) out.print("matcher::world_tile_match: NS Evil Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::evil_savagery_values::Absent: if (tile->evilness_count[i] == 256) { - if (trace) out.print("matcher::world_tile_match: NS Evil Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Evil Absent ({}, {})\n", x, y); return false; } break; @@ -2296,14 +2295,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->flux_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Flux Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->flux_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Flux Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Flux Absent ({}, {})\n", x, y); return false; } break; @@ -2316,14 +2315,14 @@ namespace embark_assist { case embark_assist::defs::present_absent_ranges::Present: if (tile->coal_count == 0) { - if (trace) out.print("matcher::world_tile_match: NS Coal Present (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Present ({}, {})\n", x, y); return false; } break; case embark_assist::defs::present_absent_ranges::Absent: if (tile->coal_count == 256) { - if (trace) out.print("matcher::world_tile_match: NS Coal Absent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Coal Absent ({}, {})\n", x, y); return false; } break; @@ -2337,28 +2336,28 @@ namespace embark_assist { case embark_assist::defs::soil_ranges::Very_Shallow: if (tile->max_region_soil < 1) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Shallow: if (tile->max_region_soil < 2) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Shallow ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Deep: if (tile->max_region_soil < 3) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Deep ({}, {})\n", x, y); return false; } break; case embark_assist::defs::soil_ranges::Very_Deep: if (tile->max_region_soil < 4) { - if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Soil_Min Very_Deep ({}, {})\n", x, y); return false; } break; @@ -2376,14 +2375,14 @@ namespace embark_assist { case embark_assist::defs::yes_no_ranges::Yes: if (!tile->blood_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain Yes ({}, {})\n", x, y); return false; } break; case embark_assist::defs::yes_no_ranges::No: if (tile->blood_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Blood_Rain No ({}, {})\n", x, y); return false; } break; @@ -2398,35 +2397,35 @@ namespace embark_assist { case embark_assist::defs::syndrome_rain_ranges::Any: if (!tile->permanent_syndrome_rain_possible && !tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Permanent: if (!tile->permanent_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Temporary: if (!tile->temporary_syndrome_rain_possible) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Temporary ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::Not_Permanent: if (tile->permanent_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain Not_Permanent ({}, {})\n", x, y); return false; } break; case embark_assist::defs::syndrome_rain_ranges::None: if (tile->permanent_syndrome_rain_full || tile->temporary_syndrome_rain_full) { - if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Syndrome_Rain None ({}, {})\n", x, y); return false; } break; @@ -2439,42 +2438,42 @@ namespace embark_assist { case embark_assist::defs::reanimation_ranges::Both: if (!tile->reanimating_possible || !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Both (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Both ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Any: if (!tile->reanimating_possible && !tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Any (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Any ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Thralling: if (!tile->thralling_possible) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Reanimation: if (!tile->reanimating_possible) { - if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match:NS Reanimating Reanimating ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::Not_Thralling: if (tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating Not_Thralling ({}, {})\n", x, y); return false; } break; case embark_assist::defs::reanimation_ranges::None: if (tile->reanimating_full || tile->thralling_full) { - if (trace) out.print("matcher::world_tile_match: NS Reanimating None (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Reanimating None ({}, {})\n", x, y); return false; } break; @@ -2484,7 +2483,7 @@ namespace embark_assist { // Magma Min/Max // Biome Count Min (Can't do anything with Max at this level) if (finder->biome_count_min > tile->biome_count) { - if (trace) out.print("matcher::world_tile_match: NS Biome_Count (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_Count ({}, {})\n", x, y); return false; } @@ -2504,7 +2503,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_1 ({}, {})\n", x, y); return false; } } @@ -2525,7 +2524,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_2 ({}, {})\n", x, y); return false; } } @@ -2546,7 +2545,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Region_Type_3 ({}, {})\n", x, y); return false; } } @@ -2563,7 +2562,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_1 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_1 ({}, {})\n", x, y); return false; } } @@ -2580,7 +2579,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_2 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_2 ({}, {})\n", x, y); return false; } } @@ -2597,7 +2596,7 @@ namespace embark_assist { } if (!found) { - if (trace) out.print("matcher::world_tile_match: NS Biome_3 (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Biome_3 ({}, {})\n", x, y); return false; } } @@ -2640,7 +2639,7 @@ namespace embark_assist { !mineral_1 || !mineral_2 || !mineral_3) { - if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral (%i, %i)\n", x, y); + if (trace) out.print("matcher::world_tile_match: NS Metal/Economic/Mineral ({}, {})\n", x, y); return false; } } @@ -2949,11 +2948,11 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter preliminary_matches = preliminary_world_match(survey_results, &iterator->finder, match_results); if (preliminary_matches == 0) { - out.printerr("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.printerr("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); return 0; } else { - out.print("matcher::find: Preliminarily matching World Tiles: %i\n", preliminary_matches); + out.print("matcher::find: Preliminarily matching World Tiles: {}\n", preliminary_matches); } while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) { diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp index d5b50294a9..37130c6897 100644 --- a/plugins/embark-assistant/overlay.cpp +++ b/plugins/embark-assistant/overlay.cpp @@ -8,7 +8,6 @@ #include "df/viewscreen.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/world.h" -#include "df/world_raws.h" #include "finder_ui.h" #include "help_ui.h" diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp index 84994b2869..ddfc6d8d49 100644 --- a/plugins/embark-assistant/survey.cpp +++ b/plugins/embark-assistant/survey.cpp @@ -30,7 +30,6 @@ #include "df/interaction_source_type.h" #include "df/interaction_target.h" #include "df/interaction_target_materialst.h" -#include "df/material_common.h" #include "df/reaction.h" #include "df/reaction_product_itemst.h" #include "df/reaction_product_type.h" @@ -42,7 +41,6 @@ #include "df/world_data.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" -#include "df/world_raws.h" #include "df/world_region.h" #include "df/world_region_details.h" #include "df/world_region_feature.h" diff --git a/plugins/eventful.cpp b/plugins/eventful.cpp index 0ed06d9284..859649c470 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include "LuaTools.h" #include "PluginManager.h" #include "PluginLua.h" @@ -9,6 +13,7 @@ #include "df/building_furnacest.h" #include "df/building_workshopst.h" #include "df/construction.h" +#include "df/global_objects.h" #include "df/item.h" #include "df/item_actual.h" #include "df/job.h" @@ -22,10 +27,6 @@ #include "df/unit_wound.h" #include "df/world.h" -#include -#include -#include - using std::vector; using std::string; using std::stack; diff --git a/plugins/examples/persistent_per_save_example.cpp b/plugins/examples/persistent_per_save_example.cpp index 3ca26fc1d3..4067e1d161 100644 --- a/plugins/examples/persistent_per_save_example.cpp +++ b/plugins/examples/persistent_per_save_example.cpp @@ -56,14 +56,14 @@ static PersistentDataItem & ensure_elem_config(color_ostream &out, int id) { if (elems.count(id)) return elems[id]; string keyname = ELEM_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for elem id %d\n", id); + DEBUG(control,out).print("creating new persistent key for elem id {}\n", id); elems.emplace(id, World::GetPersistentSiteData(keyname, true)); return elems[id]; } static void remove_elem_config(color_ostream &out, int id) { if (!elems.count(id)) return; - DEBUG(control,out).print("removing persistent key for elem id %d\n", id); + DEBUG(control,out).print("removing persistent key for elem id {}\n", id); World::DeletePersistentData(elems[id]); elems.erase(id); } @@ -82,7 +82,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -95,19 +95,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -190,7 +190,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // TODO: logic that runs every CYCLE_TICKS ticks } diff --git a/plugins/examples/simple_command_example.cpp b/plugins/examples/simple_command_example.cpp index ea45b18d47..d708bc222f 100644 --- a/plugins/examples/simple_command_example.cpp +++ b/plugins/examples/simple_command_example.cpp @@ -21,7 +21,7 @@ namespace DFHack { 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); + DEBUG(log,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, diff --git a/plugins/examples/skeleton.cpp b/plugins/examples/skeleton.cpp index fd77ab691a..a6b9e234a3 100644 --- a/plugins/examples/skeleton.cpp +++ b/plugins/examples/skeleton.cpp @@ -66,7 +66,7 @@ static command_result command_callback1(color_ostream &out, vector ¶ // run when the plugin is loaded DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status,out).print("initializing %s\n", plugin_name); + DEBUG(status,out).print("initializing {}\n", plugin_name); // For in-tree plugins, don't use the "usage" parameter of PluginCommand. // Instead, add an .rst file with the same name as the plugin to the @@ -80,7 +80,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { - DEBUG(command,out).print("%s command called with %zu parameters\n", + DEBUG(command,out).print("{} command called with {} parameters\n", plugin_name, parameters.size()); // Return CR_WRONG_USAGE to print out your help text. The help text is diff --git a/plugins/examples/ui_addition_example.cpp b/plugins/examples/ui_addition_example.cpp index bbd3af3deb..95bcd16455 100644 --- a/plugins/examples/ui_addition_example.cpp +++ b/plugins/examples/ui_addition_example.cpp @@ -39,14 +39,14 @@ struct title_version_hook : df::viewscreen_titlest { IMPLEMENT_VMETHOD_INTERPOSE(title_version_hook, render); DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(log,out).print("shutting down %s\n", plugin_name); + DEBUG(log,out).print("shutting down {}\n", plugin_name); INTERPOSE_HOOK(title_version_hook, render).remove(); return CR_OK; } DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) { if (enable != is_enabled) { - DEBUG(log,out).print("%s %s\n", plugin_name, + DEBUG(log,out).print("{} {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (!INTERPOSE_HOOK(title_version_hook, render).apply(enable)) return CR_FAILURE; diff --git a/plugins/export-world-map.cpp b/plugins/export-world-map.cpp new file mode 100644 index 0000000000..880c44497b --- /dev/null +++ b/plugins/export-world-map.cpp @@ -0,0 +1,878 @@ +#include "CoordTemplate.h" +#include "Debug.h" +#include "Error.h" +#include "MiscUtils.h" +#include "PluginLua.h" +#include "PluginManager.h" + +#include "modules/Maps.h" +#include "modules/Translation.h" + +#include "df/coord2d.h" +#include "df/creature_raw.h" +#include "df/entity_raw.h" +#include "df/historical_entity.h" +#include "df/map_block.h" +#include "df/region_map_entry.h" +#include "df/site_map_infost.h" +#include "df/world_data.h" +#include "df/world_landmass.h" +#include "df/world_region_details.h" +#include "df/world_region.h" +#include "df/world_river.h" +#include "df/world.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::string; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("export-world-map"); + +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(exportmap, log, DebugCategory::LINFO); + DBG_DECLARE(exportmap, warning, DebugCategory::LWARNING); +} + +template<> +struct std::hash +{ + std::size_t operator()(const df::coord2d& pos) const noexcept + { + // hashing is easy, if values are smaller than the hash + return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; + } +}; + +// were only dealing with 2D coordinates in this file +using coord = df::coord2d; + +constexpr int wdim = 768; // dimension of a world tile +constexpr int rdim = 48; // dimension of a region tile + +/** + * Takes a range of coordinates interpreted as global region tile coordinates + * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible + * local tile coordinates (negative y-coordinates, 48 map units per region tile) + */ +static void print_path(std::ostream &out, const std::span path) { + auto scale = rdim; + assert(path.size()); + auto print_point = [scale](std::ostream &out, const coord &pos){ + out << scale * pos.x << " " << -scale * pos.y;}; + print_range(out, path, print_point, "(", ",", ")"); +} + +// format various values separated by the separator character (ends with a separator) +static auto print_csv_line(std::ostream &out, char sep, auto ...args) { + ([&]{ out << args << sep; }() ,...); +} + +static command_result do_command(color_ostream &out, vector ¶meters); +static command_result export_regions(color_ostream &out); +static command_result export_sites(color_ostream &out); +static command_result export_rivers(color_ostream &out); +static command_result export_elevation(color_ostream &out); + +struct Config { + bool group_by_world = false; + bool group_by_date = false; +}; + +static Config config{}; + +static auto getWorldFolderName() { + auto& world_name = world->world_data->name; + return fmt::format("{} ({})", + DF2UTF(Translation::translateName(&world_name, true)), + DF2UTF(Translation::translateName(&world_name, false))); +} + +static auto getDateFolderName() { + return fmt::format("{}-{}", + *df::global::cur_year, + *df::global::cur_year_tick / 33600 + 1); +} + +static std::ofstream open_output_file( + const std::filesystem::path& filename, + std::ios_base::openmode mode = std::ios::out | std::ios::trunc) +{ + auto base = Core::getInstance().getConfigPath() / "map-export"; + if (config.group_by_world || config.group_by_date) { + base /= std::filesystem::path(getWorldFolderName()); + } + if (config.group_by_date) { + base /= std::filesystem::path(getDateFolderName()); + } + std::filesystem::create_directories(base); + return std::ofstream(base / filename, mode); +} + + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing {}\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} + +static command_result do_command(color_ostream &out, vector ¶meters) +{ + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()){ + out.printerr("This command requires a world to be loaded\n"); + return CR_WRONG_USAGE; + } + + auto has_param = [¶meters](const string &option) { + return std::ranges::find(parameters, option) != parameters.end(); + }; + + config = {}; + if (has_param("--group-by-world")) { + config.group_by_world = true; + } + if (has_param("--group-by-date")) { + config.group_by_date = true; + } + + const bool run_all = parameters.empty() || has_param("all"); + + auto result = CR_WRONG_USAGE; + const auto run_if_selected = [&](std::string name, command_result(*export_fn)(color_ostream &out)) { + if (result != CR_FAILURE && (run_all || has_param(name))) + { + result = export_fn(out); + } + }; + + run_if_selected("regions", &export_regions); + run_if_selected("sites", &export_sites); + run_if_selected("rivers", &export_rivers); + run_if_selected("elevation", &export_elevation); + + return result; +} + +/********************************************************************** */ +/* Site Export */ +/********************************************************************** */ + + +static command_result export_sites(color_ostream &out) +{ + out.print("exporting sites ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + auto out_file = open_output_file("sites.csv"); + if (!out_file) { + return CR_FAILURE; + } + + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv_line below + constexpr auto headings = std::to_array({ + "site_id", "civ_id", "created_year", "cur_owner_id", "type", + "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + }); + print_range(out_file, headings,"",";",";boundary_wkt\n"); + + #define TRANSLATE_DF_EN(guard, name_object)\ + guard ? DF2UTF(Translation::translateName(&name_object, false)) : "NONE",\ + guard ? DF2UTF(Translation::translateName(&name_object, true)) : "NONE" + + for (auto const site : world->world_data->sites) + { + auto civ = df::historical_entity::find(site->civ_id); + auto owner = df::historical_entity::find(site->cur_owner_id); + + df::creature_raw *race = nullptr; + if (owner){ + race = df::creature_raw::find(owner->race); + if (!race) { + DEBUG(warning, out).print("owner ({}) of site ({}) has undefined race ({})\n", owner->id, site->id, owner->race); + if (civ) + race = df::creature_raw::find(civ->race); + } + } + + print_csv_line(out_file, ';', + // "site_id", "civ_id", "created_year", "cur_owner_id", "type", + site->id, + site->civ_id, + site->created_year, + site->cur_owner_id, + DFHack::Maps::getSiteTypeName(site), + // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + TRANSLATE_DF_EN(true, site->name), + TRANSLATE_DF_EN(civ, civ->name), + TRANSLATE_DF_EN(owner, owner->name), + race ? race->name[2] : "NONE" + ); + const std::array path{ + coord(site->global_min_x, site->global_min_y), + coord(site->global_max_x+1, site->global_min_y), + coord(site->global_max_x+1, site->global_max_y+1), + coord(site->global_min_x, site->global_max_y+1), + coord(site->global_min_x, site->global_min_y) + }; + print_range(out_file, std::array, 1>{path}, print_path , "POLYGON(", ",", ")\n" ); + } + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + + return CR_OK; +} + +/********************************************************************** */ +/* Region Map Export */ +/********************************************************************** */ + +// reverse lex-ordering (topmost leftmost tile is smallest) +bool region_order(coord p1, coord p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); +}; + +enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; + +static constexpr direction turn_left(direction dir) { + return (direction)(((int)dir+1) % 4); +} + +static constexpr direction turn_right(direction dir) { + return (direction)(((int)dir+3) % 4); +} + +static coord as_offset(direction dir) { + switch (dir) { + case direction::North: + return { 0, -1 }; + case direction::West: + return { -1, 0 }; + case direction::South: + return { 0, 1 }; + case direction::East: + return { 1, 0 }; + default: + abort(); + } +} + +static coord advance(coord pos, direction dir) { + return pos + as_offset(dir); +} + + +/** + * Look-ahead relative to the direction of movement: + * + * L ↑ R | L + * - + - → + → + * ↑ | R + */ +static std::pair ahead(const std::vector &component, coord pos, direction dir) { + auto test = [&](int16_t x, int16_t y){ + coord offset{x,y}; + return std::ranges::binary_search(component, pos + offset, region_order); + }; + + switch (dir) { + case direction::North: + return { test(-1,-1), test(0,-1)}; + case direction::West: + return { test(-1,0), test(-1,-1)}; + case direction::South: + return { test(0,0), test(-1, 0)}; + case direction::East: + return { test(0,-1), test(0, 0)}; + default: + abort(); + } +} + +// standard DFS-based connected components algorithm. +static auto connected_components(std::vector ®ion) { + + static std::array directions{ + as_offset(direction::North), + as_offset(direction::West), + as_offset(direction::South), + as_offset(direction::East) + }; + + // component_assignment[i] is the component id of region[i] (0 means unassigned) + std::vector component_assignment; + component_assignment.resize(region.size(),0); + + // (indices of) region tiles in the current component that have been discovered but not yet explored + std::deque agenda; + unsigned int current_component = 0; + + for (size_t i = 0; i < region.size(); ++i) { + if (component_assignment[i]) { + // skip region tiles that have already been assigned a component + continue; + } else { + // start a new component for tiles that haven't been assigned yet + ++current_component; + component_assignment[i] = current_component; + agenda.push_back(i); + } + while(!agenda.empty()) { + auto pos_idx = agenda.front(); agenda.pop_front(); + auto pos = region[pos_idx]; + + for (const auto& offset : directions) { + const auto it = std::ranges::lower_bound(region, pos + offset, region_order); + if (it != region.end() && *it == pos + offset) { + auto n_idx = std::distance(region.begin(), it); + if (component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } + } + } + } + } + + // check that all parts of the region are accounted for + assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + + // distribute region tiles according to their component assignment (preserves region order) + std::vector> components; + components.resize(current_component); + for (size_t i = 0; i < region.size(); ++i) { + components.at(component_assignment.at(i) - 1).push_back(region.at(i)); + } + + return components; +} + + +// create outlines around connected components using a clockwise walk around the perimeter +// (exploits that components do not have inclusions) +static auto create_outlines(std::vector> components) { + std::vector> paths; + for (auto const &component : components) { + + // start at the NW corner of the west-most tile of the northmost row... + auto start = component.at(0); + std::vector path; + path.push_back(start); + + // ... ensuring that a step to the east is a valid clockwise step along the boundary. + auto current_direction = direction::East; + auto current_position = advance(start,current_direction); + + while (current_position != start) + { + auto [left, right] = ahead(component, current_position, current_direction); + if (left && right) { + // in front of a wall: turn left + path.push_back(current_position); + current_direction = turn_left(current_direction); + } + else if (!left && !right) { + // no walls ahead: turn right + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + else if (left && !right) { + // diagonal step: turn right (following the outline of the inclusion) + // this does not seem to happen with the maps currently generated by DF + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + // case !left && right requires no turn; advance the position in all cases + current_position = advance(current_position, current_direction); + } + // close the path + path.push_back(current_position); + paths.push_back(std::move(path)); + path.clear(); + } + assert(paths.size() > 0); + return paths; +} + +static command_result export_regions(color_ostream &out) +{ + out.print("{} / {} region map tiles loaded\n", + world->world_data->midmap_data.region_details.size(), + world->world_data->world_width * world->world_data->world_height + ); + out.print("exporting map ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + auto out_file = open_output_file("regions.csv"); + if (!out_file) { + return CR_FAILURE; + } + + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv_line below + constexpr auto headings = std::to_array({ + "world_x", "world_y", "num_tiles", "num_components", "biome_type", + "region_id", "geo_index", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", + "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity", + "surroundings", "elevation", "reanimating", "has_bogeymen" + }); + print_range(out_file, headings,"",";",";boundary_wkt\n" ); + + /* Preprocessing: cluster region tiles by the world tile used for the biome information */ + + // map world tile coord -> vector of region tiles referencing world title for biome information + std::unordered_map> world_tile_region; + + // iterating over the region details allows the user to do partial map exports + // by manually scrolling on the zoomed embark selection map + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto &world_pos = region_details->pos; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + auto biome_tile = Maps::addRegionBiomeOffset(world_pos, region_details->biome[region_x][region_y]); + world_tile_region[biome_tile].emplace_back(world_pos * 16 + coord(region_x, region_y)); + } + } + } + + // once every region tile has been assigned the world tile it references for its metadata + // everything else can be done in parallel + std::mutex out_mutex; + std::vector out_lines; + out_lines.reserve(world_tile_region.size()); + + std::for_each( + std::execution::par, world_tile_region.begin(), world_tile_region.end(), + [&out_lines,&out_mutex](auto &wtr) + { + auto& [biome_tile, region] = wtr; + + std::ostringstream out_line; + + assert(region.size() > 0); + + // sorting the region provides O(log n) membership test. + std::ranges::sort(region, region_order); + + /** + * Phase I : compute the connected components of the world tile region + * using DFS algorithm. (except for the southern and eastern map edge, + * all world tile regions should have a single component) + */ + + auto components = connected_components(region); + + /* Phase II : create paths by clockwise traversal along the outside of every component */ + /** + * Note: DF uses "picture coordinates" (positive y values go "south") + * while in GIS software positve y values go "north". Thus, [print_path] + * negates the y-coordinates, turning the clockwise traversals into + * counterclockwise traversals as specified by WKT. + * https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry + */ + + auto paths = create_outlines(components); + + /* Phase III: output the CSV line */ + + auto& region_map_entry = world->world_data->region_map[biome_tile.x][biome_tile.y]; + auto world_region = df::world_region::find(region_map_entry.region_id); + auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + + print_csv_line(out_line, ';', + // "world_x", "world_y", "num_tiles", "num_components", "biome_type", + biome_tile.x, + biome_tile.y, + region.size(), + components.size(), + ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)), + // "region_id", "geo_index", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df" + region_map_entry.region_id, + region_map_entry.geo_index, + world_region ? DF2UTF(Translation::translateName(&world_region->name, true)) : "NONE", + world_region ? DF2UTF(Translation::translateName(&world_region->name, false)) : "NONE", + region_map_entry.landmass_id, + landmass ? DF2UTF(Translation::translateName(&landmass->name, true)) : "NONE", + landmass ? DF2UTF(Translation::translateName(&landmass->name, false)) : "NONE", + // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity" + region_map_entry.evilness, + region_map_entry.savagery, + region_map_entry.volcanism, + region_map_entry.drainage, + region_map_entry.temperature, + region_map_entry.vegetation, + region_map_entry.rainfall, + region_map_entry.salinity, + // "surroundings", "elevation", "reanimating", "has_bogeymen" + Maps::describeSurroundings(region_map_entry.savagery, region_map_entry.evilness), + region_map_entry.elevation, + world_region->reanimating, + world_region->has_bogeymen + ); + + // output geometry as WKT + if (paths.size() == 1) { + print_range(out_line, paths, print_path , "POLYGON(", ",", ")\n" ); + } else { + print_range(out_line, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); + } + + std::lock_guard lock(out_mutex); + out_lines.push_back(std::move(out_line)); + }); + + for (const auto& line : out_lines) { + out_file << line.view(); + } + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + return CR_OK; +} + +/********************************************************************** */ +/* River Export */ +/********************************************************************** */ + +struct river_tile { + using polygon_t = std::vector>; + polygon_t polygon; +}; + +/** + * To get reasonably-looking river confluences, we project the centroid of all + * river gates onto the line segments between the river gates and then + * interpolate between the centroid and the projection point. + */ +static void fix_confluence_tiles(river_tile::polygon_t& polygon) +{ + assert(polygon.size() > 4 && polygon.size() % 2 == 0); + + auto centroid = Coord2d( + std::reduce(polygon.begin(), polygon.end(), Coord2d(0, 0), std::plus>()) + ) / static_cast(polygon.size()); + + river_tile::polygon_t inset_polygon; + + for (size_t i = 0; i + 1 < polygon.size(); i += 2) { + auto pair_start = polygon[i]; + auto pair_end = polygon[i + 1]; + auto next_pair_start = polygon[(i + 2) % polygon.size()]; + + inset_polygon.emplace_back(pair_start); + inset_polygon.emplace_back(pair_end); + + auto projection = centroid.project_onto_line( + Coord2d(pair_end), Coord2d(next_pair_start), true + ); + auto inset_point = projection.lerp(centroid, 0.6); + inset_polygon.emplace_back(Coord2d(inset_point)); + } + + polygon = std::move(inset_polygon); +} + + + +struct gate { + int active,min,max; + + static gate get( + const df::world_region_details *const region_details, + int region_x, int region_y, direction dir + ) { + auto& vertical = region_details->rivers_vertical; + auto& horizontal = region_details->rivers_horizontal; + switch (dir) { + case direction::North: + return { + vertical.active[region_x][region_y], + vertical.x_min[region_x][region_y], + vertical.x_max[region_x][region_y] + }; + case direction::West: + return { + horizontal.active[region_x][region_y], + horizontal.y_min[region_x][region_y], + horizontal.y_max[region_x][region_y] + }; + case direction::South: + return { + vertical.active[region_x][region_y+1], + vertical.x_min[region_x][region_y+1], + vertical.x_max[region_x][region_y+1] + }; + case direction::East: + return { + horizontal.active[region_x+1][region_y], + horizontal.y_min[region_x+1][region_y], + horizontal.y_max[region_x+1][region_y] + }; + default: + assert(false); + return {}; + } + } + + bool is_valid() const { + return active != 0 && min != -30000 && max != -30000; + } +}; + + +static bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { + CHECK_NULL_POINTER(region_details); + auto biome_tile = Maps::addRegionBiomeOffset(region_details->pos, region_details->biome[region_x][region_y]); + auto region_map_entry = Maps::getRegionBiome(biome_tile); + CHECK_NULL_POINTER(region_map_entry); + return region_map_entry->elevation >= 100 && !region_map_entry->flags.is_set(df::enums::region_map_entry_flags::is_lake); +} + +static command_result export_rivers(color_ostream &out) +{ + out.print("exporting rivers ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + auto out_file = open_output_file("rivers.csv"); + if (!out_file) { + return CR_FAILURE; + } + + + /** + * In DwarfFortress, a world tile can only have at most one river. As a + * consequence, rivers do not end at a confluence point but they already + * change their name when they enter the world tile containing the + * confluence. Grouping river tiles by world tile allows us to only output + * one multipolygon feature per river. + */ + + // create lookup table for rivers based on world tile coordinates (index into world_data->rivers) + std::unordered_map world_river; + + // assign river end first, so that it can be overridden by proper path elements + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + world_river[river->end_pos] = r_idx; + } + + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + for (size_t i = 0; i < river->path.size(); ++i) { + auto pos = river->path[i]; + world_river[pos] = r_idx; + } + } + + // river idx -> river tiles + std::unordered_map> tile_index; + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto [world_x, world_y] = region_details->pos; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + Coord2d base = { world_x * wdim + region_x * rdim, world_y * wdim + region_y * rdim }; + + auto north = gate::get(region_details, region_x, region_y, direction::North); + auto west = gate::get(region_details, region_x, region_y, direction::West); + auto south = gate::get(region_details, region_x, region_y, direction::South); + auto east = gate::get(region_details, region_x, region_y, direction::East); + + // skip tiles without any gates + if (!(north.is_valid() || west.is_valid() || south.is_valid() || east.is_valid())) + continue; + + // skip any river tiles that are on oceans or lakes + if (!is_land(region_details, region_x, region_y)) + continue; + + river_tile tile; + + if (north.is_valid()) { + tile.polygon.emplace_back(base.x + north.max, base.y); + tile.polygon.emplace_back(base.x + north.min, base.y); + } + if (west.is_valid()) { + tile.polygon.emplace_back(base.x, base.y + west.min); + tile.polygon.emplace_back(base.x, base.y + west.max); + } + if (south.is_valid()) { + tile.polygon.emplace_back(base.x + south.min, base.y + rdim); + tile.polygon.emplace_back(base.x + south.max, base.y + rdim); + } + if (east.is_valid()) { + tile.polygon.emplace_back(base.x + rdim, base.y + east.max); + tile.polygon.emplace_back(base.x + rdim, base.y + east.min); + } + + if (tile.polygon.size() > 4) { + fix_confluence_tiles(tile.polygon); + } + + // locate the river using world coordinates and assign the tile + auto r_idx = world_river.at({world_x, world_y}); + tile_index[r_idx].push_back(std::move(tile)); + } + } + } + + + // generate output + out_file << "name_df;name_en;geometry_wkt\n"; + + for (auto& [r_idx, river_tiles] : tile_index) { + auto river = world->world_data->rivers.at(r_idx); + print_csv_line(out_file, ';', + DF2UTF(Translation::translateName(&river->name, false)), + DF2UTF(Translation::translateName(&river->name, true)) + ); + out_file << "MULTIPOLYGON("; + bool first = true; + for (auto &tile : river_tiles) { + // close the polygon + tile.polygon.emplace_back(*tile.polygon.begin()); + auto print_position = [](std::ostream &out, Coord2d pos) { + out << pos.x << " " << -pos.y; + }; + if (first) { + first = false; + } else { + out_file << ","; + } + print_range(out_file, tile.polygon, print_position, "((", ",", "))"); + } + out_file << ")\n"; + } + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + + return CR_OK; +} + +/********************************************************************** */ +/* Elevation Map Export */ +/********************************************************************** */ + +template +class matrix { + std::size_t ncols; + std::size_t nrows; + std::vector _data; + +public: + matrix(std::size_t cols, std::size_t rows) : ncols(cols), nrows(rows), _data(cols * rows) {}; + + T& operator()(std::size_t col, std::size_t row) { + return _data[row * ncols + col]; + }; + + T* data() { return _data.data(); }; + std::size_t size() { return _data.size(); }; +}; + + +static command_result export_elevation(color_ostream &out) +{ + out.print("exporting elevation ... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + auto data_file = open_output_file("elevation.dat", std::ios::out | std::ios::trunc | std::ios::binary); + auto vrt_file = open_output_file("elevation.vrt"); + if (!data_file || !vrt_file) { + return CR_FAILURE; + } + + auto world_width = world->world_data->world_width * 16; + auto world_height = world->world_data->world_height * 16; + + matrix height_map(world_width, world_height); + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto world_x = region_details->pos.x; + auto world_y = region_details->pos.y; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + auto elevation = region_details->elevation[region_x][region_y]; + height_map(16 * world_x + region_x, 16 * world_y + region_y) = elevation; + } + } + } + + data_file.write(reinterpret_cast(height_map.data()), height_map.size() * sizeof(int16_t)); + + // provide an interpretation for the elevation map consistent with the remaining exports + const std::string vrtTemplate = +R"( +EPSG:3857 +0,48,0,0,0,-48 + + elevation.dat + 0 + 2 + {LINE_OFFSET} + LSB + + +)"; + + auto vrt = std::regex_replace(vrtTemplate, std::regex("\\{WIDTH\\}"), std::to_string(world_width)); + vrt = std::regex_replace(vrt, std::regex("\\{HEIGHT\\}"), std::to_string(world_height)); + vrt = std::regex_replace(vrt, std::regex("\\{LINE_OFFSET\\}"), std::to_string(world_width * sizeof(int16_t))); + vrt_file << vrt; + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in {:2f} s !\n", elapsed_seconds.count()); + + return CR_OK; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(getWorldFolderName), + DFHACK_LUA_FUNCTION(getDateFolderName), + DFHACK_LUA_END +}; diff --git a/plugins/fastdwarf.cpp b/plugins/fastdwarf.cpp index 0d4808db8c..d1a6750a5d 100644 --- a/plugins/fastdwarf.cpp +++ b/plugins/fastdwarf.cpp @@ -37,7 +37,7 @@ enum ConfigValues { static command_result do_command(color_ostream &out, vector & parameters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector & commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( "fastdwarf", @@ -48,7 +48,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vectorpath.dest)) return; - DEBUG(cycle,out).print("teleporting unit %d\n", unit->id); + DEBUG(cycle,out).print("teleporting unit {}\n", unit->id); if (!Units::teleport(unit, unit->path.dest)) return; @@ -150,7 +150,7 @@ static void do_tele(color_ostream &out, df::unit * unit) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // fast mode 2 is handled by DF itself bool is_fast = config.get_int(CONFIG_FAST) == 1; @@ -161,7 +161,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { do_tele(out, unit); if (is_fast) { - DEBUG(cycle,out).print("fastifying unit %d\n", unit->id); + DEBUG(cycle,out).print("fastifying unit {}\n", unit->id); Units::setGroupActionTimers(out, unit, 1, df::unit_action_type_group::All); } }); @@ -176,7 +176,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -186,7 +186,7 @@ static command_result do_command(color_ostream &out, vector & parameter return CR_WRONG_USAGE; if (num_params == 0 || parameters[0] == "status") { - out.print("Current state: fast = %d, teleport = %d.\n", + out.print("Current state: fast = {}, teleport = {}.\n", config.get_int(CONFIG_FAST), config.get_int(CONFIG_TELE)); return CR_OK; @@ -196,11 +196,11 @@ static command_result do_command(color_ostream &out, vector & parameter int tele = num_params >= 2 ? string_to_int(parameters[1]) : 0; if (fast < 0 || fast > 2) { - out.printerr("Invalid value for fast: '%s'", parameters[0].c_str()); + out.printerr("Invalid value for fast: '{}'", parameters[0]); return CR_WRONG_USAGE; } if (tele < 0 || tele > 1) { - out.printerr("Invalid value for tele: '%s'", parameters[1].c_str()); + out.printerr("Invalid value for tele: '{}'", parameters[1]); return CR_WRONG_USAGE; } diff --git a/plugins/filltraffic.cpp b/plugins/filltraffic.cpp index c8b0a209e3..b1fa075483 100644 --- a/plugins/filltraffic.cpp +++ b/plugins/filltraffic.cpp @@ -1,6 +1,7 @@ // Wide-area traffic designation utility. // Flood-fill from cursor or fill entire map. +#include #include //For toupper(). #include //for min(). #include @@ -26,9 +27,10 @@ command_result restrictLiquid(color_ostream &out, std::vector & par command_result restrictIce(color_ostream &out, std::vector & params); //Forward Declarations for Utility Functions +constexpr auto coord_max = std::numeric_limits::max(); command_result setAllMatching(color_ostream &out, checkTile checkProc, DFCoord minCoord = DFCoord(0, 0, 0), - DFCoord maxCoord = DFCoord(0xFFFF, 0xFFFF, 0xFFFF)); + DFCoord maxCoord = DFCoord(coord_max, coord_max, coord_max)); void allHigh(DFCoord coord, MapExtras::MapCache & map); void allNormal(DFCoord coord, MapExtras::MapCache & map); @@ -157,7 +159,7 @@ command_result filltraffic(color_ostream &out, std::vector & params return CR_FAILURE; } - out.print("%d/%d/%d ... FILLING!\n", cx,cy,cz); + out.print("{}/{}/{} ... FILLING!\n", cx,cy,cz); //Naive four-way or six-way flood fill with possible tiles on a stack. stack flood; diff --git a/plugins/fix-occupancy.cpp b/plugins/fix-occupancy.cpp index 7b5a416dbb..5ed962e4d4 100644 --- a/plugins/fix-occupancy.cpp +++ b/plugins/fix-occupancy.cpp @@ -107,8 +107,8 @@ static void scan_building(color_ostream &out, df::building * bld, Expected & exp auto expected_bld = expected.bld(x, y, bld->z); if (bld->isSettingOccupancy() && expected_bld) { if (*expected_bld) { - WARN(log,out).print("Buildings overlap at (%d, %d, %d); please manually remove overlapping building." - " Run ':lua dfhack.gui.revealInDwarfmodeMap(%d, %d, %d, true, true)' to zoom to the tile.\n", + WARN(log,out).print("Buildings overlap at ({}, {}, {}); please manually remove overlapping building." + " Run ':lua dfhack.gui.revealInDwarfmodeMap({}, {}, {}, true, true)' to zoom to the tile.\n", x, y, bld->z, x, y, bld->z); } *expected_bld = bld; @@ -163,7 +163,7 @@ static void normalize_item_vector(color_ostream &out, df::map_block *block, bool prev_id = item_id; } if (needs_sorting) { - INFO(log,out).print("%s item list for map block at (%d, %d, %d)\n", + INFO(log,out).print("{} item list for map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) std::sort(block->items.begin(), block->items.end()); @@ -175,7 +175,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: { // clear building occupancy if there is no building there if (expected_occ.bits.building == df::tile_building_occ::None && block_occ.bits.building != df::tile_building_occ::None) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.building = df::tile_building_occ::None; @@ -189,7 +189,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: if (block_occ.bits.building == df::tile_building_occ::Dynamic) block_occ.bits.building = prev_occ; else if (prev_occ != block_occ.bits.building) { - INFO(log,out).print("%s building occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} building occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (dry_run) block_occ.bits.building = prev_occ; @@ -198,13 +198,13 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear unit occupancy if there are no units there if (!expected_occ.bits.unit && block_occ.bits.unit) { - INFO(log,out).print("%s standing unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} standing unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit = false; } if (!expected_occ.bits.unit_grounded && block_occ.bits.unit_grounded) { - INFO(log,out).print("%s grounded unit occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} grounded unit occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.unit_grounded = false; @@ -212,7 +212,7 @@ static void reconcile_map_tile(color_ostream &out, df::building * bld, const df: // clear item occupancy if there are no items there if (!expected_occ.bits.item && block_occ.bits.item) { - INFO(log,out).print("%s item occupancy at (%d, %d, %d)\n", + INFO(log,out).print("{} item occupancy at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", x, y, z); if (!dry_run) block_occ.bits.item = false; @@ -223,7 +223,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { auto occ = Maps::getTileOccupancy(pos); auto block = Maps::getTileBlock(pos); if (!occ || !block) { - WARN(log,out).print("invalid tile: (%d, %d, %d)\n", pos.x, pos.y, pos.z); + WARN(log,out).print("invalid tile: ({}, {}, {})\n", pos.x, pos.y, pos.z); return; } @@ -264,7 +264,7 @@ static void fix_tile(color_ostream &out, df::coord pos, bool dry_run) { if (expected_bld && expected_occ) reconcile_map_tile(out, *expected_bld, *expected_occ, block->occupancy[pos.x&15][pos.y&15], dry_run, pos.x, pos.y, pos.z); - INFO(log,out).print("verified %zd building(s), %zd unit(s), %zd item(s), 1 map block(s), and 1 map tile(s)\n", + INFO(log,out).print("verified {} building(s), {} unit(s), {} item(s), 1 map block(s), and 1 map tile(s)\n", num_buildings, num_units, num_items); } @@ -273,7 +273,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect if (!expected_items) { if (block_items.size()) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) block_items.resize(0); @@ -282,7 +282,7 @@ static void reconcile_block_items(color_ostream &out, std::set * expect } if (!std::equal(expected_items->begin(), expected_items->end(), block_items.begin(), block_items.end())) { - INFO(log,out).print("%s stale item references in map block at (%d, %d, %d)\n", + INFO(log,out).print("{} stale item references in map block at ({}, {}, {})\n", dry_run ? "would fix" : "fixing", block->map_pos.x, block->map_pos.y, block->map_pos.z); if (!dry_run) { block_items.resize(expected_items->size()); @@ -324,20 +324,20 @@ static void fix_map(color_ostream &out, bool dry_run) { auto expected_occ = expected.occ(x, y, z); auto expected_bld = expected.bld(x, y, z); if (!expected_occ || !expected_bld) { - TRACE(log,out).print("pos out of bounds (%d, %d, %d)\n", x, y, z); + TRACE(log,out).print("pos out of bounds ({}, {}, {})\n", x, y, z); continue; } df::tile_occupancy &block_occ = block->occupancy[xoff][yoff]; if (*expected_bld || (expected_occ->whole & occ_mask) != (block_occ.whole & occ_mask)) { - DEBUG(log,out).print("reconciling occupancy at (%d, %d, %d) (bld=%p, 0x%x ?= 0x%x)\n", - x, y, z, *expected_bld, expected_occ->whole & occ_mask, block_occ.whole & occ_mask); + DEBUG(log,out).print("reconciling occupancy at ({}, {}, {}) (bld={}, 0x{:x} ?= 0x{:x})\n", + x, y, z, static_cast(*expected_bld), expected_occ->whole & occ_mask, block_occ.whole & occ_mask); reconcile_map_tile(out, *expected_bld, *expected_occ, block_occ, dry_run, x, y, z); } } } } - INFO(log,out).print("verified %zd buildings, %zd units, %zd items, %zd map blocks, and %zd map tiles\n", + INFO(log,out).print("verified {} buildings, {} units, {} items, {} map blocks, and {} map tiles\n", world->buildings.all.size(), world->units.active.size(), world->items.other.IN_PLAY.size(), world->map.map_blocks.size(), expected.get_size()); } diff --git a/plugins/fixveins.cpp b/plugins/fixveins.cpp index f87701e0bc..1fc003972e 100644 --- a/plugins/fixveins.cpp +++ b/plugins/fixveins.cpp @@ -92,9 +92,9 @@ command_result df_fixveins (color_ostream &out, vector & parameters) } } if (mineral_removed || feature_removed) - out.print("Removed invalid references from %i mineral inclusion and %i map feature tiles.\n", mineral_removed, feature_removed); + out.print("Removed invalid references from {} mineral inclusion and {} map feature tiles.\n", mineral_removed, feature_removed); if (mineral_added || feature_added) - out.print("Restored missing references to %i mineral inclusion and %i map feature tiles.\n", mineral_added, feature_added); + out.print("Restored missing references to {} mineral inclusion and {} map feature tiles.\n", mineral_added, feature_added); return CR_OK; } diff --git a/plugins/flows.cpp b/plugins/flows.cpp index 8ebf968a0b..c99d21c33b 100644 --- a/plugins/flows.cpp +++ b/plugins/flows.cpp @@ -5,6 +5,7 @@ #include "PluginManager.h" #include "DataDefs.h" +#include "df/global_objects.h" #include "df/world.h" #include "df/map_block.h" #include "df/tile_liquid.h" @@ -46,11 +47,11 @@ command_result df_flows (color_ostream &out, vector & parameters) } } - out.print("Blocks with liquid_1=true: %d\n", flow1); - out.print("Blocks with liquid_2=true: %d\n", flow2); - out.print("Blocks with both: %d\n", flowboth); - out.print("Water tiles: %d\n", water); - out.print("Magma tiles: %d\n", magma); + out.print("Blocks with liquid_1=true: {}\n", flow1); + out.print("Blocks with liquid_2=true: {}\n", flow2); + out.print("Blocks with both: {}\n", flowboth); + out.print("Water tiles: {}\n", water); + out.print("Magma tiles: {}\n", magma); return CR_OK; } diff --git a/plugins/follow.cpp b/plugins/follow.cpp index 5649d6bdfb..109c37823b 100644 --- a/plugins/follow.cpp +++ b/plugins/follow.cpp @@ -150,7 +150,7 @@ command_result follow (color_ostream &out, std::vector & parameter ss << "Unpause to begin following " << world->raws.creatures.all[followedUnit->race]->name[0]; if (followedUnit->name.has_name) ss << " " << followedUnit->name.first_name; ss << ". Simply manually move the view to break the following.\n"; - out.print("%s", ss.str().c_str()); + out.print("{}", ss.str()); } else followedUnit = 0; is_enabled = (followedUnit != NULL); diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index df716831db..290772e87b 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -80,7 +80,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * } else if(!item->isClothing() && !item->isArmorNotClothing()) { - if (verbose) { WARN(log).print("Item %d is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); } return false; } else if (item->getType() != df::enums::item_type::GLOVES && @@ -90,22 +90,22 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * item->getType() != df::enums::item_type::SHOES && !targetBodyPart) { - if (verbose) { WARN(log).print("Item %d is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); } return false; } else if (itemOwner && itemOwner->id != unit->id) { - if (verbose) { WARN(log).print("Item %d is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); } return false; } else if (item->flags.bits.in_inventory) { - if (verbose) { WARN(log).print("Item %d is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } + if (verbose) { WARN(log).print("Item {} is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); } return false; } else if (item->flags.bits.in_job) { - if (verbose) { WARN(log).print("Item %d is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } + if (verbose) { WARN(log).print("Item {} is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); } return false; } @@ -132,45 +132,45 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * else if (bpIndex < unit->body.body_plan->body_parts.size()) { // The current body part is not the one that was specified in the function call, but we can keep searching - if (verbose) { WARN(log).print("Found bodypart %s; not a match; continuing search.\n", currPart->token.c_str()); } + if (verbose) { WARN(log).print("Found bodypart {}; not a match; continuing search.\n", currPart->token); } continue; } else { // The specified body part has not been found, and we've reached the end of the list. Report failure. - if (verbose) { WARN(log).print("The specified body part (%s) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token.c_str()); } + if (verbose) { WARN(log).print("The specified body part ({}) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token); } return false; } - if (verbose) { DEBUG(log).print("Inspecting bodypart %s.\n", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Inspecting bodypart {}.\n", currPart->token); } // Inspect the current bodypart if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_raw_flags::GRASP) && ((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_raw_flags::LEFT)) || (item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_raw_flags::RIGHT)))) { - if (verbose) { DEBUG(log).print("Hand found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Hand found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_raw_flags::HEAD)) { - if (verbose) { DEBUG(log).print("Head found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Head found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_raw_flags::UPPERBODY)) { - if (verbose) { DEBUG(log).print("Upper body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Upper body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_raw_flags::LOWERBODY)) { - if (verbose) { DEBUG(log).print("Lower body found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Lower body found ({}).", currPart->token); } } else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_raw_flags::STANCE)) { - if (verbose) { DEBUG(log).print("Foot found (%s)...", currPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Foot found ({}).", currPart->token); } } else if (targetBodyPart && ignoreRestrictions) { // The BP in question would normally be considered ineligible for equipment. But since it was deliberately specified by the user, we'll proceed anyways. - if (verbose) { DEBUG(log).print("Non-standard bodypart found (%s)...", targetBodyPart->token.c_str()); } + if (verbose) { DEBUG(log).print("Non-standard bodypart found ({}).", targetBodyPart->token); } } else if (targetBodyPart) { @@ -205,7 +205,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * // Collision detected; have we reached the limit? if (++collisions >= multiEquipLimit) { - if (verbose) { WARN(log).print(" but it already carries %d piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } + if (verbose) { WARN(log).print(" but it already carries {} piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); } confirmedBodyPart = NULL; break; } @@ -238,7 +238,7 @@ static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * return false; } - if (!Items::moveToInventory(item, unit, df::unit_inventory_item::Worn, bpIndex)) + if (!Items::moveToInventory(item, unit, df::inv_item_role_type::Worn, bpIndex)) { if (verbose) { WARN(log).print("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); } return false; @@ -382,13 +382,13 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (targetBodyPart->token.compare(targetBodyPartCode) == 0) { // It is indeed a match; exit the loop (while leaving the variable populated) - if (verbose) { INFO(log).print("Matching bodypart (%s) found.\n", targetBodyPart->token.c_str()); } + if (verbose) { INFO(log).print("Matching bodypart ({}) found.\n", targetBodyPart->token); } break; } else { // Not a match; nullify the variable (it will get re-populated on the next pass through the loop) - if (verbose) { WARN(log).print("Bodypart \"%s\" does not match \"%s\".\n", targetBodyPart->token.c_str(), targetBodyPartCode.c_str()); } + if (verbose) { WARN(log).print("Bodypart ({}) does not match ({}).\n", targetBodyPart->token, targetBodyPartCode); } targetBodyPart = NULL; } } @@ -396,7 +396,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (!targetBodyPart) { // Loop iteration is complete but no match was found. - WARN(log).print("The unit does not possess a bodypart of type \"%s\". Please check the spelling or choose a different unit.\n", targetBodyPartCode.c_str()); + WARN(log).print("The unit does not possess a bodypart of type ({}). Please check the spelling or choose a different unit.\n", targetBodyPartCode); return CR_FAILURE; } } @@ -466,7 +466,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) if (itemsEquipped == 0 && !verbose) { WARN(log).print("Some items were found but no equipment changes could be made. Use the /verbose switch to display the reasons for failure.\n"); } - if (itemsEquipped > 0) { INFO(log).print("%d items equipped.\n", itemsEquipped); } + if (itemsEquipped > 0) { INFO(log).print("{} items equipped.\n", itemsEquipped); } // Note: we might expect to recalculate the unit's weight at this point, in order to account for the // added items. In fact, this recalculation occurs automatically during each dwarf's "turn". diff --git a/plugins/generated-creature-renamer.cpp b/plugins/generated-creature-renamer.cpp index b0a00b3f21..7c330e0056 100644 --- a/plugins/generated-creature-renamer.cpp +++ b/plugins/generated-creature-renamer.cpp @@ -3,7 +3,6 @@ #include "Export.h" #include "PluginManager.h" #include "df/world.h" -#include "df/world_raws.h" #include "df/creature_raw.h" #include "df/caste_raw.h" #include "modules/World.h" @@ -191,10 +190,10 @@ command_result list_creatures(color_ostream &out, std::vector & pa auto creatureRaw = world->raws.creatures.all[i]; if (!creatureRaw->flags.is_set(df::enums::creature_raw_flags::GENERATED)) continue; - out.print("%s",creatureRaw->creature_id.c_str()); + out.print("{}",creatureRaw->creature_id); if (detailed) { - out.print("\t%s",creatureRaw->caste[0]->description.c_str()); + out.print("\t{}",creatureRaw->caste[0]->description); } out.print("\n"); } diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 9bd3f6f0d4..3cbb3381c1 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "PluginManager.h" #include "TileTypes.h" @@ -5,7 +7,9 @@ #include "modules/Designations.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Random.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/map_block_column.h" #include "df/material.h" @@ -46,6 +50,77 @@ enum class selectability { Unselected }; +// Trait categories for the --brewable/--edible/--oil/--cloth/--dye filters. +// Brewable and oil-bearing materials are identified by the reaction products +// that the vanilla brewing and pressing reactions require (DRINK_MAT and +// PRESS_LIQUID_MAT respectively). +enum plant_trait { + trait_none = 0, + trait_brewable = 1 << 0, + trait_edible = 1 << 1, + trait_oil = 1 << 2, + trait_cloth = 1 << 3, + trait_dye = 1 << 4, +}; + +static bool materialHasProduct(const df::material *mat, const std::string &name) { + if (!mat) + return false; + for (const auto &id : mat->reaction_product.id) { + if (*id == name) + return true; + } + return false; +} + +static df::material *plantMatDef(const df::plant_raw *plant, int16_t def) { + const DFHack::MaterialInfo mi(plant->material_defs.type[def], plant->material_defs.idx[def]); + return mi.isValid() ? mi.material : nullptr; +} + +static bool growthMatchesTraits(const df::plant_growth *growth, unsigned traits) { + const DFHack::MaterialInfo mat(growth->mat_type, growth->mat_index); + if (!mat.isValid()) + return false; + return ((traits & trait_brewable) && materialHasProduct(mat.material, "DRINK_MAT")) || + ((traits & trait_oil) && materialHasProduct(mat.material, "PRESS_LIQUID_MAT")) || + ((traits & trait_edible) && mat.material->flags.is_set(material_flags::EDIBLE_RAW)) || + ((traits & trait_dye) && mat.material->flags.is_set(material_flags::IS_DYE)); +} + +// Does the plant have a part matching any of the given trait categories? +static bool plantMatchesTraits(const df::plant_raw *plant, unsigned traits) { + if ((traits & trait_cloth) && plant->flags.is_set(plant_raw_flags::THREAD)) + return true; + + df::material *basic = plantMatDef(plant, plant_material_def::basic_mat); + if (basic && + (((traits & trait_brewable) && materialHasProduct(basic, "DRINK_MAT")) || + ((traits & trait_edible) && basic->flags.is_set(material_flags::EDIBLE_RAW)) || + ((traits & trait_dye) && basic->flags.is_set(material_flags::IS_DYE)))) + return true; + + // Seeds and nuts are milled into paste when their material yields a press liquid + if ((traits & trait_oil) && + materialHasProduct(plantMatDef(plant, plant_material_def::seed), "PRESS_LIQUID_MAT")) + return true; + + if (traits & trait_dye) { + for (int16_t def : {plant_material_def::mill, plant_material_def::extract_vial, + plant_material_def::extract_barrel, plant_material_def::extract_still_vial}) { + df::material *mat = plantMatDef(plant, def); + if (mat && mat->flags.is_set(material_flags::IS_DYE)) + return true; + } + } + + for (auto *growth : plant->growths) { + if (growthMatchesTraits(growth, traits)) + return true; + } + return false; +} + // Determination of whether seeds can be collected is somewhat messy: // - Growths of type SEEDS are collected only if they are edible either raw or cooked. // - Growths of type PLANT_GROWTH are collected provided the STOCKPILE_PLANT_GROWTH @@ -76,13 +151,13 @@ enum class selectability { // 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) { - TRACE(log, out).print("analyzing %s\n", plant->id.c_str()); + TRACE(log, out).print("analyzing {}\n", plant->id); 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)) { - DEBUG(log, out).print("%s is a selectable tree\n", plant->id.c_str()); + DEBUG(log, out).print("{} is a selectable tree\n", plant->id); if (farming) { return selectability::Nonselectable; } @@ -91,7 +166,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } } 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()); + DEBUG(log, out).print("{} is a non selectable Grass\n", plant->id); return selectability::Grass; } @@ -103,7 +178,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (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()); + DEBUG(log, out).print("{} is edible\n", plant->id); if (farming) { if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW)) { result = selectability::Selectable; @@ -119,7 +194,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo 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)) { - DEBUG(log, out).print("%s is thread/mill/extract\n", plant->id.c_str()); + DEBUG(log, out).print("{} is thread/mill/extract\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -132,7 +207,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo (basic_mat.material->reaction_product.id.size() > 0 || basic_mat.material->reaction_class.size() > 0)) { - DEBUG(log, out).print("%s has a reaction\n", plant->id.c_str()); + DEBUG(log, out).print("{} has a reaction\n", plant->id); if (farming) { result = selectability::Selectable; } @@ -154,9 +229,10 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo 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 (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]) { + auto& mat = growth_mat.material->reaction_product.material; + for (size_t k = 0; k < mat.mat_type.size(); k++) { + if (mat.mat_type[k] == plant->material_defs.type[plant_material_def::seed] && + mat.mat_index[k] == plant->material_defs.idx[plant_material_def::seed]) { seedSource = true; break; } @@ -166,7 +242,7 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo if (*cur_year_tick >= plant->growths[i]->timing_1 && (plant->growths[i]->timing_2 == -1 || *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()); + DEBUG(log, out).print("{} has an edible seed or a stockpile growth\n", plant->id); if (!farming || seedSource) { return selectability::Selectable; } @@ -200,42 +276,41 @@ selectability selectablePlant(color_ostream& out, const df::plant_raw* plant, bo } if (outOfSeason) { - DEBUG(log, out).print("%s has an out of season growth\n", plant->id.c_str()); + DEBUG(log, out).print("{} has an out of season growth\n", plant->id); return selectability::OutOfSeason; } else { - DEBUG(log, out).print("%s cannot be gathered\n", plant->id.c_str()); + DEBUG(log, out).print("{} cannot be gathered\n", plant->id); return result; } } // Formula for determination of the variance in plant growth maturation time, determined via disassembly. -// The x and y parameters are in tiles relative to the embark. -bool ripe(int32_t x, int32_t y, int32_t start, int32_t end) { - int32_t time = (((435522653 - (((y + 3) * x + 5) * ((y + 7) * y * 400181475 + 289700012))) & 0x3FFFFFFF) % 2000 + *cur_year_tick) % 403200; +// The coordinates are relative to the embark region. +bool ripe(int32_t x, int32_t y, int32_t z, int32_t start, int32_t end) { + DFHack::Random::SplitmixRNG rng((world->map.region_x * 48 + x) + (world->map.region_y * 48 + y) * 10000 + (world->map.region_z + z) * 100000000); + int32_t time = (rng.df_trandom(2000) + *cur_year_tick) % 403200; return time >= start && (end == -1 || time <= 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); - 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); - if (!object_data) { +// Looks in the local creation zone's picked growths vector to see if a matching growth has been marked as picked. +bool picked(const df::plant* plant, int32_t growth_subtype, int32_t growth_density) { + int32_t pos_x = plant->pos.x / 48 + world->map.region_x; + int32_t pos_y = plant->pos.y / 48 + world->map.region_y; + size_t cz_id = pos_x + pos_y * 16 * world->world_data->world_width; + auto cz = df::world_object_data::find(cz_id); + if (!cz) { 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)]; - for (size_t i = 0; i < object_data->picked_growths.x.size(); i++) { - if (object_data->picked_growths.x[i] == plant->pos.x && - object_data->picked_growths.y[i] == plant->pos.y && - object_data->picked_growths.z[i] - column->z_base == plant->pos.z && - object_data->picked_growths.subtype[i] == growth_subtype && - object_data->picked_growths.year[i] == *cur_year) { + for (size_t i = 0; i < cz->picked_growths.x.size(); i++) { + if (cz->picked_growths.x[i] == (plant->pos.x % 48) && + cz->picked_growths.y[i] == (plant->pos.y % 48) && + cz->picked_growths.z[i] == (plant->pos.z + world->map.region_z) && + cz->picked_growths.density[i] >= growth_density && + cz->picked_growths.subtype[i] == growth_subtype && + cz->picked_growths.year[i] == *cur_year) { return true; } } @@ -243,13 +318,15 @@ bool picked(const df::plant* plant, int32_t growth_subtype) { return false; } -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); +bool designate(color_ostream& out, const df::plant* plant, bool farming, bool dry_run) { + TRACE(log, out).print("Attempting to designate {} at ({}, {}, {})\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z); + + auto mark = dry_run ? Designations::canMarkPlant : Designations::markPlant; 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); + return mark(plant); } df::plant_raw* plant_raw = world->raws.plants.all[plant->material]; @@ -257,7 +334,7 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) { - return Designations::markPlant(plant); + return mark(plant); } if (plant_raw->flags.is_set(plant_raw_flags::THREAD) || @@ -266,26 +343,26 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { plant_raw->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || plant_raw->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) { if (!farming) { - return Designations::markPlant(plant); + return mark(plant); } } if (basic_mat.material->reaction_product.id.size() > 0 || basic_mat.material->reaction_class.size() > 0) { if (!farming) { - return Designations::markPlant(plant); + return mark(plant); } } 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); + TRACE(log, out).print("growth item type={}\n", ENUM_AS_STR(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; 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", + TRACE(log, out).print("edible_cooked={} edible_raw={} leaf_mat={}\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::STOCKPILE_PLANT_GROWTH)); @@ -309,9 +386,9 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } 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); + ripe(plant->pos.x, plant->pos.y, plant->pos.z, plant_raw->growths[i]->timing_1, plant_raw->growths[i]->timing_2) && + !picked(plant, i, plant_raw->growths[i]->density)) + return mark(plant); } return false; @@ -322,7 +399,8 @@ command_result df_getplants(color_ostream& out, vector & parameters) { std::vector plantSelections; std::vector collectionCount; set plantNames; - bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false, verbose = false, farming = false; + bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false, verbose = false, farming = false, dry_run = false; + unsigned traits = trait_none; size_t maxCount = 999999; int count = 0; @@ -353,6 +431,18 @@ command_result df_getplants(color_ostream& out, vector & parameters) { verbose = true; else if (parameters[i] == "-f") farming = true; + else if (parameters[i] == "-d" || parameters[i] == "--dry-run") + dry_run = true; + else if (parameters[i] == "--brewable") + traits |= trait_brewable; + else if (parameters[i] == "--edible") + traits |= trait_edible; + else if (parameters[i] == "--oil") + traits |= trait_oil; + else if (parameters[i] == "--cloth") + traits |= trait_cloth; + else if (parameters[i] == "--dye") + traits |= trait_dye; else if (parameters[i] == "-n") { if (parameters.size() > i + 1) { maxCount = atoi(parameters[i + 1].c_str()); @@ -389,30 +479,46 @@ command_result df_getplants(color_ostream& out, vector & parameters) { return CR_WRONG_USAGE; } + std::vector traitMatches(world->raws.plants.all.size(), false); + if (traits != trait_none) { + for (size_t i = 0; i < world->raws.plants.all.size(); i++) + traitMatches[i] = plantMatchesTraits(world->raws.plants.all[i], traits); + } + 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); + if (traits != trait_none && !traitMatches[i]) + plantSelections[i] = selectability::Nonselectable; } else if (plantNames.find(plant->id) != plantNames.end()) { plantNames.erase(plant->id); plantSelections[i] = selectablePlant(out, plant, farming); + if (traits != trait_none && !traitMatches[i] && + (plantSelections[i] == selectability::Selectable || + plantSelections[i] == selectability::OutOfSeason)) + { + out.printerr("{} does not match the specified trait filters\n", plant->id); + plantSelections[i] = selectability::Nonselectable; + continue; + } switch (plantSelections[i]) { case selectability::Grass: - out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); + out.printerr("{} is a grass and cannot be gathered\n", plant->id); break; case selectability::Nonselectable: if (farming) { - out.printerr("%s does not have any parts that can be gathered for seeds for farming\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered for seeds for farming\n", plant->id); } else { - out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str()); + out.printerr("{} does not have any parts that can be gathered\n", plant->id); } break; case selectability::OutOfSeason: - out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str()); + out.printerr("{} is out of season, with nothing that can be gathered now\n", plant->id); break; case selectability::Selectable: @@ -426,7 +532,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { 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()); + out.printerr(" {}", *it); out.printerr("\n"); return CR_FAILURE; } @@ -443,6 +549,8 @@ command_result df_getplants(color_ostream& out, vector & parameters) { 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]; + if (traits != trait_none && !traitMatches[i]) + continue; switch (selectablePlant(out, plant, farming)) { case selectability::Grass: case selectability::Nonselectable: @@ -451,7 +559,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { case selectability::OutOfSeason: { if (!treesonly) { - out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str()); + out.print("* (shrub) {} - {} is out of season\n", plant->id, plant->name); } break; } @@ -462,7 +570,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { (shrubsonly && !plant->flags.is_set(plant_raw_flags::TREE)) || (!treesonly && !shrubsonly)) // 'farming' weeds out trees when determining selectability, so no need to test that explicitly { - out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str()); + out.print("* ({}) {} - {}\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id, plant->name); } break; } @@ -475,52 +583,65 @@ 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]; + for (auto* plant : world->plants.all) + { 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); + auto mat = plant->material; + if (mat < 0 || mat >= int16_t(world->raws.plants.all.size())) + { + WARN(log, out).print("plant with invalid material {} in plant vector", mat); + continue; + } + + TRACE(log, out).print("Examining {} at ({}, {}, {})\n", world->raws.plants.all[mat]->id, plant->pos.x, plant->pos.y, plant->pos.z); int x = plant->pos.x % 16; int y = plant->pos.y % 16; - if (plantSelections[plant->material] == selectability::OutOfSeason || - plantSelections[plant->material] == selectability::Selectable) { + if (plantSelections[mat] == selectability::OutOfSeason || + plantSelections[mat] == selectability::Selectable) + { if (exclude || - plantSelections[plant->material] == selectability::OutOfSeason) + plantSelections[mat] == selectability::OutOfSeason) continue; } - else { + else + { if (!exclude) continue; } + if (traits != trait_none && !traitMatches[mat]) + continue; df::tiletype tt = cur->tiletype[x][y]; - df::tiletype_material mat = tileMaterial(tt); + df::tiletype_material tile_mat = tileMaterial(tt); if ((treesonly || tt != tiletype::Shrub) && ENUM_ATTR(plant_type, is_shrub, plant->type)) continue; - if ((shrubsonly || mat != tiletype_material::TREE) && !ENUM_ATTR(plant_type, is_shrub, plant->type)) + if ((shrubsonly || tile_mat != tiletype_material::TREE) && !ENUM_ATTR(plant_type, is_shrub, plant->type)) continue; if (cur->designation[x][y].bits.hidden) continue; - if (collectionCount[plant->material] >= maxCount) + if (collectionCount[mat] >= maxCount) continue; - if (deselect && Designations::unmarkPlant(plant)) { - collectionCount[plant->material]++; + if (deselect && (dry_run ? Designations::canUnmarkPlant(plant) : Designations::unmarkPlant(plant))) + { + collectionCount[mat]++; ++count; } - 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]++; + if (!deselect && designate(out, plant, farming, dry_run)) + { + DEBUG(log, out).print("Designated {} at ({}, {}, {})\n", world->raws.plants.all[mat]->id, plant->pos.x, plant->pos.y, plant->pos.z); + collectionCount[mat]++; ++count; } } 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("{} {} {} designations.\n", dry_run ? "Would update" : "Updated", collectionCount[i], world->raws.plants.all[i]->id); } out.print("\n"); } - out.print("Updated %d plant designations.\n", (int)count); + out.print("{} {} plant designations.\n", dry_run ? "Would update" : "Updated", count); return CR_OK; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f1..e692043868 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -2,8 +2,10 @@ #include #include +#include "modules/DFSDL.h" #include "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Hotkey.h" #include "Debug.h" #include "LuaTools.h" @@ -40,11 +42,11 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) { } static int cleanupHotkeys(lua_State *) { - DEBUG(log).print("cleaning up old stub keybindings for: %s\n", join_strings(", ", Gui::getCurFocus(true)).c_str()); + DEBUG(log).print("cleaning up old stub keybindings for: {}\n", join_strings(", ", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); + DEBUG(log).print("clearing keybinding: {}\n", keyspec); + Core::getInstance().getHotkeyManager()->removeKeybind(keyspec); }); valid = false; sorted_keys.clear(); @@ -82,61 +84,18 @@ static void add_binding_if_valid(color_ostream &out, const string &sym, const st sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); - DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); - Core::getInstance().AddKeyBinding(keyspec, binding); + DEBUG(log).print("adding keybinding: {} -> {}\n", keyspec, binding); + Core::getInstance().getHotkeyManager()->addKeybind(keyspec, binding); } static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, bool filtermenu) { - DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); - vector valid_keys; - - for (char c = '0'; c <= '9'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (char c = 'A'; c <= 'Z'; c++) { - valid_keys.push_back(string(&c, 1)); - } - - for (int i = 1; i <= 12; i++) { - valid_keys.push_back('F' + int_to_string(i)); - } - - valid_keys.push_back("`"); - - for (int shifted = 0; shifted < 2; shifted++) { - for (int alt = 0; alt < 2; alt++) { - for (int ctrl = 0; ctrl < 2; ctrl++) { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { - string sym; - if (ctrl) sym += "Ctrl-"; - if (alt) sym += "Alt-"; - if (shifted) sym += "Shift-"; - sym += *it; - - auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - string::size_type colon_pos = invoke_cmd->find(":"); - // colons at location 0 are for commands like ":lua" - if (colon_pos == string::npos || colon_pos == 0) { - add_binding_if_valid(out, sym, *invoke_cmd, screen, filtermenu); - } - else { - vector tokens; - split_string(&tokens, *invoke_cmd, ":"); - string focus = tokens[0].substr(1); - if(Gui::matchFocusString(focus)) { - auto cmdline = trim(tokens[1]); - add_binding_if_valid(out, sym, cmdline, screen, filtermenu); - } - } - } - } - } - } + auto active_binds = Core::getInstance().getHotkeyManager()->listActiveKeybinds(); + for (const auto& bind : active_binds) { + string sym = bind.spec.toString(false); + add_binding_if_valid(out, sym, bind.cmdline, screen, filtermenu); } valid = true; @@ -164,10 +123,10 @@ static void list(color_ostream &out) { if (!valid) 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()); + out.print("Valid keybindings for the current focus:\n {}\n", + join_strings("\n", Gui::getCurFocus(true))); std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { - out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); + out.print("{}: {}\n", sym, current_bindings[sym]); }); if (!was_valid) @@ -181,7 +140,7 @@ static bool invoke_command(color_ostream &out, const size_t index) { return false; auto cmd = current_bindings[sorted_keys[index]]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); { Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); @@ -194,11 +153,11 @@ static bool invoke_command(color_ostream &out, const size_t index) { static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { - DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_DEFAULT_COMMAND.c_str()); + DEBUG(log).print("invoking command: '{}'\n", INVOKE_MENU_DEFAULT_COMMAND); return Core::getInstance().runCommand(out, INVOKE_MENU_DEFAULT_COMMAND); } else if (parameters.size() == 2 && parameters[0] == "menu") { string cmd = INVOKE_MENU_BASE_COMMAND + parameters[1]; - DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + DEBUG(log).print("invoking command: '{}'\n", cmd); return Core::getInstance().runCommand(out, cmd); } diff --git a/plugins/infinite-sky.cpp b/plugins/infinite-sky.cpp index 9b41c2e2ca..4977dd3e30 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -8,9 +8,16 @@ #include "modules/Maps.h" #include "modules/World.h" +#include "df/block_column_print_infost.h" #include "df/construction.h" +#include "df/entity_plot_invasion_mapst.h" +#include "df/global_objects.h" +#include "df/historical_entity.h" +#include "df/invasion_info.h" #include "df/map_block.h" #include "df/map_block_column.h" +#include "df/plotinfost.h" +#include "df/plot_invasion_mapst.h" #include "df/world.h" #include "df/z_level_flags.h" @@ -27,6 +34,7 @@ using namespace df::enums; DFHACK_PLUGIN("infinite-sky"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); +REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); namespace DFHack { @@ -61,13 +69,13 @@ void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; DEBUG(control, out) - .print("%s from the API; persisting\n", + .print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); @@ -79,7 +87,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } } else { DEBUG(control, out) - .print("%s from the API, but already %s; no action\n", + .print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -102,7 +110,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { plugin_enable(out, true); } DEBUG(control, out) - .print("loading persisted enabled state: %s\n", + .print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; } @@ -112,7 +120,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { DEBUG(control, out) - .print("world unloaded; disabling %s\n", plugin_name); + .print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; cleanup(); } @@ -129,98 +137,10 @@ static void constructionEventHandler(color_ostream &out, void *ptr) { doInfiniteSky(out, 1); } -void doInfiniteSky(color_ostream& out, int32_t howMany) { - int32_t z_count_block = world->map.z_count_block; - df::map_block ****block_index = world->map.block_index; - - cuboid last_air_layer( - 0, 0, world->map.z_count_block - 1, - world->map.x_count_block - 1, world->map.y_count_block - 1, world->map.z_count_block - 1); - - last_air_layer.forCoord([&](df::coord bpos) { - // Allocate a new block column and copy over data from the old - df::map_block **blockColumn = - new df::map_block *[z_count_block + howMany]; - memcpy(blockColumn, block_index[bpos.x][bpos.y], - z_count_block * sizeof(df::map_block *)); - delete[] block_index[bpos.x][bpos.y]; - block_index[bpos.x][bpos.y] = blockColumn; - - df::map_block *last_air_block = blockColumn[bpos.z]; - for (int32_t count = 0; count < howMany; count++) { - df::map_block *air_block = new df::map_block(); - std::fill(&air_block->tiletype[0][0], - &air_block->tiletype[0][0] + (16 * 16), - df::tiletype::OpenSpace); - - // Set block positions properly (based on prior air layer) - air_block->map_pos = last_air_block->map_pos; - air_block->map_pos.z += count + 1; - air_block->region_pos = last_air_block->region_pos; - - // Copy other potentially important metadata from prior air - // layer - std::memcpy(air_block->lighting, last_air_block->lighting, - sizeof(air_block->lighting)); - std::memcpy(air_block->temperature_1, last_air_block->temperature_1, - sizeof(air_block->temperature_1)); - std::memcpy(air_block->temperature_2, last_air_block->temperature_2, - sizeof(air_block->temperature_2)); - std::memcpy(air_block->region_offset, last_air_block->region_offset, - sizeof(air_block->region_offset)); - - // Create tile designations to inform lighting and - // outside markers - df::tile_designation designation{}; - designation.bits.light = true; - designation.bits.outside = true; - std::fill(&air_block->designation[0][0], - &air_block->designation[0][0] + (16 * 16), designation); - - blockColumn[z_count_block + count] = air_block; - world->map.map_blocks.push_back(air_block); - - // deal with map_block_column stuff even though it'd probably be - // fine - df::map_block_column *column = - world->map.column_index[bpos.x][bpos.y]; - if (!column) { - DEBUG(cycle, out) - .print("%s, line %d: column is null (%d, %d).\n", __FILE__, - __LINE__, bpos.x, bpos.y); - continue; - } - df::map_block_column::T_unmined_glyphs *glyphs = - new df::map_block_column::T_unmined_glyphs; - glyphs->x[0] = 0; - glyphs->x[1] = 1; - glyphs->x[2] = 2; - glyphs->x[3] = 3; - glyphs->y[0] = 0; - glyphs->y[1] = 0; - glyphs->y[2] = 0; - glyphs->y[3] = 0; - glyphs->tile[0] = 'e'; - glyphs->tile[1] = 'x'; - glyphs->tile[2] = 'p'; - glyphs->tile[3] = '^'; - column->unmined_glyphs.push_back(glyphs); - } - return true; - }); - - // Update global z level flags - df::z_level_flags *flags = new df::z_level_flags[z_count_block + howMany]; - memcpy(flags, world->map_extras.z_level_flags, - z_count_block * sizeof(df::z_level_flags)); - for (int32_t count = 0; count < howMany; count++) { - flags[z_count_block + count].whole = 0; - flags[z_count_block + count].bits.update = 1; - } - world->map.z_count_block += howMany; - world->map.z_count += howMany; - delete[] world->map_extras.z_level_flags; - world->map_extras.z_level_flags = flags; + +void doInfiniteSky(color_ostream& out, int32_t quantity) +{ + Maps::addBlockColumns(world->map.z_count_block + quantity); } struct infinitesky_options { @@ -242,7 +162,7 @@ struct_identity infinitesky_options::_identity{sizeof(infinitesky_options), &df: command_result infiniteSky(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -254,11 +174,11 @@ command_result infiniteSky(color_ostream &out, return CR_WRONG_USAGE; if (opts.n > 0) { - out.print("Infinite-sky: creating %d new z-level%s of sky.\n", opts.n, + out.print("Infinite-sky: creating {} new z-level{} of sky.\n", opts.n, opts.n == 1 ? "" : "s"); doInfiniteSky(out, opts.n); } else { - out.print("Construction monitoring is %s.\n", + out.print("Construction monitoring is {}.\n", is_enabled ? "enabled" : "disabled"); } return CR_OK; diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp index 37912d8429..c32b9adf22 100644 --- a/plugins/jobutils.cpp +++ b/plugins/jobutils.cpp @@ -100,8 +100,8 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!new_mat.isValid() || new_mat.type != 0) { - out.printerr("New job material isn't inorganic: %s\n", - new_mat.toString().c_str()); + out.printerr("New job material isn't inorganic: {}\n", + new_mat.toString()); return CR_FAILURE; } @@ -109,22 +109,22 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (!cur_mat.isValid() || cur_mat.type != 0) { - out.printerr("Current job material isn't inorganic: %s\n", - cur_mat.toString().c_str()); + out.printerr("Current job material isn't inorganic: {}\n", + cur_mat.toString()); return CR_FAILURE; } df::craft_material_class old_class = cur_mat.getCraftClass(); if (old_class == craft_material_class::None) { - out.printerr("Unexpected current material type: %s\n", - cur_mat.toString().c_str()); + out.printerr("Unexpected current material type: {}\n", + cur_mat.toString()); return CR_FAILURE; } if (new_mat.getCraftClass() != old_class) { - out.printerr("New material %s does not satisfy requirement: %s\n", - new_mat.toString().c_str(), ENUM_KEY_STR(craft_material_class, old_class).c_str()); + out.printerr("New material {} does not satisfy requirement: {}\n", + new_mat.toString(), ENUM_KEY_STR(craft_material_class, old_class)); return CR_FAILURE; } @@ -135,15 +135,15 @@ static command_result job_material_in_job(color_ostream &out, MaterialInfo &new_ if (item_mat != cur_mat) { - out.printerr("Job item %zu has different material: %s\n", - i, item_mat.toString().c_str()); + out.printerr("Job item {} has different material: {}\n", + i, item_mat.toString()); return CR_FAILURE; } if (!new_mat.matches(*item)) { - out.printerr("Job item %zu requirements not satisfied by %s.\n", - i, new_mat.toString().c_str()); + out.printerr("Job item {} requirements not satisfied by {}.\n", + i, new_mat.toString()); return CR_FAILURE; } } @@ -212,7 +212,7 @@ static command_result job_material_in_build(color_ostream &out, MaterialInfo &ne } } - out.printerr("Could not find material in list: %s\n", new_mat.toString().c_str()); + out.printerr("Could not find material in list: {}\n", new_mat.toString()); return CR_FAILURE; } @@ -222,7 +222,7 @@ static command_result job_material(color_ostream &out, vector & paramet if (parameters.size() == 1) { if (!new_mat.find(parameters[0])) { - out.printerr("Could not find material: %s\n", parameters[0].c_str()); + out.printerr("Could not find material: {}\n", parameters[0]); return CR_WRONG_USAGE; } } @@ -253,7 +253,7 @@ static command_result job_duplicate(color_ostream &out, vector & parame job->job_type != job_type::CollectSand && job->job_type != job_type::CollectClay)) { - out.printerr("Cannot duplicate job %s\n", ENUM_KEY_STR(job_type,job->job_type).c_str()); + out.printerr("Cannot duplicate job {}\n", ENUM_KEY_STR(job_type,job->job_type)); return CR_FAILURE; } diff --git a/plugins/lair.cpp b/plugins/lair.cpp index ac5b88c4f9..dcfe3863fb 100644 --- a/plugins/lair.cpp +++ b/plugins/lair.cpp @@ -3,6 +3,7 @@ #include "modules/Maps.h" +#include "df/global_objects.h" #include "df/map_block.h" #include "df/world.h" diff --git a/plugins/liquids.cpp b/plugins/liquids.cpp index a814bbd5b0..ae0b0ef6c3 100644 --- a/plugins/liquids.cpp +++ b/plugins/liquids.cpp @@ -58,7 +58,8 @@ using namespace df::enums; DFHACK_PLUGIN("liquids"); REQUIRE_GLOBAL(world); -static const char * HISTORY_FILE = "dfhack-config/liquids.history"; +auto constexpr HISTORY_FILE = "liquids.history"; + CommandHistory liquids_hist; command_result df_liquids (color_ostream &out, vector & parameters); @@ -66,7 +67,7 @@ command_result df_liquids_here (color_ostream &out, vector & parameters DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - liquids_hist.load(HISTORY_FILE); + liquids_hist.load(DFHack::Core::getInstance().getConfigPath() / HISTORY_FILE); commands.push_back(PluginCommand( "liquids", "Place magma, water or obsidian.", @@ -82,7 +83,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector is_valid()) + return; + if (!block->Allocate()) + return; + for (int x = 0; x < 16; x++) + for (int y = 0; y < 16; y++) + block->setTiletypeAt(df::coord2d(x, y), tiletype::OpenSpace); +} + +// ensure the target map blocks exist so that liquids and obsidian can be +// placed in unallocated sky blocks +static void ensure_brush_blocks(MapExtras::MapCache &mcache, + const OperationMode &mode, const DFCoord &cursor) +{ + switch (mode.brush) + { + case B_POINT: + case B_BLOCK: + ensure_block(mcache, cursor); + break; + case B_RANGE: + { + // RectangleBrush paints the size-sized box extending + // forward from the cursor; note that z stays a map level + // so only x and y are converted to block units + DFCoord lo = Maps::getTileBlockCoord(cursor); + DFCoord hi = Maps::getTileBlockCoord(cursor + mode.size - DFCoord(1,1,1)); + for (int32_t z = cursor.z; z < cursor.z + mode.size.z; z++) + for (int32_t x = lo.x; x <= hi.x; x++) + for (int32_t y = lo.y; y <= hi.y; y++) + ensure_block(mcache, Maps::getBlockOrigin(DFCoord(x, y, z))); + } + break; + case B_COLUMN: + // ensure blocks upward while the column stays open, mirroring + // ColumnBrush's stopping condition + for (int32_t z = cursor.z; z < (int32_t)mcache.maxZ(); z++) + { + DFCoord pos(cursor.x, cursor.y, z); + ensure_block(mcache, pos); + df::tiletype tt = mcache.tiletypeAt(pos); + if (!DFHack::LowPassable(tt) && + !(z == cursor.z && DFHack::HighPassable(tt))) + break; + } + break; + case B_FLOOD: + // flood fill only travels through water tiles, which unallocated + // blocks can never contain + break; + } +} + static void print_prompt(std::ostream &str, OperationMode &cur_mode) { str <<"[" << paint_mode_name[cur_mode.paint] << ":" << brush_name[cur_mode.brush]; @@ -445,6 +504,11 @@ command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, d } MapCache mcache; + + // allocate any unallocated target blocks so that liquids and obsidian + // can be placed in the sky + ensure_brush_blocks(mcache, cur_mode, cursor); + coord_vec all_tiles = brush->points(mcache,cursor); // Force the game to recompute its walkability cache @@ -464,6 +528,7 @@ command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, d des.bits.flow_size = 0; des.bits.flow_forbid = false; mcache.setDesignationAt(*iter, des); + mcache.propagateVerticalFlags(*iter); iter ++; } break; @@ -474,6 +539,7 @@ command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, d while (iter != all_tiles.end()) { mcache.setTiletypeAt(*iter, findRandomVariant(tiletype::LavaFloor1)); + mcache.propagateVerticalFlags(*iter); iter ++; } break; @@ -493,9 +559,10 @@ command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, d mcache.setTemp2At(*iter,10015); mcache.setDesignationAt(*iter,a); - Block * b = mcache.BlockAt((*iter)/16); + Block * b = mcache.BlockAtTile(*iter); b->enableBlockUpdates(true); + mcache.propagateVerticalFlags(*iter); iter++; } break; @@ -523,9 +590,8 @@ command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, d while (iter != all_tiles.end()) { DFHack::DFCoord current = *iter; // current tile coord - DFHack::DFCoord curblock = current /16; // current block coord // check if the block is actually there - auto block = mcache.BlockAt(curblock); + auto block = mcache.BlockAtTile(current); if(!block) { iter ++; diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 52b9bf43a4..2358535621 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -10,6 +10,7 @@ #include "modules/World.h" #include "df/building.h" +#include "df/buildingitemst.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" #include "df/caravan_state.h" @@ -58,15 +59,21 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_FORBID = 6, }; +enum StockpileConfigForbidValues { + STOCKPILE_CONFIG_FORBID_OFF = 0, + STOCKPILE_CONFIG_FORBID_FORBID = 1, + STOCKPILE_CONFIG_FORBID_CLAIM = 2, +}; + static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { - TRACE(control, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); + TRACE(control, out).print("ensuring stockpile config stockpile_number={}\n", stockpile_number); if (watched_stockpiles.count(stockpile_number)) { TRACE(control, out).print("stockpile exists in watched_stockpiles\n"); return watched_stockpiles[stockpile_number]; } string keyname = CONFIG_KEY_PREFIX + int_to_string(stockpile_number); - DEBUG(control, out).print("creating new persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("creating new persistent key for stockpile {}\n", stockpile_number); watched_stockpiles.emplace(stockpile_number, World::GetPersistentSiteData(keyname, true)); PersistentDataItem& c = watched_stockpiles[stockpile_number]; c.set_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER, stockpile_number); @@ -75,14 +82,14 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock c.set_bool(STOCKPILE_CONFIG_DUMP, false); c.set_bool(STOCKPILE_CONFIG_TRAIN, false); c.set_bool(STOCKPILE_CONFIG_MELT_MASTERWORKS, false); - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); return c; } static void remove_stockpile_config(color_ostream& out, int stockpile_number) { if (!watched_stockpiles.count(stockpile_number)) return; - DEBUG(control, out).print("removing persistent key for stockpile %d\n", stockpile_number); + DEBUG(control, out).print("removing persistent key for stockpile {}\n", stockpile_number); World::DeletePersistentData(watched_stockpiles[stockpile_number]); watched_stockpiles.erase(stockpile_number); } @@ -98,7 +105,7 @@ static void do_cycle(color_ostream& out, static void logistics_cycle(color_ostream &out, bool quiet); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(control, out).print("initializing %s\n", plugin_name); + DEBUG(control, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -110,7 +117,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector 0))) { + !(c.get_int(STOCKPILE_CONFIG_FORBID) > STOCKPILE_CONFIG_FORBID_OFF))) { to_remove.push_back(stockpile_number); continue; } @@ -179,7 +186,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (c.key() == CONFIG_KEY) continue; if (c.get_int(STOCKPILE_CONFIG_FORBID) == -1) // remove this once saves from 51.01 are no longer compatible - c.set_int(STOCKPILE_CONFIG_FORBID, 0); + c.set_int(STOCKPILE_CONFIG_FORBID, STOCKPILE_CONFIG_FORBID_OFF); watched_stockpiles.emplace(c.get_int(STOCKPILE_CONFIG_STOCKPILE_NUMBER), c); } migrate_old_keys(out); @@ -198,7 +205,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -438,12 +445,12 @@ static const struct BadFlags { } bad_flags; static void scan_item(color_ostream &out, df::item *item, StockProcessor &processor) { - DEBUG(cycle,out).print("scan_item [%s] item_id=%d\n", processor.name.c_str(), item->id); + DEBUG(cycle,out).print("scan_item [{}] item_id={}\n", processor.name, item->id); if (DBG_NAME(cycle).isEnabled(DebugCategory::LTRACE)) { string name = ""; item->getItemDescription(&name, 0); - TRACE(cycle,out).print("item: %s\n", name.c_str()); + TRACE(cycle,out).print("item: {}\n", name); } if (processor.is_designated(out, item)) { @@ -532,7 +539,7 @@ static void train_partials(color_ostream& out, int32_t& train_count) { continue; if (Units::assignTrainer(unit)) { - DEBUG(cycle,out).print("assigned trainer to unit %d\n", unit->id); + DEBUG(cycle,out).print("assigned trainer to unit {}\n", unit->id); ++train_count; } } @@ -542,7 +549,7 @@ static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_count, int32_t& dump_count, int32_t& train_count, int32_t& forbid_count, int32_t& claim_count) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); cycle_timestamp = world->frame_counter; ProcessorStats melt_stats, trade_stats, dump_stats, train_stats, forbid_stats, claim_stats; @@ -559,8 +566,8 @@ static void do_cycle(color_ostream& out, bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); @@ -586,7 +593,7 @@ static void do_cycle(color_ostream& out, train_partials(out, train_count); } - TRACE(cycle,out).print("exit %s do_cycle\n", plugin_name); + TRACE(cycle,out).print("exit {} do_cycle\n", plugin_name); } ///////////////////////////////////////////////////// @@ -644,8 +651,8 @@ static int logistics_getStockpileData(lua_State *L) { bool trade = c.get_bool(STOCKPILE_CONFIG_TRADE); bool dump = c.get_bool(STOCKPILE_CONFIG_DUMP); bool train = c.get_bool(STOCKPILE_CONFIG_TRAIN); - bool forbid = 1 == c.get_int(STOCKPILE_CONFIG_FORBID); - bool claim = 2 == c.get_int(STOCKPILE_CONFIG_FORBID); + bool forbid = STOCKPILE_CONFIG_FORBID_FORBID == c.get_int(STOCKPILE_CONFIG_FORBID); + bool claim = STOCKPILE_CONFIG_FORBID_CLAIM == c.get_int(STOCKPILE_CONFIG_FORBID); unordered_map sconfig; sconfig.emplace("melt", melt ? "true" : "false"); @@ -666,21 +673,21 @@ static int logistics_getStockpileData(lua_State *L) { } static void logistics_cycle(color_ostream &out, bool quiet = false) { - DEBUG(control, out).print("entering logistics_cycle%s\n", quiet ? " (quiet)" : ""); + DEBUG(control, out).print("entering logistics_cycle{}\n", quiet ? " (quiet)" : ""); int32_t melt_count = 0, trade_count = 0, dump_count = 0, train_count = 0, forbid_count = 0, claim_count = 0; do_cycle(out, melt_count, trade_count, dump_count, train_count, forbid_count, claim_count); if (0 < melt_count || !quiet) - out.print("logistics: designated %d item%s for melting\n", melt_count, (melt_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for melting\n", melt_count, (melt_count == 1) ? "" : "s"); if (0 < trade_count || !quiet) - out.print("logistics: designated %d item%s for trading\n", trade_count, (trade_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for trading\n", trade_count, (trade_count == 1) ? "" : "s"); if (0 < dump_count || !quiet) - out.print("logistics: designated %d item%s for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} for dumping\n", dump_count, (dump_count == 1) ? "" : "s"); if (0 < train_count || !quiet) - out.print("logistics: designated %d animal%s for training\n", train_count, (train_count == 1) ? "" : "s"); + out.print("logistics: designated {} animal{} for training\n", train_count, (train_count == 1) ? "" : "s"); if (0 < forbid_count || !quiet) - out.print("logistics: designated %d item%s forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); + out.print("logistics: designated {} item{} forbidden\n", forbid_count, (forbid_count == 1) ? "" : "s"); if (0 < claim_count || !quiet) - out.print("logistics: claimed %d forbidden item%s\n", claim_count, (claim_count == 1) ? "" : "s"); + out.print("logistics: claimed {} forbidden item{} \n", claim_count, (claim_count == 1) ? "" : "s"); } static void find_stockpiles(lua_State *L, int idx, @@ -718,7 +725,7 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); - stockpile_config.emplace("forbid", 0); + stockpile_config.emplace("forbid", STOCKPILE_CONFIG_FORBID_OFF); } return stockpile_config; } @@ -749,11 +756,11 @@ static void logistics_setStockpileConfig(color_ostream& out, int stockpile_numbe bool dump, bool train, int forbid, bool melt_masterworks) { DEBUG(control, out).print("entering logistics_setStockpileConfig\n"); - DEBUG(control, out).print("stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d, forbid=%d, melt_masterworks=%d\n", + DEBUG(control, out).print("stockpile_number={}, melt={}, trade={}, dump={}, train={}, forbid={}, melt_masterworks={}\n", stockpile_number, melt, trade, dump, train, forbid, melt_masterworks); if (!find_stockpile(stockpile_number)) { - out.printerr("invalid stockpile number: %d\n", stockpile_number); + out.printerr("invalid stockpile number: {}\n", stockpile_number); return; } @@ -836,8 +843,8 @@ static int logistics_getGlobalCounts(lua_State *L) { } static bool logistics_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control, out).print("entering logistics_setFeature (enabled=%d, feature=%s)\n", - enabled, feature.c_str()); + DEBUG(control, out).print("entering logistics_setFeature (enabled={}, feature={})\n", + enabled, feature); if (feature != "autoretrain") return false; config.set_bool(CONFIG_TRAIN_PARTIAL, enabled); @@ -847,7 +854,7 @@ static bool logistics_setFeature(color_ostream &out, bool enabled, string featur } static bool logistics_getFeature(color_ostream &out, string feature) { - DEBUG(control, out).print("entering logistics_getFeature (feature=%s)\n", feature.c_str()); + DEBUG(control, out).print("entering logistics_getFeature (feature={})\n", feature); if (feature != "autoretrain") return false; return config.get_bool(CONFIG_TRAIN_PARTIAL); diff --git a/plugins/lua/aquifer.lua b/plugins/lua/aquifer.lua index d5255ccd33..e9eab45cb8 100644 --- a/plugins/lua/aquifer.lua +++ b/plugins/lua/aquifer.lua @@ -84,7 +84,7 @@ function parse_commandline(args) {'z', 'cur-zlevel', handler=function() opts.curz = true end}, }) - if help or positionals[1] == 'help' then + if opts.help or positionals[1] == 'help' then print(dfhack.script_help()) return false end @@ -135,11 +135,11 @@ function parse_commandline(args) local modified = 0 if action == 'drain' then - modified = aquifer_drain(aq_type or 'all', pos1, pos2, opts.skip_top, opts.levels, opts.leaky) + modified = aquifer_drain(aq_type or 'all', pos1, pos2, opts.skip_top, opts.levels, opts.leaky, opts.all) elseif action == 'convert' then - modified = aquifer_convert(aq_type, pos1, pos2, opts.skip_top, opts.levels, opts.leaky) + modified = aquifer_convert(aq_type, pos1, pos2, opts.skip_top, opts.levels, opts.leaky, opts.all) elseif action == 'add' then - modified = aquifer_add(aq_type, pos1, pos2, opts.skip_top, opts.levels, opts.leaky) + modified = aquifer_add(aq_type, pos1, pos2, opts.skip_top, opts.levels, opts.leaky, opts.all) else qerror(('invalid action: %s'):format(action)) end diff --git a/plugins/lua/autochop.lua b/plugins/lua/autochop.lua index a1a7368821..768bee61a4 100644 --- a/plugins/lua/autochop.lua +++ b/plugins/lua/autochop.lua @@ -38,6 +38,9 @@ local function do_set_burrow_config(var_name, val, burrows) end for _,bspec in ipairs(burrows) do local config = autochop_getBurrowConfig(bspec) + if not config then + qerror(('burrow not found: %s'):format(bspec)) + end config[var_name] = val autochop_setBurrowConfig(config.id, config.chop, config.clearcut, config.protect_brewable, config.protect_edible, diff --git a/plugins/lua/autofarm.lua b/plugins/lua/autofarm.lua new file mode 100644 index 0000000000..1278aa99dd --- /dev/null +++ b/plugins/lua/autofarm.lua @@ -0,0 +1,184 @@ +local _ENV = mkmodule('plugins.autofarm') + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local function get_farm() + local bld = dfhack.gui.getSelectedBuilding(true) + if bld and bld:getType() == df.building_type.FarmPlot then + return bld + end +end + +local function farm_is_managed(farm) + return farm and autofarm_isFarmExcluded and + not autofarm_isFarmExcluded(farm.id) +end + +-------------------------------- +-- FarmControlsOverlay +-- +-- toggle strip shown on the farm plot info sheet: enables/disables autofarm +-- globally, marks the inspected farm as managed or manually controlled, and +-- provides a link to the autofarm configuration GUI + +FarmControlsOverlay = defclass(FarmControlsOverlay, overlay.OverlayWidget) +FarmControlsOverlay.ATTRS{ + desc='Adds autofarm controls to the farm plot info sheet.', + default_pos={x=-43, y=13}, + version=3, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/FarmPlot', + frame={w=50, h=2}, + frame_background=gui.CLEAR_PEN, +} + +function FarmControlsOverlay:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id='autofarm_enabled', + frame={t=0, l=0}, + label='autofarm is', + key='CUSTOM_CTRL_A', + options={{value=true, label='enabled', pen=COLOR_GREEN}, + {value=false, label='disabled', pen=COLOR_RED}}, + on_change=function(val) setEnabled(val) end, + }, + widgets.ToggleHotkeyLabel{ + view_id='farm_managed', + frame={t=1, l=0}, + label='this farm is', + key='CUSTOM_CTRL_M', + enabled=function() return isEnabled() end, + options={{value=true, label='managed by autofarm', pen=COLOR_GREEN}, + {value=false, label='managed manually', pen=COLOR_YELLOW}}, + on_change=function(val) + local farm = get_farm() + if farm then autofarm_setFarmExcluded(farm.id, not val) end + end, + }, + widgets.TextButton{ + frame={t=0, r=0, w=18, h=1}, + label='configure', + key='CUSTOM_CTRL_S', + on_activate=function() dfhack.run_script('gui/autofarm') end, + }, + } +end + +function FarmControlsOverlay:render(dc) + self.subviews.autofarm_enabled:setOption(isEnabled()) + self.subviews.farm_managed:setOption(farm_is_managed(get_farm())) + FarmControlsOverlay.super.render(self, dc) +end + +-------------------------------- +-- FarmCoverOverlay +-- +-- opaque panel shown over the crop list of farms managed by autofarm. warns +-- the player that manual crop selection is overridden by autofarm and +-- provides links to the autofarm configuration GUI and to per-farm control + +local function get_crop_name(plant_id) + if not plant_id or plant_id < 0 then return 'fallow' end + local raw = df.plant_raw.find(plant_id) + return raw and raw.name or 'unknown' +end + +FarmCoverOverlay = defclass(FarmCoverOverlay, overlay.OverlayWidget) +FarmCoverOverlay.ATTRS{ + desc='Covers the crop list of farm plots managed by autofarm.', + default_pos={x=-40, y=16}, + version=4, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING/FarmPlot', + frame={w=57, h=22}, + frame_style=gui.FRAME_MEDIUM, + frame_background=gui.CLEAR_PEN, + visible=function() + local farm = get_farm() + return isEnabled() and farm_is_managed(farm) and + farm:getBuildStage() == farm:getMaxBuildStage() + end, +} + +function FarmCoverOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=1, l=2}, + text='This farm is managed by autofarm', + text_pen=COLOR_LIGHTGREEN, + }, + widgets.Label{ + view_id='now_planting', + frame={t=2, l=2}, + text='Now planting: ?', + }, + widgets.WrappedLabel{ + frame={t=3, l=2, r=2}, + text_to_wrap='Crops are chosen automatically based on your'.. + ' configured stock thresholds. Manual crop selection'.. + ' will be overridden.', + }, + widgets.WrappedLabel{ + view_id='fallow_note', + frame={t=6, l=2, r=2}, + text_pen=COLOR_YELLOW, + }, + widgets.TextButton{ + frame={b=4, l=2, r=2, h=1}, + label='configure autofarm', + key='CUSTOM_CTRL_S', + on_activate=function() dfhack.run_script('gui/autofarm') end, + }, + widgets.TextButton{ + frame={b=3, l=2, r=2, h=1}, + label='manage this farm manually', + key='CUSTOM_CTRL_M', + on_activate=function() + local farm = get_farm() + if farm then autofarm_setFarmExcluded(farm.id, true) end + end, + }, + } +end + +function FarmCoverOverlay:render(dc) + local farm = get_farm() + if farm then + self.subviews.now_planting:setText{ + 'Now planting: ', + {text=get_crop_name(farm.plant_id[df.global.cur_season]), + pen=COLOR_LIGHTCYAN}, + } + local all_fallow = true + for i=0,3 do + if farm.plant_id[i] >= 0 then + all_fallow = false + break + end + end + self.subviews.fallow_note:setText(all_fallow and + 'All seasons are fallow: no plantable seeds available, or all'.. + ' crops are above their thresholds.' or '') + end + FarmCoverOverlay.super.render(self, dc) +end + +-- swallow mouse input over the covered area so that the underlying crop +-- list cannot be manipulated while autofarm is in control +function FarmCoverOverlay:onInput(keys) + if FarmCoverOverlay.super.onInput(self, keys) then return true end + if (keys._MOUSE_L or keys._MOUSE_R) and self:getMousePos() then + return true + end + return false +end + +OVERLAY_WIDGETS = { + farm_controls=FarmControlsOverlay, + farm_cover=FarmCoverOverlay, +} + +return _ENV diff --git a/plugins/lua/autolabor.lua b/plugins/lua/autolabor.lua index 8cedf0bb19..7f3116291e 100644 --- a/plugins/lua/autolabor.lua +++ b/plugins/lua/autolabor.lua @@ -1,52 +1,6 @@ local _ENV = mkmodule('plugins.autolabor') -local gui = require('gui') -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') - -AutolaborOverlay = defclass(AutolaborOverlay, overlay.OverlayWidget) -AutolaborOverlay.ATTRS{ - desc='Adds information to the work details screen about whether work details are enabled.', - default_pos={x=7,y=-13}, - default_enabled=true, - viewscreens='dwarfmode/Info/LABOR/WORK_DETAILS', - frame={w=29, h=5}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, -} - -function AutolaborOverlay:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - 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={ - 'Any changes made on this', NEWLINE, - 'screen will have no effect.' - }, - }, - } -end - -function AutolaborOverlay:render(dc) - if not df.global.game.external_flag.automatic_professions_disabled then return end - AutolaborOverlay.super.render(self, dc) -end - -OVERLAY_WIDGETS = { - overlay=AutolaborOverlay, -} +-- The labor automation controls are part of the work details overlay in +-- scripts/gui/settings-manager.lua. return _ENV diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index dfe8f4eae6..58e7421c51 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.blueprint') local argparse = require('argparse') +local logistics = require('plugins.logistics') local utils = require('utils') local valid_phase_list = { @@ -9,17 +10,14 @@ local valid_phase_list = { 'construct', 'build', 'place', - -- 'zone', - -- 'query', - -- 'rooms', + 'zone', } valid_phases = utils.invert(valid_phase_list) local meta_phase_list = { 'build', 'place', - -- 'zone', - -- 'query', + 'zone', } meta_phases = utils.invert(meta_phase_list) @@ -206,7 +204,7 @@ end -- returns the name of the output file for the given context function get_filename(opts, phase, ordinal) - local fullname = 'dfhack-config/blueprints/' .. opts.name + local fullname = dfhack.getConfigPath() .. '/blueprints/' .. opts.name local _,_,basename = opts.name:find('([^/]+)/*$') if not basename then -- should not happen since opts.name should already be validated @@ -224,6 +222,21 @@ function get_filename(opts, phase, ordinal) return ('%s-%d-%s.csv'):format(fullname, ordinal, phase) end +function get_logistics_settings(stockpile_number) + local automelt, autotrade, autodump, autotrain, autoforbid, autoclaim = false, false, false, false, false, false + local configs = logistics.logistics_getStockpileConfigs(stockpile_number) + if configs and #configs == 1 then + local config = configs[1] + automelt = config.melt ~= 0 + autotrade = config.trade ~= 0 + autodump = config.dump ~= 0 + autotrain = config.train ~= 0 + autoforbid = config.forbid == 1 + autoclaim = config.forbid == 2 + end + return automelt, autotrade, autodump, autotrain, autoforbid, autoclaim +end + -- compatibility with old exported API. local function do_phase(start_pos, end_pos, name, phase) local width = math.abs(start_pos.x - end_pos.x) + 1 diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d6eb5fb9ae..d094f59c9d 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -59,6 +59,19 @@ function is_suspendmanager_enabled() return require('plugins.suspendmanager').isEnabled() end +-- The flag lives on the job, which exists as soon as the building is +-- designated, so it can be set before the job is unsuspended and it persists +-- with the save like any other job flag. +function setDoNow(bld, val) + if not bld or #bld.jobs == 0 then return false end + bld.jobs[0].flags.do_now = val + return true +end + +function getDoNow(bld) + return bld ~= nil and #bld.jobs > 0 and bld.jobs[0].flags.do_now +end + function get_num_filters(btype, subtype, custom) local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) return filters and #filters or 0 diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 62e1a6c49c..352286f626 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -11,10 +11,37 @@ local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') -config = config or json.open('dfhack-config/buildingplan.json') +config = config or json.open(dfhack.getConfigPath() .. '/buildingplan.json') local uibs = df.global.buildreq +local ITEM_TO_JOB = { + [df.item_type.BED] = 'ConstructBed', [df.item_type.DOOR] = 'ConstructDoor', + [df.item_type.CABINET] = 'ConstructCabinet', [df.item_type.TABLE] = 'ConstructTable', + [df.item_type.CHAIR] = 'ConstructThrone', [df.item_type.BOX] = 'ConstructChest', + [df.item_type.ARMORSTAND] = 'ConstructArmorStand', [df.item_type.WEAPONRACK] = 'ConstructWeaponRack', + [df.item_type.STATUE] = 'ConstructStatue', [df.item_type.COFFIN] = 'ConstructCoffin', + [df.item_type.HATCH_COVER] = 'ConstructHatchCover', [df.item_type.GRATE] = 'ConstructGrate', + [df.item_type.QUERN] = 'ConstructQuern', [df.item_type.MILLSTONE] = 'ConstructMillstone', + [df.item_type.TRACTION_BENCH] = 'ConstructTractionBench', [df.item_type.SLAB] = 'ConstructSlab', + [df.item_type.ANVIL] = 'ForgeAnvil', [df.item_type.WINDOW] = 'MakeWindow', + [df.item_type.CAGE] = 'MakeCage', [df.item_type.BARREL] = 'MakeBarrel', + [df.item_type.BUCKET] = 'MakeBucket', [df.item_type.ANIMALTRAP] = 'MakeAnimalTrap', + [df.item_type.CHAIN] = 'MakeChain', [df.item_type.FLASK] = 'MakeFlask', + [df.item_type.GOBLET] = 'MakeGoblet', [df.item_type.BLOCKS] = 'ConstructBlocks', +} + +local JOB_DEFAULTS = { + ConstructBed='wood', ConstructTractionBench='wood', MakeCage='wood', + MakeBarrel='wood', MakeBucket='wood', MakeAnimalTrap='wood', + ForgeAnvil='iron', MakeChain='iron', MakeFlask='iron', MakeWindow='glass' +} + +local VALID_MAT_CATS = { + wood=true, bone=true, shell=true, horn=true, pearl=true, tooth=true, + leather=true, silk=true, yarn=true, cloth=true, plant=true +} + reset_counts_flag = false editing_filters_flag = false @@ -119,6 +146,10 @@ local function is_construction() return uibs.building_type == df.building_type.Construction end +local function is_siege_engine() + return uibs.building_type == df.building_type.SiegeEngine +end + local function tile_is_construction(pos) local tt = dfhack.maps.getTileType(pos) if not tt then return false end @@ -144,7 +175,7 @@ local function is_interior(bounds, x, y) y ~= bounds.y1 and y ~= bounds.y2 end --- adjusted from CycleHotkeyLabel on the planner panel +-- adjusted from WeaponSpiketrapPanel (HotKey & Slider) on the planner panel local weapon_quantity = 1 local function get_quantity(filter, hollow, bounds) @@ -177,7 +208,8 @@ local function get_quantity(filter, hollow, bounds) end return quantity * count end - return quantity * get_selected_volume(bounds) + -- job item quantities are totals for the whole building, not per-tile + return quantity end local function cur_building_has_no_area() @@ -235,6 +267,7 @@ local direction_panel_types = utils.invert{ df.building_type.WaterWheel, df.building_type.AxleHorizontal, df.building_type.Rollers, + df.building_type.SiegeEngine, } local function has_direction_panel() @@ -243,7 +276,7 @@ local function has_direction_panel() and uibs.building_subtype == df.trap_type.TrackStop) end -local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} +local pressure_plate_panel_frame = {t=4, h=38, w=50, r=28} local function has_pressure_plate_panel() return is_pressure_plate() @@ -405,6 +438,8 @@ function ItemLine:get_item_line_text() end self.note = string.char(192) .. self.note -- character 192 is "└" + self.quantity = quantity + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end @@ -605,7 +640,7 @@ function PlannerOverlay:init() local main_panel = widgets.Panel{ view_id='main', - frame={t=1, l=0, r=0, h=14}, + frame={t=1, l=0, r=0, h=15}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -653,6 +688,43 @@ function PlannerOverlay:init() local buildingplan = require('plugins.buildingplan') + -- WeaponSpiketrapPanel defined outside of main_panel, otherwise addviews breaks -> addviews expects table + local WeaponSpiketrapPanel = defclass(WeaponSpiketrapPanel, widgets.Panel) + WeaponSpiketrapPanel.ATTRS{ + view_id='weapons', + visible=is_weapon_or_spike_trap, + } + + function WeaponSpiketrapPanel:init() + self.options = utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10) + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='weapons_hotkey', + frame={b=5, l=1, w=28}, + key='CUSTOM_T', + key_back='CUSTOM_SHIFT_T', + label='Number of weapons:', + options=self.options, + initial_option=weapon_quantity, + on_change=function(val) + weapon_quantity = val + end + }, + + widgets.Slider{ + view_id='weapons_slider', + frame={b=7, l=4, w=35}, + num_stops=#self.options, + get_idx_fn=function() return weapon_quantity end, + on_change=function(val) + weapon_quantity = val + self.subviews.weapons_hotkey:setOption(val) + end + } + } + end + main_panel:addviews{ widgets.Label{ frame={}, @@ -678,7 +750,7 @@ function PlannerOverlay:init() on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', - frame={b=4, l=1, w=21}, + frame={b=5, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, @@ -689,7 +761,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={b=7, l=1, w=30}, + frame={b=8, l=1, w=30}, key='CUSTOM_R', label='Top stair type: ', visible=is_multi_level_stairs, @@ -701,7 +773,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={b=6, l=1, w=30}, + frame={b=7, l=1, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', visible=is_multi_level_stairs, @@ -713,7 +785,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='stairs_only_subtype', - frame={b=7, l=1, w=30}, + frame={b=8, l=1, w=30}, key='CUSTOM_R', label='Single level stair:', visible=is_single_level_stairs, @@ -723,19 +795,12 @@ function PlannerOverlay:init() {label='Down', value=df.construction_type.DownStair}, }, }, - widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider - view_id='weapons', - frame={b=4, l=1, w=28}, - key='CUSTOM_T', - key_back='CUSTOM_SHIFT_T', - label='Number of weapons:', - visible=is_weapon_or_spike_trap, - options=utils.tabulate(function(i) return {label='('..i..')', value=i, pen=COLOR_YELLOW} end, 1, 10), - on_change=function(val) weapon_quantity = val end, - }, + + WeaponSpiketrapPanel{}, + widgets.ToggleHotkeyLabel { view_id='engraved', - frame={b=4, l=1, w=22}, + frame={b=5, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, @@ -745,7 +810,7 @@ function PlannerOverlay:init() }, widgets.ToggleHotkeyLabel { view_id='empty', - frame={b=4, l=1, w=22}, + frame={b=5, l=1, w=22}, key='CUSTOM_T', label='Empty only:', visible=is_cage, @@ -753,9 +818,32 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) end, }, + widgets.ToggleHotkeyLabel{ + view_id='do_now', + -- b=4 in the left column is the favorites divider row; the first + -- free row of the options block is b=3 in the right column + frame={b=3, l=24, w=25}, + key='CUSTOM_N', + label='Do now:', + initial_option=self.state.do_now or false, + on_change=function(val) + self.state.do_now = val + config:write() + end, + }, widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, subviews={ + widgets.HotkeyLabel{ + frame={b=3, l=1, w=22}, + key='CUSTOM_CTRL_Q', + label='Queue order', + on_activate=function() self:queue_order(self.selected) end, + visible=function() + local item = self.subviews['item'..tostring(self.selected)] + return item and item.available and item.quantity and (item.available < item.quantity) + end + }, widgets.HotkeyLabel{ frame={b=2, l=1, w=22}, key='CUSTOM_F', @@ -836,7 +924,7 @@ function PlannerOverlay:init() local error_panel = widgets.ResizingPanel{ view_id='errors', - frame={t=15, l=0, r=0}, + frame={t=16, l=0, r=0}, frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), @@ -898,7 +986,7 @@ function PlannerOverlay:init() local favorites_panel = widgets.Panel{ view_id='favorites', - frame={t=15, l=0, r=0, h=9}, + frame={t=16, l=0, r=0, h=9}, frame_style=gui.FRAME_INTERIOR_MEDIUM, frame_background=gui.CLEAR_PEN, visible=self:callback('show_favorites'), @@ -935,7 +1023,7 @@ function PlannerOverlay:init() is_selected_fn=make_is_selected_filter('0') }, widgets.CycleHotkeyLabel { view_id='slot_select', - frame={b=0, l=2}, + frame={b=2, l=2}, key='CUSTOM_X', key_back='CUSTOM_SHIFT_X', label='next/previous slot', @@ -945,11 +1033,16 @@ function PlannerOverlay:init() on_change=function(val) self.selected_favorite = val end, }, widgets.HotkeyLabel{ - frame={b=0, l=28}, + frame={b=2, l=28}, label="set/apply selected", key='CUSTOM_Y', on_activate=function () self:save_restore_filter(self.selected_favorite) end, }, + widgets.TooltipLabel { + frame={b=0, l=2}, + show_tooltip=true, + text="Shift+click to edit the label of a favorite", + }, } } @@ -969,7 +1062,7 @@ function PlannerOverlay:show_favorites() end function PlannerOverlay:show_hide_favorites(new) - local errors_frame = {t=15+(new and 9 or 0), l=0, r=0} + local errors_frame = {t=16+(new and 9 or 0), l=0, r=0} self.subviews.errors.frame = errors_frame self:updateLayout() end @@ -1038,6 +1131,74 @@ function PlannerOverlay:clear_filter(idx) desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end +function PlannerOverlay:queue_order(idx) + local item = self.subviews['item'..tostring(idx)] + if not item or not item.available or not item.quantity or item.available >= item.quantity then return end + local missing = item.quantity - item.available + if missing <= 0 then return end + + local filter = get_cur_filters()[idx] + + local job_name = "ConstructBlocks" + local item_type = nil + if filter.item_type and filter.item_type ~= -1 then + item_type = filter.item_type + elseif filter.vector_id and filter.vector_id ~= -1 then + local mapping_vector = { + [df.job_item_vector_id.ANY_WEAPON] = df.item_type.WEAPON, + [df.job_item_vector_id.ANY_ARMOR] = df.item_type.ARMOR, + } + item_type = mapping_vector[filter.vector_id] + end + + if item_type and ITEM_TO_JOB[item_type] then + job_name = ITEM_TO_JOB[item_type] + end + + local order_json = { + job = job_name, + amount_total = missing + } + + local buildingplan = require('plugins.buildingplan') + local cats_list = {} + if buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) then + local cats = buildingplan.getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) + for cat, enabled in pairs(cats) do + if enabled and cat ~= 'unset' then + table.insert(cats_list, cat) + end + end + end + + if #cats_list == 0 then + -- df manager requires a material category for generic jobs or it queues "unknown material" orders + table.insert(cats_list, JOB_DEFAULTS[job_name] or 'stone') + end + + local mat_cats = {} + for _, cat in ipairs(cats_list) do + if VALID_MAT_CATS[cat] then + table.insert(mat_cats, cat) + elseif cat == 'stone' then + order_json.material = "INORGANIC" + elseif cat == 'glass' then + order_json.material = "GLASS_GREEN" + elseif cat == 'metal' or cat == 'iron' then + order_json.material = "IRON" + end + end + + if #mat_cats > 0 then + order_json.material_category = mat_cats + end + + dfhack.run_command_silent('workorder', json.encode(order_json)) + + local desc = item.desc or "item" + dfhack.gui.showAnnouncement('Work order queued for ' .. tostring(missing) .. ' ' .. desc .. '.', COLOR_YELLOW, true) +end + local function get_placement_data() local direction = uibs.direction local bounds = get_selected_bounds() @@ -1307,10 +1468,16 @@ function PlannerOverlay:place_building(placement_data, chosen_items) if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end + local fields = {} + if is_siege_engine() then + local facing = df.global.buildreq.direction + fields.facing = facing + fields.resting_orientation = facing + end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, - direction=uibs.direction, filters=filters} + direction=uibs.direction, filters=filters, fields=fields} if err then -- it's ok if some buildings fail to build goto continue @@ -1348,7 +1515,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) dfhack.printerr(('item no longer available: %d'):format(item_id)) break end - if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + if not dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, idx-1, -1) then dfhack.printerr(('cannot attach item: %d'):format(item_id)) break end @@ -1358,6 +1525,11 @@ function PlannerOverlay:place_building(placement_data, chosen_items) end end buildingplan.addPlannedBuilding(bld) + -- the job already exists at designation time; flagging it now means it + -- is posted with do_now whenever it gets unsuspended + if self.state.do_now then + buildingplan.setDoNow(bld, true) + end end buildingplan.scheduleCycle() uibs.selection_pos:clear() diff --git a/plugins/lua/buildingplan/unlink_mechanisms.lua b/plugins/lua/buildingplan/unlink_mechanisms.lua index 86ae80940f..d6a676fdd1 100644 --- a/plugins/lua/buildingplan/unlink_mechanisms.lua +++ b/plugins/lua/buildingplan/unlink_mechanisms.lua @@ -4,6 +4,7 @@ local dialogs = require('gui.dialogs') local overlay = require("plugins.overlay") local utils = require("utils") local widgets = require("gui.widgets") +local lever = reqscript("lever") local function mech_iter(b) --iterate mechanisms backwards local t = b.contained_items @@ -35,6 +36,40 @@ local function get_mech_target(m) --mechanism target building if exists return i and df.building.find(m.general_refs[i].building_id) or nil end +local function is_lever(b) --building is a lever + return b and b._type == df.building_trapst and b.trap_type == df.trap_type.Lever +end + +local function get_pull_job(b) --pending pull job on lever, or nil + for _, j in ipairs(b.jobs) do + if j.job_type == df.job_type.PullLever then + return j + end + end +end + +local ASCII_LEVER_OFF = string.char(0x95) --ò +local ASCII_LEVER_ON = string.char(0xA2) --ó + +local function get_lever_state_char(lever) --lever position glyph for the current tileset + -- match the current mode because ASCII and premium lever glyphs differ in directions + if dfhack.screen.inGraphicsMode() then + return lever.state == 0 and "/" or "\\" + end + return lever.state == 0 and ASCII_LEVER_OFF or ASCII_LEVER_ON +end + +local function get_pull_label(lever) --button label and pen for the lever's pull state + local state_char = get_lever_state_char(lever) --current lever position + local job = get_pull_job(lever) + if not job then + return "Pull "..state_char, COLOR_WHITE + elseif dfhack.job.getWorker(job) then + return "Pulling "..state_char, COLOR_GREEN --a citizen has taken the job + end + return "Queued "..state_char, COLOR_YELLOW --queued, not yet taken +end + local function has_link_tab(b) --linked building tab exists if not b then return @@ -148,7 +183,7 @@ local valid_build = { MechLinkOverlay = defclass(MechLinkOverlay, overlay.OverlayWidget) MechLinkOverlay.ATTRS { - desc = "Allows unlinking mechanisms from buildings.", + desc = "Allows unlinking mechanisms and pulling linked levers from buildings.", default_enabled = true, default_pos = {x=-41, y=-4}, frame = {w=56, h=27}, @@ -303,6 +338,55 @@ function MechLinkOverlay:activate_button(n) end end +function MechLinkOverlay:get_pull_button(n, ensure) + local button = self.subviews["pull_"..n] + if not button and ensure then + self:addviews + { + widgets.TextButton + { + view_id = "pull_"..n, + frame = {t=0, r=17, w=11, h=1}, + label = "", --set per-frame in update_buttons + on_activate = function() self:activate_pull(n) end, + visible = false, + }, + } + button = self.subviews["pull_"..n] + button:updateLayout(self.frame_body) + end + + return button +end + +function MechLinkOverlay:pull_target(n) --linked lever for button n, or nil + local button = self:get_pull_button(n) + if not button then + return + end + + local idx = self:idx_from_offset(button.frame.t) + if idx > 0 and idx < #self.building.contained_items then + local target = get_mech_target(self.building.contained_items[idx].item) + if is_lever(target) then + return target + end + end +end + +function MechLinkOverlay:activate_pull(n) + local target = self:pull_target(n) + if not target then + return + end + local job = get_pull_job(target) + if job then + dfhack.job.removeJob(job) --cancel queued pull + else + lever.leverPullJob(target, true) --do now + end +end + function MechLinkOverlay:ask_unlink_all() local saved_mode = self.subviews.unlink_mode:getOptionValue() local message = { @@ -318,8 +402,17 @@ function MechLinkOverlay:ask_unlink_all() local free_src = (saved_mode % 2 == 1) local free_dst = (saved_mode // 2 > 0) + -- freeing a mechanism removes it from contained_items and shifts the + -- indices of the entries after it, so resolve items before mutating + local items = {} for _, idx in ipairs(self.links) do - local item = self.building.contained_items[idx].item + local entry = self.building.contained_items[idx] + if entry then + table.insert(items, entry.item) + end + end + + for _, item in ipairs(items) do local target = get_mech_target(item) if target then @@ -356,6 +449,20 @@ function MechLinkOverlay:update_buttons() button.visible = true end button:updateLayout() + + local pbutton = self:get_pull_button(i, true) + pbutton.visible = false + local target = idx > 0 and idx < bci_len and + get_mech_target(self.building.contained_items[idx].item) + if is_lever(target) then + local label, pen = get_pull_label(target) + pbutton:setLabel(label) + pbutton.label.text_pen = pen + pbutton.frame.t = offset + pbutton.frame.r = h_offset + 9 + pbutton.visible = true + end + pbutton:updateLayout() end local b = (self.frame.h % 3) == 1 and #self.links >= self.num_buttons and 0 or 1 @@ -371,6 +478,10 @@ function MechLinkOverlay:preUpdateLayout(parent_rect) if button then button.visible = false end + local pbutton = self:get_pull_button(i) + if pbutton then + pbutton.visible = false + end end local h = parent_rect.height - 49 diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index b988e9d42e..e8cf3eb723 100644 --- a/plugins/lua/dig.lua +++ b/plugins/lua/dig.lua @@ -4,7 +4,7 @@ local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/damp_dig_toolbar.png', 8, 12, true) +local toolbar_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/damp_dig_toolbar.png', 8, 12, true) local main_if = df.global.game.main_interface local selection_rect = df.global.selection_rect @@ -348,25 +348,31 @@ function WarmDampOverlay:onRenderFrame(dc, rect) end -- -------------------------------- --- CarveOverlay +-- DesignatedOverlay -- -CarveOverlay = defclass(CarveOverlay, overlay.OverlayWidget) -CarveOverlay.ATTRS{ +DesignatedOverlay = defclass(DesignatedOverlay, overlay.OverlayWidget) +DesignatedOverlay.ATTRS{ desc='Makes existing carving designations visible when in ASCII mode.', viewscreens={ + 'dwarfmode/Designate/DIG_DIG', + 'dwarfmode/Designate/DIG_REMOVE_STAIRS_RAMPS', + 'dwarfmode/Designate/DIG_STAIR_UPDOWN', + 'dwarfmode/Designate/DIG_RAMP', + 'dwarfmode/Designate/DIG_CHANNEL', 'dwarfmode/Designate/SMOOTH', 'dwarfmode/Designate/ENGRAVE', 'dwarfmode/Designate/TRACK', 'dwarfmode/Designate/FORTIFY', 'dwarfmode/Designate/ERASE', + 'dwarfmode/dfhack/lua/quickfort', }, default_enabled=true, frame={w=0, h=0}, } -function CarveOverlay:onRenderFrame() - paintScreenCarve() +function DesignatedOverlay:onRenderFrame() + paintScreenDesignated() end -- -------------------------------- @@ -374,7 +380,7 @@ end -- OVERLAY_WIDGETS = { - asciicarve=CarveOverlay, + asciidesignated=DesignatedOverlay, warmdamp=WarmDampOverlay, warmdamptoolbar=WarmDampToolbarOverlay, } diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index d98bfd80f5..40ed1c71df 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -5,7 +5,7 @@ local guidm = require('gui.dwarfmode') local overlay = require('plugins.overlay') local utils = require('utils') -local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json' +local DWARFMONITOR_CONFIG_FILE = dfhack.getConfigPath() .. '/dwarfmonitor.json' -- ------------- -- -- WeatherWidget -- @@ -117,12 +117,10 @@ end function MiseryWidget:overlay_onupdate() local counts, num_colors = {}, #self.colors - for _,unit in ipairs(df.global.world.units.active) do - if not dfhack.units.isCitizen(unit, true) then goto continue end + for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do local stress_category = math.min(num_colors, dfhack.units.getStressCategory(unit)+1) counts[stress_category] = (counts[stress_category] or 0) + 1 - ::continue:: end local width = 2 + num_colors - 1 -- 'H:' plus the slashes diff --git a/plugins/lua/export-world-map.lua b/plugins/lua/export-world-map.lua new file mode 100644 index 0000000000..672f141e43 --- /dev/null +++ b/plugins/lua/export-world-map.lua @@ -0,0 +1,12 @@ +local _ENV = mkmodule('plugins.export-world-map') + +--[[ + +Native functions: + +- getWorldFolderName() +- getDateFolderName() + +]] + +return _ENV diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 74d0b96231..5c04fba2df 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,8 +5,7 @@ local helpdb = require('helpdb') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true) -local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12, true) +local logo_textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/logo.png', 8, 12, true) local function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] @@ -41,7 +40,11 @@ function HotspotMenuWidget:init() {VERT_BAR, 'c', 'k', VERT_BAR}, }, tileset=logo_textures, - tileset_hover=logo_hovered_textures, + tileset_offset=1, + tileset_stride=8, + tileset_hover=logo_textures, + tileset_hover_offset=5, + tileset_hover_stride=8, }, on_click=function() dfhack.run_command{'hotkeys', 'menu', self.name} end, }, diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 2774bd80ee..a470e3e2ce 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -38,7 +38,7 @@ local function do_import() dismiss_on_select2=false, on_select2=function(_, choice) if choice.text:startswith('library/') then return end - local fname = 'dfhack-config/orders/'..choice.text..'.json' + local fname = dfhack.getConfigPath() .. '/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, @@ -74,10 +74,11 @@ local mi = df.global.game.main_interface OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget) OrdersOverlay.ATTRS{ desc='Adds import, export, and other functions to the manager orders screen.', - default_pos={x=53,y=-6}, + default_pos={x=41,y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', frame={w=43, h=4}, + version=1, } function OrdersOverlay:init() @@ -709,11 +710,344 @@ function QuantityRightClickOverlay:onInput(keys) end end +-- +-- OrdersSearchOverlay +-- + +local ORDER_HEIGHT = 3 +local TABS_WIDTH_THRESHOLD = 155 +local LIST_START_Y_ONE_TABS_ROW = 8 +local LIST_START_Y_TWO_TABS_ROWS = 10 +local BOTTOM_MARGIN = 9 +local ARROW_X = 10 + +local SELECTED_PEN = dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE, bold=true} +local MATCH_PEN = dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_BLACK, bold=true} + +local function perform_search(text) + local matches = {} + + if text == '' then + return matches + end + + local orders = df.global.world.manager_orders.all + for i = 0, #orders - 1 do + local order = orders[i] + local search_key = dfhack.job.getManagerOrderName(order) + if search_key and utils.search_text(search_key, text) then + table.insert(matches, i) + end + end + + return matches +end + +local function concat_order_names() + local orders = df.global.world.manager_orders.all + if #orders == 0 then return "" end + + local names = {} + for i = 0, #orders - 1 do + local name = dfhack.job.getManagerOrderName(orders[i]) + table.insert(names, name or "") + end + + return table.concat(names, "|") +end + +local function getListStartY() + local rect = gui.get_interface_rect() + + if rect.width >= TABS_WIDTH_THRESHOLD then + return LIST_START_Y_ONE_TABS_ROW + else + return LIST_START_Y_TWO_TABS_ROWS + end +end + +local function getViewportSize() + local rect = gui.get_interface_rect() + local list_start_y = getListStartY() + + local available_height = rect.height - list_start_y - BOTTOM_MARGIN + return math.floor(available_height / ORDER_HEIGHT) +end + +local function getVisibleOrderIndices() + local orders = df.global.world.manager_orders.all + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + + if #orders == 0 then return 0, -1 end + + local viewport_size = getViewportSize() + local viewport_start = scroll_pos + local viewport_end = scroll_pos + viewport_size - 1 + + -- Handle end-of-list case + if viewport_end >= #orders then + viewport_end = #orders - 1 + viewport_start = math.max(0, viewport_end - viewport_size + 1) + end + + return viewport_start, viewport_end +end + +local function calculateOrderY(order_idx) + local orders = df.global.world.manager_orders.all + + if #orders == 0 or order_idx < 0 or order_idx >= #orders then + return nil + end + + local viewport_start, viewport_end = getVisibleOrderIndices() + + -- Check if order is in viewport + if order_idx < viewport_start or order_idx > viewport_end then + return nil + end + + local list_start_y = getListStartY() + local pos_in_viewport = order_idx - viewport_start + + return list_start_y + (pos_in_viewport * ORDER_HEIGHT) +end + +OrdersSearchOverlay = defclass(OrdersSearchOverlay, overlay.OverlayWidget) +OrdersSearchOverlay.ATTRS{ + desc='Adds a search box to find and navigate to matching manager orders.', + default_pos={x=85, y=-6}, + default_enabled=true, + viewscreens='dwarfmode/Info/WORK_ORDERS/Default', + frame={w=26, h=4}, + overlay_onupdate_max_freq_seconds=1, +} + +function OrdersSearchOverlay:init() + local main_panel = widgets.Panel{ + view_id='main_panel', + frame={t=0, l=0, r=0, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + frame_title='Search', + visible=function() return not self.minimized end, + subviews={ + widgets.EditField{ + view_id='filter', + frame={t=0, l=0}, + key='CUSTOM_ALT_S', + on_change=self:callback('update_filter'), + on_submit=self:callback('on_submit'), + on_submit2=self:callback('on_submit2'), + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='prev', + key='CUSTOM_ALT_P', + auto_width=true, + on_activate=self:callback('cycle_match', -1), + enabled=function() return #self.matched_indices > 0 end, + }, + widgets.HotkeyLabel{ + frame={t=1, l=12}, + label='next', + key='CUSTOM_ALT_N', + auto_width=true, + on_activate=self:callback('cycle_match', 1), + enabled=function() return #self.matched_indices > 0 end, + }, + }, + } + + 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, + } + + self.minimized = false + self.matched_indices = {} + self.current_match_idx = 0 + self.cached_order_names = nil +end + +function OrdersSearchOverlay:overlay_onupdate() + if self.minimized then return end + + local current_order_names = concat_order_names() + if current_order_names ~= self.cached_order_names then + self.cached_order_names = current_order_names + self:update_filter() + end +end + +function OrdersSearchOverlay:update_filter() + local text = self.subviews.filter.text + self.matched_indices = perform_search(text) + self.current_match_idx = 0 + + if text == '' then + self.subviews.main_panel.frame_title = 'Search' + else + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() + end +end + +function OrdersSearchOverlay:on_submit() + self:cycle_match(1) + self.subviews.filter:setFocus(true) +end + +function OrdersSearchOverlay:on_submit2() + self:cycle_match(-1) + self.subviews.filter:setFocus(true) +end + +function OrdersSearchOverlay:cycle_match(direction) + local search_text = self.subviews.filter.text + + local new_matches = perform_search(search_text) + + if #new_matches == 0 then + self.matched_indices = {} + self.current_match_idx = 0 + self.subviews.main_panel.frame_title = 'Search' + return + end + + local new_match_idx = self.current_match_idx + direction + + if new_match_idx > #new_matches then + new_match_idx = 1 + elseif new_match_idx < 1 then + new_match_idx = #new_matches + end + + self.matched_indices = new_matches + self.current_match_idx = new_match_idx + + -- Scroll to the selected match only if not already visible + local order_idx = self.matched_indices[self.current_match_idx] + local viewport_start, viewport_end = getVisibleOrderIndices() + if order_idx < viewport_start or order_idx > viewport_end then + mi.info.work_orders.scroll_position_work_orders = order_idx + end + + self.subviews.main_panel.frame_title = 'Search' .. self:get_match_text() +end + +function OrdersSearchOverlay:get_match_text() + local total_matches = #self.matched_indices + + if total_matches == 0 then + return '' + end + + if self.current_match_idx == 0 then + return string.format(': %d matches', total_matches) + end + + return string.format(': %d of %d', self.current_match_idx, total_matches) +end + +local function is_mouse_key(keys) + return keys._MOUSE_L + or keys._MOUSE_R + or keys._MOUSE_M + or keys.CONTEXT_SCROLL_UP + or keys.CONTEXT_SCROLL_DOWN + or keys.CONTEXT_SCROLL_PAGEUP + or keys.CONTEXT_SCROLL_PAGEDOWN +end + +function OrdersSearchOverlay:onInput(keys) + if mi.job_details.open then return end + + local filter_field = self.subviews.filter + if not filter_field then return false end + + -- Unfocus search on right-click + if keys._MOUSE_R and filter_field.focus then + filter_field:setFocus(false) + return true + end + + -- Let parent handle input first (for HotkeyLabel clicks and widget interactions) + if OrdersSearchOverlay.super.onInput(self, keys) then + return true + end + + -- Unfocus search on left-click when focused (for workshop and number of times changes) + -- And let the click pass through + if keys._MOUSE_L and filter_field.focus then + filter_field:setFocus(false) + return false + end + + -- Only consume input if search field has focus and it's not a mouse key + -- This allows scrolling, navigation, and mouse interaction in the orders list + if filter_field.focus and not is_mouse_key(keys) then + return true + end + + return false +end + +function OrdersSearchOverlay:render(dc) + if mi.job_details.open then return end + OrdersSearchOverlay.super.render(self, dc) + self:render_highlights(dc) +end + +function OrdersSearchOverlay:render_highlights(dc) + if #self.matched_indices == 0 then return end + + local selected_order_idx = self.current_match_idx > 0 and + self.matched_indices[self.current_match_idx] or nil + + for _, match_order_idx in ipairs(self.matched_indices) do + local match_y = calculateOrderY(match_order_idx) + + if match_y then + local pen = (match_order_idx == selected_order_idx) and SELECTED_PEN or MATCH_PEN + + dc:seek(ARROW_X, match_y):string('|', pen) + dc:seek(ARROW_X, match_y + 1):string('>', pen) + dc:seek(ARROW_X, match_y + 2):string('|', pen) + end + end +end + -- ------------------- OVERLAY_WIDGETS = { recheck=RecheckOverlay, importexport=OrdersOverlay, + search=OrdersSearchOverlay, skillrestrictions=SkillRestrictionOverlay, laborrestrictions=LaborRestrictionsOverlay, conditionsrightclick=ConditionsRightClickOverlay, diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 8d7c9a7e2d..95437c7993 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -6,7 +6,7 @@ local scriptmanager = require('script-manager') local utils = require('utils') local widgets = require('gui.widgets') -local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' +local OVERLAY_CONFIG_FILE = dfhack.getConfigPath() .. '/overlay.json' local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' local GLOBAL_KEY = 'OVERLAY' @@ -24,6 +24,7 @@ local overlay_config = {} -- map of widget name to persisted state local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db +-- for use by gui/overlay function get_state() return {index=widget_index, config=overlay_config, db=widget_db} end @@ -73,6 +74,11 @@ local function save_config() end end +function isOverlayEnabled(name) + if not overlay_config[name] then return false end + return overlay_config[name].enabled +end + -- ----------- -- -- utility fns -- -- ----------- -- @@ -165,6 +171,9 @@ local function do_enable(args, quiet, skip_save) vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if db_entry.widget.overlay_onenable then + db_entry.widget.overlay_onenable() + end if not quiet then print(('enabled widget %s'):format(name)) end @@ -196,6 +205,9 @@ local function do_disable(args, quiet) active_viewscreen_widgets[vs_name] = nil end end + if db_entry.widget.overlay_ondisable then + db_entry.widget.overlay_ondisable() + end if not quiet then print(('disabled widget %s'):format(name)) end @@ -474,6 +486,10 @@ local function matches_focus_strings(db_entry, vs_name, vs) if dfhack.gui.matchFocusString(fs, vs) then return true end + local lua_focus = fs:match('^[^/]+/(dfhack/lua/.*)$') + if lua_focus and dfhack.gui.matchFocusString(lua_focus) then + return true + end end end return matched @@ -518,7 +534,25 @@ local function _feed_viewscreen_widgets(vs_name, vs, keys) return false end +-- when the native UI is capturing text input (renaming a building, editing a +-- workshop job filter, etc.), overlay widgets must not consume keys, since +-- their bound hotkeys would otherwise steal typed characters (#5764) +local function is_native_text_entry_active() + local mi = df.global.game.main_interface + local vs = mi.view_sheets + return mi.entering_building_name or + vs.building_entering_nickname or + vs.unit_overview_entering_nickname or + vs.unit_overview_entering_profession_nickname or + vs.entering_building_job_filter or + vs.entering_gen_work_order_num or + vs.entering_wq_number +end + function feed_viewscreen_widgets(vs_name, vs, keys) + if is_native_text_entry_active() then + return false + end if not _feed_viewscreen_widgets(vs_name, vs, keys) and not _feed_viewscreen_widgets('all', nil, keys) then return false @@ -526,16 +560,16 @@ function feed_viewscreen_widgets(vs_name, vs, keys) return true end -local function _render_viewscreen_widgets(vs_name, vs, full_dc, scaled_dc) +local function _render_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end local full, scaled = get_interface_rects() - full_dc = full_dc or gui.Painter.new(full) - scaled_dc = scaled_dc or gui.Painter.new(scaled) for _,db_entry in pairs(vs_widgets) do local w = db_entry.widget if (not vs or matches_focus_strings(db_entry, vs_name, vs)) and utils.getval(w.visible) then - detect_frame_change(w, function() w:render(w.fullscreen and full_dc or scaled_dc) end) + detect_frame_change(w, function() + w:render(w.fullscreen and gui.Painter.new(full) or gui.Painter.new(scaled)) + end) end end return full_dc, scaled_dc @@ -544,8 +578,8 @@ end local force_refresh function render_viewscreen_widgets(vs_name, vs) - local full_dc, scaled_dc = _render_viewscreen_widgets(vs_name, vs, nil, nil) - _render_viewscreen_widgets('all', nil, full_dc, scaled_dc) + _render_viewscreen_widgets(vs_name, vs) + _render_viewscreen_widgets('all', nil) if force_refresh then force_refresh = nil df.global.gps.force_full_display_count = 1 diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 8dd4e507af..3851920daf 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -320,9 +320,13 @@ local function get_codes(positions) if position.replaced_by == -1 then ensure_key(grouped, id)[id] = position else + -- replaced-by links may be cyclic. this code will behave "incorrectly" in the event + -- of a cycle but will not hang. a proper fix is still needed. see issue DFHack/dfhack#5538 local parent = positions[position.replaced_by] - while parent.replaced_by ~= -1 do + local counter = 0 + while parent.replaced_by ~= -1 and counter < 10 do parent = positions[parent.replaced_by] + counter = counter + 1 end ensure_key(grouped, parent.id)[id] = position end diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index fe42cbb1ed..5c22fb6486 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -43,7 +43,7 @@ local function get_active_idx_cache() end local function is_original_dwarf(unit) - return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 + return df.global.plotinfo.fortress_age == unit.usable_interaction.time_on_site // 10 end local WAVE_END_GAP = 10000 @@ -53,7 +53,7 @@ local function get_most_recent_wave_oldest_active_idx(cache) for idx=#active_units-1,0,-1 do local unit = active_units[idx] if not dfhack.units.isCitizen(unit) then goto continue end - if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + if oldest_unit and unit.usable_interaction.time_on_site - oldest_unit.usable_interaction.time_on_site > WAVE_END_GAP then return cache[oldest_unit.id] else oldest_unit = unit @@ -293,7 +293,7 @@ local function get_mental_stability(unit) local emotionally_obsessive = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE local humor = unit.status.current_soul.personality.traits.HUMOR local love_propensity = unit.status.current_soul.personality.traits.LOVE_PROPENSITY - local perseverence = unit.status.current_soul.personality.traits.PERSEVERENCE + local perseverance = unit.status.current_soul.personality.traits.PERSEVERANCE local politeness = unit.status.current_soul.personality.traits.POLITENESS local privacy = unit.status.current_soul.personality.traits.PRIVACY local stress_vulnerability = unit.status.current_soul.personality.traits.STRESS_VULNERABILITY @@ -315,7 +315,7 @@ local function get_mental_stability(unit) + (anxiety_propensity * -0.06) + (bravery * 0.06) + (cheer_propensity * 0.41) + (curious * -0.06) + (discord * 0.14) + (dutifulness * -0.03) + (emotionally_obsessive * -0.13) - + (humor * -0.05) + (love_propensity * 0.15) + (perseverence * -0.07) + + (humor * -0.05) + (love_propensity * 0.15) + (perseverance * -0.07) + (politeness * -0.14) + (privacy * 0.03) + (stress_vulnerability * -0.20) + (tolerant * -0.11) @@ -1029,12 +1029,29 @@ function SquadFilterOverlay:init() local left_panel = widgets.Panel{ view_id='left_panel', - frame={t=1, b=0, l=0, w=NARROW_WIDTH-4}, + frame={t=0, b=0, l=0, w=NARROW_WIDTH-4}, visible=true, subviews={ + widgets.HotkeyLabel{ + view_id='toggle_all', + frame={t=0, l=0}, + key='CUSTOM_SHIFT_A', + label='Toggle all', + on_activate=function() + local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' + self.subviews.military:setOption(target) + self.subviews.officials:setOption(target) + self.subviews.nobles:setOption(target) + self.subviews.infant:setOption(target) + self.subviews.unstable:setOption(target) + self.subviews.maimed:setOption(target) + self.subviews.labor_conflict:setOption(target) + poke_list() + end, + }, widgets.CycleHotkeyLabel{ view_id='military', - frame={t=0, l=0}, + frame={t=1, l=0}, key='CUSTOM_SHIFT_Q', label='Other squads:', options={ @@ -1047,7 +1064,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='officials', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_O', label=' Officials:', options={ @@ -1060,7 +1077,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='nobles', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_N', label=' Nobility:', options={ @@ -1076,12 +1093,25 @@ function SquadFilterOverlay:init() local right_panel = widgets.Panel{ view_id='right_panel', - frame={t=1, b=0, r=2, w=NARROW_WIDTH-4}, + frame={t=0, b=0, r=2, w=NARROW_WIDTH-4}, visible=false, subviews={ widgets.CycleHotkeyLabel{ - view_id='infant', + view_id='labor_conflict', frame={t=0, l=0}, + key='CUSTOM_SHIFT_U', + label=' Uniformed:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_LIGHTRED}, + }, + initial_option='include', + on_change=poke_list, + }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={t=1, l=0}, key='CUSTOM_SHIFT_M', label='With infants:', options={ @@ -1094,7 +1124,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='unstable', - frame={t=1, l=0}, + frame={t=2, l=0}, key='CUSTOM_SHIFT_D', label='Hates combat:', options={ @@ -1107,7 +1137,7 @@ function SquadFilterOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='maimed', - frame={t=2, l=0}, + frame={t=3, l=0}, key='CUSTOM_SHIFT_I', label=' Maimed:', options={ @@ -1125,21 +1155,6 @@ function SquadFilterOverlay:init() frame_style=gui.FRAME_MEDIUM, frame_background=gui.CLEAR_PEN, subviews={ - widgets.HotkeyLabel{ - frame={t=0, w=NARROW_WIDTH-3}, - key='CUSTOM_SHIFT_A', - label='Toggle all filters', - on_activate=function() - local target = self.subviews.military:getOptionValue() == 'exclude' and 'include' or 'exclude' - self.subviews.military:setOption(target) - self.subviews.officials:setOption(target) - self.subviews.nobles:setOption(target) - self.subviews.infant:setOption(target) - self.subviews.unstable:setOption(target) - self.subviews.maimed:setOption(target) - poke_list() - end, - }, left_panel, widgets.Label{ view_id='shifter', @@ -1167,9 +1182,8 @@ function SquadFilterOverlay:init() main_panel, widgets.Divider{ view_id='divider', - frame={l=NARROW_WIDTH-1, w=1, t=2}, + frame={l=NARROW_WIDTH-1, w=1, t=0}, frame_style=gui.FRAME_MEDIUM, - frame_style_t=false, visible=false, }, widgets.HelpButton{ @@ -1253,6 +1267,12 @@ local function is_maimed(unit) unit.status2.limbs_stand_count == 0 end +local function has_labor_conflict(unit) + return unit.status.labors[df.unit_labor.MINE] or + unit.status.labors[df.unit_labor.CUTWOOD] or + unit.status.labors[df.unit_labor.HUNT] +end + local function filter_matches(unit, filter) if filter.military == 'only' and not is_in_military(unit) then return false end if filter.military == 'exclude' and is_in_military(unit) then return false end @@ -1266,6 +1286,8 @@ local function filter_matches(unit, filter) if filter.unstable == 'exclude' and is_unstable(unit) then return false end if filter.maimed == 'only' and not is_maimed(unit) then return false end if filter.maimed == 'exclude' and is_maimed(unit) then return false end + if filter.labor_conflict == 'only' and not has_labor_conflict(unit) then return false end + if filter.labor_conflict == 'exclude' and has_labor_conflict(unit) then return false end return true end @@ -1281,6 +1303,7 @@ function do_squad_filter(unit) infant=self.subviews.infant:getOptionValue(), unstable=self.subviews.unstable:getOptionValue(), maimed=self.subviews.maimed:getOptionValue(), + labor_conflict=self.subviews.labor_conflict:getOptionValue(), } return filter_matches(unit, filter) end @@ -1294,6 +1317,7 @@ OVERLAY_WIDGETS = { candidates=require('plugins.sort.info').CandidatesOverlay, interrogation=require('plugins.sort.info').InterrogationOverlay, conviction=require('plugins.sort.info').ConvictionOverlay, + deathcause_button=require('plugins.sort.deathcause_button').DeathCauseOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, -- TODO: maybe rewrite for 50.12 -- burrow_assignment=require('plugins.sort.unitselector').BurrowAssignmentOverlay, @@ -1302,6 +1326,7 @@ OVERLAY_WIDGETS = { places=require('plugins.sort.places').PlacesOverlay, elevate_barony=require('plugins.sort.diplomacy').DiplomacyOverlay, elevate_barony_preferences=require('plugins.sort.diplomacy').PreferenceOverlay, + stress_icons=require('plugins.sort.stressicons').StressIconsOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua new file mode 100644 index 0000000000..b149601a7a --- /dev/null +++ b/plugins/lua/sort/deathcause_button.lua @@ -0,0 +1,78 @@ +local _ENV = mkmodule('plugins.sort.deathcause_button') + +local dialogs = require('gui.dialogs') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +DeathCauseOverlay = defclass(DeathCauseOverlay, overlay.OverlayWidget) +DeathCauseOverlay.ATTRS{ + desc='Adds a button to view death cause on the dead/missing tab.', + default_pos={x=50, y=-7}, + default_enabled=true, + viewscreens='dwarfmode/Info/CREATURES/DECEASED', + frame={w=21, h=1}, +} + +function DeathCauseOverlay:init() + local deathcause = reqscript('deathcause') + + local function get_selected_unit() + -- Navigate to the creatures/deceased widget hierarchy: + -- list_widget - the main deceased list + -- ├─ children[0]: scrollbar widget + -- └─ children[1]: container widget (list_container) + -- ├─ grandchildren[0]: header + -- ├─ grandchildren[1]: header or other UI + -- └─ grandchildren[2]: scrollable rows container (scrollable_list) + -- └─ rows: row widgets (each row = one unit in the list) + -- └─ row.children[x]: unit widget + -- └─ unit_widget.u: pointer to the df.unit object + + local creatures = df.global.game.main_interface.info.creatures + local list_widget = dfhack.gui.getWidget(creatures, 'Tabs', 'Dead/Missing') + if not list_widget then return nil end + + local children = dfhack.gui.getWidgetChildren(list_widget) + local list_container = children[1] + local grandchildren = dfhack.gui.getWidgetChildren(list_container) + + local scrollable_list = grandchildren[2] + if not scrollable_list then + return nil + end + + local rows = dfhack.gui.getWidgetChildren(scrollable_list) + + local cursor_idx = list_widget.cursor_idx or 0 + + if cursor_idx >= 0 and cursor_idx < #rows then + local row = rows[cursor_idx + 1] + + local ok, unit = pcall(function() return dfhack.gui.getWidget(row, 0).u end) + if ok and unit then + return unit + end + end + + return nil + end + + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='Show death cause', + key='CUSTOM_D', + on_activate=function() + local unit = get_selected_unit() + if not unit then + dialogs.showMessage('Death Cause', 'No unit selected.') + return + end + local cause = deathcause.getDeathCause(unit) + dialogs.showMessage('Death Cause', dfhack.df2console(cause)) + end, + }, + } +end + +return _ENV diff --git a/plugins/lua/sort/diplomacy.lua b/plugins/lua/sort/diplomacy.lua index c0ec818505..505e02b743 100644 --- a/plugins/lua/sort/diplomacy.lua +++ b/plugins/lua/sort/diplomacy.lua @@ -64,7 +64,7 @@ local function get_preferences(unit) if not unit then return {} end local preferences = {} for _, pref in ipairs(unit.status.current_soul.preferences) do - if pref.type == df.unit_preference.T_type.LikeItem and pref.active then + if pref.type == df.unitpref_type.LikeItem and pref.flags.visible then table.insert(preferences, make_item_description(pref.item_type, pref.item_subtype)) end end diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index feadee99c0..413b51f249 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -29,6 +29,10 @@ local function get_task_search_key(cri_unit) table.insert(result, bld.name) local btype = bld:getType() if btype == df.building_type.Workshop then + -- the display name may differ from the enum item name (e.g. + -- Masons -> "Stoneworker's Workshop") and is the raw-defined + -- name for custom workshops + table.insert(result, dfhack.buildings.getName(bld)) table.insert(result, df.workshop_type.attrs[bld.type].name or '') table.insert(result, df.workshop_type[bld.type]) elseif btype == df.building_type.Furnace then @@ -278,8 +282,8 @@ function InfoOverlay:get_key() end function resize_overlay(self) - local sw = dfhack.screen.getWindowSize() - local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + local iw = gui.get_interface_rect().width + local overlay_width = math.min(40, iw - (self.frame_rect.x1 + 30)) if overlay_width ~= self.frame.w then self.frame.w = overlay_width return true @@ -292,15 +296,13 @@ end function get_panel_offsets() local tabs_in_two_rows = is_tabs_in_two_rows() - local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS or - info.current_mode == df.info_interface_mode_type.LABOR + local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS local l_offset = (not tabs_in_two_rows and shift_right) and 4 or 0 local t_offset = 1 if tabs_in_two_rows then t_offset = shift_right and 0 or 3 end - if info.current_mode == df.info_interface_mode_type.JOBS or - info.current_mode == df.info_interface_mode_type.BUILDINGS then + if info.current_mode == df.info_interface_mode_type.JOBS then t_offset = t_offset - 1 end return l_offset, t_offset diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index 35a5569493..2b42be60c3 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -21,7 +21,7 @@ local zone_names = { [df.civzone_type.SandCollection] = 'Sand', [df.civzone_type.Office] = 'Office', [df.civzone_type.Dormitory] = 'Dormitory', - [df.civzone_type.Barracks] = 'Barrachs', + [df.civzone_type.Barracks] = 'Barracks', [df.civzone_type.ArcheryRange] = 'Archery Range', [df.civzone_type.Dump] = 'Garbage Dump', [df.civzone_type.AnimalTraining] = 'Animal Training', @@ -79,8 +79,9 @@ local function get_zone_search_key(zone) end -- allow zones w/ assignments to be searchable by their assigned unit - if zone.assigned_unit ~= nil then - table.insert(result, sortoverlay.get_unit_search_key(zone.assigned_unit)) + local owner = dfhack.buildings.getOwner(zone) + if owner then + table.insert(result, sortoverlay.get_unit_search_key(owner)) end -- allow zones to be searchable by type @@ -152,6 +153,12 @@ local function get_workshop_search_key(workshop) end table.insert(result, workshop.name) + + -- the display name may differ from the enum item name (e.g. Masons -> + -- "Stoneworker's Workshop") and is the raw-defined name for custom + -- workshops + table.insert(result, dfhack.buildings.getName(workshop)) + table.insert(result, df.workshop_type.attrs[workshop.type].name or '') table.insert(result, df.workshop_type[workshop.type]) @@ -169,6 +176,117 @@ local function get_farmplot_search_key(farmplot) return table.concat(result, ' ') end +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_type(siege_engine) + if siege_engine.type == df.siegeengine_type.BoltThrower then + return 'Bolt Thrower' + end + return df.siegeengine_type[siege_engine.type] +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_status(siege_engine) + -- portions of return value with with underscores are to allow easier + -- word-anchored matching even when the DFHack full-text search mode is + -- enabled; e.g. + -- - "loaded" would match "Loaded" and "Unloaded", + -- - but "_loaded" would only match "_Loaded" + local count = 0 + local count_all = siege_engine.type == df.siegeengine_type.BoltThrower + for _, building_item in ipairs(siege_engine.contained_items) do + if building_item.use_mode == df.building_item_role_type.TEMP then + if not count_all then + return 'Loaded _Loaded' + end + count = count + building_item.item:getStackSize() + end + end + if count_all and count > 0 then + return ('%d bolts _%d_bolts'):format(count, count) + end + return 'Unloaded' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_job_status(siege_engine) + for _, job in ipairs(siege_engine.jobs) do + if job.job_type == df.job_type.LoadCatapult + or job.job_type == df.job_type.LoadBallista + or job.job_type == df.job_type.LoadBoltThrower + then + if dfhack.job.getWorker(job) ~= nil then + return 'Loading' + else + return 'Inactive load task' + end + end + local firing_bolt_thrower = job.job_type == df.job_type.FireBoltThrower + local firing = job.job_type == df.job_type.FireCatapult + or job.job_type == df.job_type.FireBallista + or firing_bolt_thrower + if firing then + local unit = dfhack.job.getWorker(job) + if unit == nil then + return 'No operator' + else + ---@type integer?, integer?, integer? + local x, y, z = dfhack.units.getPosition(unit) + -- DF shows "present" when the unit is inside the building's + -- footprint (or, for bolt throwers, next to it); the unit does + -- not need to be at the exact firing position tile (which + -- varies based on siege engine type and direction) + if x ~= nil and z == siege_engine.z then + ---@cast y integer + if firing_bolt_thrower then + if siege_engine.x1 - 1 <= x and x <= siege_engine.x2 + 1 + and siege_engine.y1 - 1 <= y and y <= siege_engine.y2 + 1 + then + return 'Operator present' + end + elseif dfhack.buildings.containsTile(siege_engine, x, y) then + return 'Operator present' + end + end + return 'Operator assigned' + end + end + end + return '' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function get_siege_engine_search_key(siege_engine) + -- DF 53.05 Info window, Places tab, Siege Engines subtab shows this info: + -- name: assigned name or siege engine type name + -- status: "Unloaded", "Loaded", " bolts" + -- job status: + -- - "Inactive load task" (load job unassigned), + -- - "Loading" (load job assigned), + -- - "No operator" (fire job unassigned), + -- - "Operator present" (fire job assigned), + -- - "Operator assigned" (fire job assigned, but not in position), + -- - blank + -- action: (icons) fire-at-will, practice, prepare-to-fire, keep-loaded, not-in-use + -- These have associated text blurbs that are shown in the + -- building info window, but those texts are not discoverable + -- from the Info > Places > Siege engine list view. + local result = {} + + if #siege_engine.name ~= 0 then table.insert(result, siege_engine.name) end + + table.insert(result, siege_engine_type(siege_engine)) + + table.insert(result, siege_engine_status(siege_engine)) + + table.insert(result, siege_engine_job_status(siege_engine)) + + return table.concat(result, ' ') +end + -- ---------------------- -- PlacesOverlay -- @@ -176,7 +294,8 @@ end PlacesOverlay = defclass(PlacesOverlay, sortoverlay.SortOverlay) PlacesOverlay.ATTRS{ desc='Adds search functionality to the places overview screens.', - default_pos={x=71, y=9}, + default_pos={x=52, y=9}, + version=2, viewscreens='dwarfmode/Info', frame={w=40, h=6} } @@ -204,6 +323,7 @@ function PlacesOverlay:init() self:register_handler('STOCKPILES', buildings.list[df.buildings_mode_type.STOCKPILES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_stockpile_search_key})) self:register_handler('WORKSHOPS', buildings.list[df.buildings_mode_type.WORKSHOPS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_workshop_search_key})) self:register_handler('FARMPLOTS', buildings.list[df.buildings_mode_type.FARMPLOTS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_farmplot_search_key})) + self:register_handler('SIEGE_ENGINES', buildings.list[df.buildings_mode_type.SIEGE_ENGINES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_siege_engine_search_key})) end function PlacesOverlay:get_key() diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index c823c4c34e..53a591251d 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -4,10 +4,7 @@ local overlay = require('plugins.overlay') local utils = require('utils') function get_unit_search_key(unit) - return ('%s %s %s'):format( - dfhack.units.getReadableName(unit), - dfhack.units.getProfessionName(unit), - dfhack.translation.translateName(unit.name, true, true)) -- get English last name + return dfhack.units.getReadableName(unit) end local function copy_to_lua_table(vec) diff --git a/plugins/lua/sort/stressicons.lua b/plugins/lua/sort/stressicons.lua new file mode 100644 index 0000000000..1971326e6e --- /dev/null +++ b/plugins/lua/sort/stressicons.lua @@ -0,0 +1,102 @@ +local _ENV = mkmodule('plugins.sort.stressicons') + +local dialogs = require('gui.dialogs') +local overlay = require('plugins.overlay') + +-- indexed by stress category (0-6, where 0 is most stressed) +local STRESS_CATEGORY_NAMES = { + [0]='Miserable', 'Unhappy', 'Displeased', 'Content', 'Pleased', 'Happy', + 'Ecstatic', +} + +-- single-unit equivalent of the dfhack.units.getCitizens(true, true) filter +local function is_listable(unit) + return dfhack.units.isCitizen(unit, true) and dfhack.units.isActive(unit) +end + +local function show_stress_units(category) + local choices = {} + -- living, on-map citizens only (excluding residents), matching the game's + -- own stress icon counts + for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do + if dfhack.units.getStressCategory(unit) == category then + table.insert(choices, { + text=dfhack.units.getReadableName(unit), + unit_id=unit.id, + }) + end + end + local name = STRESS_CATEGORY_NAMES[category] + if #choices == 0 then + dialogs.showMessage('Citizens: ' .. name, + ('No citizens are currently %s.'):format(name:lower())) + return + end + dialogs.ListBox{ + frame_title='Citizens: ' .. name, + with_filter=true, + choices=choices, + on_select=function(_, choice) + -- re-resolve in case the unit died or left the map while the list + -- was open; df.unit.find still resolves dead units + local unit = df.unit.find(choice.unit_id) + if not unit or not is_listable(unit) then return end + dfhack.gui.revealInDwarfmodeMap(unit.pos.x, unit.pos.y, unit.pos.z, true, true) + end, + }:show() +end + +-- invisible hotspot over the stress icons in the top bar; clicking an icon +-- opens a list of the citizens in that stress category +StressIconsOverlay = defclass(StressIconsOverlay, overlay.OverlayWidget) +StressIconsOverlay.ATTRS{ + desc='Click a stress icon in the top bar to list the citizens in that stress category.', + default_pos={x=1, y=1}, + default_enabled=true, + viewscreens={'dwarfmode/Default'}, + overlay_onupdate_max_freq_seconds=1, + frame={w=1, h=1}, +} + +function StressIconsOverlay:overlay_onupdate() + self.icon_rects = nil + local dimx = df.global.gps.dimx + -- the 'Pop' label immediately precedes the stress icons in the top bar; + -- scan right to left so a fortress name containing 'Pop' doesn't win + local pop_x + for x = math.min(dimx-3, 200), 0, -1 do + if dfhack.screen.readTile(x, 0).ch == 80 and -- 'P' + dfhack.screen.readTile(x+1, 0).ch == 111 and -- 'o' + dfhack.screen.readTile(x+2, 0).ch == 112 then -- 'p' + pop_x = x + break + end + end + if not pop_x then return end + -- the icons are drawn in 3-column cells right of the 'Pop' block, with the + -- happiest category first + local rects = {} + for i = 0, 6 do + local x1 = pop_x + 4 + i * 3 + if x1 + 1 >= dimx then break end + table.insert(rects, {x1=x1, y1=0, x2=x1+2, y2=2, category=6-i}) + end + self.icon_rects = rects + self.frame = {l=pop_x+4, t=0, w=#rects*3, h=3} +end + +function StressIconsOverlay:onInput(keys) + if not keys._MOUSE_L or not self.icon_rects then return false end + local mx, my = dfhack.screen.getMousePos() + if not mx then return false end + for _,rect in ipairs(self.icon_rects) do + if my >= rect.y1 and my <= rect.y2 and + mx >= rect.x1 and mx <= rect.x2 then + show_stress_units(rect.category) + return true + end + end + return false +end + +return _ENV diff --git a/plugins/lua/spectate.lua b/plugins/lua/spectate.lua new file mode 100644 index 0000000000..1e0d81f917 --- /dev/null +++ b/plugins/lua/spectate.lua @@ -0,0 +1,686 @@ +local _ENV = mkmodule('plugins.spectate') + +local argparse = require('argparse') +local dlg = require('gui.dialogs') +local gui = require('gui') +local json = require('json') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +-- settings starting with 'tooltip-' are not passed to the C++ plugin +local lua_only_settings_prefix = 'tooltip-' + +-- how many lines the text following unit is allowed to be moved down to avoid overlapping +local max_banner_y_offset = 4 + +local function get_default_state() + return { + ['auto-unpause']=false, + ['cinematic-action']=true, + ['follow-seconds']=10, + ['include-animals']=false, + ['include-hostiles']=false, + ['include-visitors']=false, + ['include-wildlife']=false, + ['prefer-conflict']=true, + ['prefer-new-arrivals']=true, + ['prefer-nicknamed']=true, + ['tooltip-follow']=true, + ['tooltip-follow-blink-milliseconds']=3000, + ['tooltip-follow-hold-to-show']='none', -- one of none, ctrl, alt, or shift + ['tooltip-follow-job']=true, + ['tooltip-follow-activity']=true, + ['tooltip-follow-job-shortenings'] = { + ["Store item in stockpile"] = "Store item", + }, + ['tooltip-follow-name']=false, + ['tooltip-follow-stress']=true, + ['tooltip-follow-stress-levels']={ + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic + }, + ['tooltip-hover']=true, + ['tooltip-hover-job']=true, + ['tooltip-hover-activity']=true, + ['tooltip-hover-name']=true, + ['tooltip-hover-stress']=true, + ['tooltip-hover-stress-levels']={ + ["0"] = true, -- Miserable + ["1"] = true, + ["2"] = false, + ["3"] = false, + ["4"] = false, + ["5"] = true, + ["6"] = true, -- Ecstatic + }, + ['tooltip-stress-levels']={ + -- keep in mind, the text will look differently with game's font + -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE + ["0"] = {text = "=C", pen = COLOR_RED, name = "Miserable"}, + ["1"] = {text = ":C", pen = COLOR_LIGHTRED, name = "Unhappy"}, + ["2"] = {text = ":(", pen = COLOR_YELLOW, name = "Displeased"}, + ["3"] = {text = ":]", pen = COLOR_GREY, name = "Content"}, + ["4"] = {text = ":)", pen = COLOR_GREEN, name = "Pleased"}, + ["5"] = {text = ":D", pen = COLOR_LIGHTGREEN, name = "Happy"}, + ["6"] = {text = "=D", pen = COLOR_LIGHTCYAN, name = "Ecstatic"}, + } + } +end + +local function load_state() + local state = get_default_state() + local config_file = json.open(dfhack.getConfigPath() .. '/spectate.json') + for key in pairs(config_file.data) do + if state[key] == nil then + config_file.data[key] = nil + end + end + utils.assign(state, config_file.data) + config_file.data = state + return config_file.data, + function() config_file:write() end +end + +local config, save_state = load_state() + +-- called by gui/spectate +function get_config_elem(name, key) + local elem = config[name] + if elem == nil then return end + if type(elem) == 'table' then + return elem[key] + end + return elem +end + +function refresh_cpp_config() + for name,value in pairs(config) do + if not name:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(name, value) + end + end +end + +function show_squads_warning() + local message = { + 'Cannot start spectate mode while the squads panel is open. Spectate', + 'automatically disengages when you open the squads panel.', + '', + 'Please close the squads panel before enabling spectate mode.', + } + dlg.showMessage("Spectate", table.concat(message, '\n')) +end + +----------------------------- +-- commandline interface + +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function () -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + +-- no recursion protection, but it shouldn't be needed for a config... +local function print_table(t, indent) + indent = indent or '' + for key, value in pairsByKeys(t) do + if type(value) == 'table' then + print(indent .. key .. ':') + print_table(value, indent .. ' ') + else + print(indent .. key .. ': ' .. tostring(value)) + end + end +end + +local function print_status() + print('spectate is:', isEnabled() and 'enabled' or 'disabled') + print() + print('settings:') + + print_table(config, ' ') +end + +local function do_toggle() + if isEnabled() then + dfhack.run_command('disable', 'spectate') + else + dfhack.run_command('enable', 'spectate') + end +end + +local function set_setting(args) + local n = #args + if n == 0 then + qerror('missing key') + end + + local cfg = config + local v + for i = 1, n do + v = cfg[args[i]] + if v == nil then + -- probably an unknown option, but we may allow adding new keys + break + elseif type(v) == 'table' then + if i == n then + -- arrived at the very last argument, but have a table + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + end + cfg = v + else + -- arrived at something that's not a table + if i == n-1 then + -- if there is exactly 1 argument left, we're good + break + elseif i == n then + qerror('missing value for ' .. table.concat(args, '/', 1, i)) + else -- i < n-1 then + qerror('too many arguments for ' .. table.concat(args, '/', 1, i)) + end + end + end + if v == nil then + if n == 3 and args[1] == 'tooltip-follow-job-shortenings' then + -- user should be able to add new shortenings, but not other things + else + qerror('unknown option: ' .. table.concat(args, '/', 1, i)) + end + end + + local path = table.concat(args, '/', 1, n-1) + local key = args[n-1] + local value = args[n] + local entry_type = type(cfg[key]) + if entry_type == 'table' then + -- here just in case, is already checked in the loop above + qerror('missing value for ' .. path) + elseif entry_type == 'boolean' then + if value == 'toggle' then + value = not cfg[key] + else + value = argparse.boolean(value, path) + end + elseif entry_type == 'number' then + if path == 'follow-seconds' then + value = argparse.positiveInt(value, path) + else + value = argparse.nonnegativeInt(value, path) + end + end + + cfg[key] = value + + if n == 2 and not key:startswith(lua_only_settings_prefix) then + if type(value) == 'boolean' then + value = value and 1 or 0 + end + spectate_setSetting(key, value) + end + + save_state() +end + +local function set_overlay(value) + value = argparse.boolean(value, name) + dfhack.run_command('overlay', value and 'enable' or 'disable', 'spectate.tooltip') +end + +function parse_commandline(args) + local command = table.remove(args, 1) + if not command or command == 'status' then + print_status() + elseif command == 'toggle' then + if #args == 0 then + do_toggle() + else + args[#args+1] = 'toggle' + set_setting(args) + end + elseif command == 'set' then + set_setting(args) + elseif command == 'overlay' then + if #args == 0 then qerror('missing option') end + set_overlay(args[1]) + else + return false + end + + return true +end + +----------------------------- +-- info functions + +local function GetUnitStress(unit, stress_levels) + local stressCat = dfhack.units.getStressCategory(unit) + if stressCat > 6 then stressCat = 6 end + stressCat = tostring(stressCat) + if not stress_levels[stressCat] then return end + + local level_cfg = config['tooltip-stress-levels'][stressCat] + return {text=level_cfg.text, pen=level_cfg.pen} +end + +local function GetUnitName(unit) + return dfhack.units.getReadableName(unit) +end + +local function GetUnitJob(unit) + local job = unit.job.current_job + return job and dfhack.job.getName(job) +end + +-- there is no equivalent of `dfhack.job.GetName` yet +-- this function is by no means a replacement, it's +-- here to show something at least +local activityNames = { + [df.activity_entry_type.TrainingSession] = "Training Session", + [df.activity_entry_type.IndividualSkillDrill] = "Individual Skill Drill", + [df.activity_entry_type.FillServiceOrder] = "Fill Service Order", + [df.activity_entry_type.StoreObject] = "Store Object", + -- other types are single words +} +local function activity_GetName(activity) + local t = activity.type + local n = df.activity_entry_type[t] + n = activityNames[n] or n + return {text = n, pen = COLOR_LIGHTGREEN} +end + +local function GetUnitActivity(unit) + local activity = dfhack.units.getMainSocialActivity(unit) + return activity and activity_GetName(activity) +end + +local function GetRelevantSettings(key) + return config['tooltip-' .. key .. '-name'], + config['tooltip-' .. key .. '-job'], + config['tooltip-' .. key .. '-activity'], + config['tooltip-' .. key .. '-stress'], + config['tooltip-' .. key .. '-stress-levels'], + config['tooltip-' .. key .. '-job-shortenings'] +end + +local function GetUnitInfoText(unit, settings_group_name) + local show_name, show_job, show_activity, show_stress, stress_levels, job_shortenings = GetRelevantSettings(settings_group_name) + + local stress = show_stress and GetUnitStress(unit, stress_levels) or nil + local name = show_name and GetUnitName(unit) or nil + local job = show_job and GetUnitJob(unit) or nil + if job_shortenings then job = job_shortenings[job] or job end + local activity = show_activity and GetUnitActivity(unit) or nil + + local job_or_activity = job or activity + + local txt = {} + if stress then + txt[#txt+1] = stress + if name or job_or_activity then txt[#txt+1] = ' ' end + end + if name then + txt[#txt+1] = name + end + if job_or_activity then + if name then txt[#txt+1] = ": " end + txt[#txt+1] = job_or_activity + end + + return txt +end + +local function unit_filter(unit) + return not dfhack.units.isHidden(unit) +end + +local function GetHoverText(pos) + if not pos then return end + + local txt = {} + local units = dfhack.units.getUnitsInBox(pos, pos, unit_filter) or {} + + for _,unit in ipairs(units) do + local info = GetUnitInfoText(unit, 'hover') + if not next(info) then goto continue end + + for _,t in ipairs(info) do + txt[#txt+1] = t + end + txt[#txt+1] = NEWLINE + + ::continue:: + end + + return txt +end + +----------------------------- +-- TooltipOverlay + +TooltipOverlay = defclass(TooltipOverlay, overlay.OverlayWidget) +TooltipOverlay.ATTRS{ + desc='Adds info tooltips that follow units or appear when you hover the mouse.', + default_pos={x=1,y=1}, + fullscreen=true, + viewscreens='dwarfmode/Default', +} + +function TooltipOverlay:init() + self:addviews{MouseTooltip{view_id = 'tooltip'}} +end + +function TooltipOverlay:preUpdateLayout(parent_rect) + -- this is required, otherwise there is no room to draw child widgets in + self.frame.w = parent_rect.width + self.frame.h = parent_rect.height +end + +function TooltipOverlay:render(dc) + self:render_unit_banners(dc) + TooltipOverlay.super.render(self, dc) +end + +-- map coordinates -> interface layer coordinates +local function GetScreenCoordinates(map_coord) + -- -> map viewport offset + local vp = df.global.world.viewport + local vp_Coord = vp.corner + local map_offset_by_vp = { + x = map_coord.x - vp_Coord.x, + y = map_coord.y - vp_Coord.y, + z = map_coord.z - vp_Coord.z, + } + + if not dfhack.screen.inGraphicsMode() then + return map_offset_by_vp + else + -- -> pixel offset + local gps = df.global.gps + local map_tile_pixels = gps.viewport_zoom_factor // 4; + local screen_coord_px = { + x = map_tile_pixels * map_offset_by_vp.x, + y = map_tile_pixels * map_offset_by_vp.y, + } + -- -> interface layer coordinates + local screen_coord_text = { + x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), + y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), + } + + return screen_coord_text + end +end + +local function GetString(tokens) + local sb = {} + for _, tok in ipairs(tokens) do + if type(tok) == "string" then + sb[#sb+1] = tok + else -- must be a table token + sb[#sb+1] = tok.text + end + end + if not next(sb) then return nil end + return table.concat(sb) +end + +function TooltipOverlay:render_unit_banners(dc) + if not config['tooltip-follow'] then return end + + local hold_to_show = config['tooltip-follow-hold-to-show'] + if hold_to_show and hold_to_show ~= 'none' then + if not dfhack.internal.getModifiers()[hold_to_show] then + return + end + else + local blink_duration = config['tooltip-follow-blink-milliseconds'] + if blink_duration > 0 and not gui.blink_visible(blink_duration) then + return + end + + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + end + + local vp = df.global.world.viewport + local topleft = vp.corner + local width = vp.max_x + local height = vp.max_y + local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} + + local units = dfhack.units.getUnitsInBox(topleft, bottomright, unit_filter) + if not units or #units == 0 then return end + + local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) + local pen = COLOR_WHITE + + local _, screenHeight = dfhack.screen:getWindowSize() + + local used_tiles = {} + -- reverse order yields better offsets for overlapping texts + for i = #units, 1, -1 do + local unit = units[i] + + local posX, posY, posZ = dfhack.units.getPosition(unit) + if not posX then goto continue end + local pos = xyz2pos(posX, posY, posZ) + + local info = GetUnitInfoText(unit, 'follow') + if not info or not next(info) then goto continue end + + local str = GetString(info) + if not str then goto continue end + + local scrPos = GetScreenCoordinates(pos) + local y = scrPos.y - 1 -- subtract 1 to move the text over the heads + local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + + -- do not write anything in the top rows, where DF's interface is. + -- todo: use precise rectangles + if y < 4 then goto continue end + + -- to resolve overlaps, we'll mark every coordinate we write anything in, + -- and then check if the new tooltip will overwrite any used coordinate. + -- if it will, try the next row, to a maximum offset of 4. + local row + local dy = 0 + -- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best + local usedAt = -1 + for yOffset = 0, max_banner_y_offset do + dy = yOffset + + row = used_tiles[y + dy] + if not row then + row = {} + used_tiles[y + dy] = row + end + + usedAt = -1 + for j = 0, #str - 1 do + if row[x + j] then + usedAt = j + break + end + end + + if usedAt == -1 then break end + end -- for dy + -- if other text starts at the same position, or even 2 to the right, + -- we can't place any useful information, and will ignore it instead. + if 0 <= usedAt and usedAt <= 2 then goto continue end + + -- do not write anything over DF's interface + -- todo: use precise rectangles + if y + dy > screenHeight - 4 then goto continue end + + dc:seek(x, y + dy) + local ix = 0 + for _, tok in ipairs(info) do + local s + if type(tok) == "string" then + dc:pen(pen) + s = tok + else + dc:pen(tok.pen) + s = tok.text + end + + -- in case there isn't enough space, cut the text off + local len = #s + if usedAt > 0 and ix + len + 1 >= usedAt then + -- last position we can write is `usedAt - len - ix - 1` + -- we want to replace it with an `_`, so we need another `- 1` + s = s:sub(1, usedAt - len - ix - 1 - 1) .. '_' + + dc:string(s) + break -- nothing more will fit + else + dc:string(s) + end + + ix = ix + len + end + + -- mark coordinates as used + for j = 0, #str - 1 do + row[x + j] = true + end + + ::continue:: + end +end + +-- MouseTooltip is an almost copy&paste of the DimensionsTooltip +MouseTooltip = defclass(MouseTooltip, widgets.ResizingPanel) + +MouseTooltip.ATTRS{ + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, + no_force_pause_badge=true, + auto_width=true, + display_offset={x=3, y=3}, +} + +function MouseTooltip:init() + ensure_key(self, 'frame').w = 17 + self.frame.h = 4 + + self.label = widgets.Label{ + frame={t=0}, + auto_width=true, + } + + self:addviews{ + widgets.Panel{ + -- set minimum size for tooltip frame so the DFHack frame badge fits + frame={t=0, l=0, w=7, h=2}, + }, + self.label, + } +end + +function MouseTooltip:render(dc) + if not config['tooltip-hover'] then return end + + local x, y = dfhack.screen.getMousePos() + if not x then return end + + local pos = dfhack.gui.getMousePos() + local text = GetHoverText(pos) + if not text or not next(text) then return end + self.label:setText(text) + + local sw, sh = dfhack.screen.getWindowSize() + local frame_width = math.max(9, self.label:getTextWidth() + 2) + self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) + self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) + self:updateLayout() + MouseTooltip.super.render(self, dc) +end + +----------------------------- +-- FollowPanelOverlay + +local plotinfo = df.global.plotinfo +local mi = df.global.game.main_interface + +local function follow_panel_is_visible() + return plotinfo.follow_unit > -1 and + mi.current_hover == -1 and + not mi.hover_instructions_on and + not mi.current_hover_alert +end + +FollowPanelOverlay = defclass(FollowPanelOverlay, overlay.OverlayWidget) +FollowPanelOverlay.ATTRS{ + desc='Adds spectate widgets to the vanilla follow panel.', + default_pos={x=6,y=-5}, + viewscreens='dwarfmode/Default', + default_enabled=true, + frame={w=29, h=1}, + visible=follow_panel_is_visible, +} + +function FollowPanelOverlay:init() + self:addviews{ + widgets.Label{ + frame={l=0, t=0, w=3}, + text=(' %s '):format(string.char(27)), + on_click=spectate_followPrev, + }, + widgets.Label{ + frame={l=5, t=0, w=3}, + text=(' %s '):format(string.char(26)), + on_click=spectate_followNext, + }, + widgets.Label{ + frame={l=10, t=0, w=14}, + text={ + ' spectate:', + {text=function() return isEnabled() and ' on ' or 'off ' end, + pen=function() return isEnabled() and COLOR_GREEN or COLOR_LIGHTRED end}, + }, + on_click=function() dfhack.run_command(isEnabled() and 'disable' or 'enable', 'spectate') end, + }, + widgets.ConfigureButton{ + frame={l=26, t=0}, + on_click=function() dfhack.run_script('gui/spectate') end, + } + } +end + +function FollowPanelOverlay:onInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + spectate_followPrev() + return true + elseif keys.KEYBOARD_CURSOR_RIGHT then + spectate_followNext() + return true + end + return FollowPanelOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = { + followpanel=FollowPanelOverlay, + tooltip=TooltipOverlay, +} + +return _ENV diff --git a/plugins/lua/stockflow.lua b/plugins/lua/stockflow.lua index 3b2b0fe669..d1880cf310 100644 --- a/plugins/lua/stockflow.lua +++ b/plugins/lua/stockflow.lua @@ -1,11 +1,31 @@ local _ENV = mkmodule('plugins.stockflow') local gui = require "gui" +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') reaction_list = reaction_list or {} saved_orders = saved_orders or {} jobs_to_create = jobs_to_create or {} +local PERSIST_KEY = 'stockflow/orders' + +local function serialize_orders(orders) + local stored = {} + for spid, spec in pairs(orders) do + stored[tostring(spid)] = { + name = spec.name, + order_number = spec.order_number, + trigger_number = spec.trigger_number, + } + end + return stored +end + +local function persist_orders() + dfhack.persistent.saveSiteData(PERSIST_KEY, serialize_orders(saved_orders)) +end + triggers = { {filled = false, divisor = 1, name = "Per empty space"}, {filled = true, divisor = 1, name = "Per stored item"}, @@ -18,12 +38,6 @@ triggers = { {name = "Never"}, } -entry_ints = { - stockpile_id = 1, - order_number = 2, - trigger_number = 3, -} - FirstRow = 3 CenterCol = 38 ExtraLines = 9 @@ -49,8 +63,8 @@ function clear_caches() jobs_to_create = {} end -function trigger_name(cache) - local trigger = triggers[cache.entry.ints[entry_ints.trigger_number]] +function trigger_name(spec) + local trigger = triggers[spec.trigger_number] return trigger and trigger.name or "Never" end @@ -58,7 +72,7 @@ function list_orders() local listed = false for _, spec in pairs(saved_orders) do local num = spec.stockpile.stockpile_number - local name = spec.entry.value + local name = spec.name local trigger = trigger_name(spec) print("Stockpile #"..num, name, trigger) listed = true @@ -66,12 +80,12 @@ function list_orders() if not listed then print("No manager jobs have been set for your stockpiles.") - print("Use j in a stockpile menu to create one...") + print("Select a stockpile and use the stockflow overlay to create one...") end end --- Save the stockpile jobs for later creation. --- Called when the bookkeeper starts updating stockpile records. +-- Gather the stockpile jobs for later creation. +-- Called at the start of each periodic update. function start_bookkeeping() local result = {} for reaction_id, quantity in pairs(check_stockpiles()) do @@ -84,8 +98,8 @@ function start_bookkeeping() jobs_to_create = result end --- Insert any saved jobs. --- Called when the bookkeeper finishes updating stockpile records. +-- Insert the gathered jobs. +-- Called at the end of each periodic update. function finish_bookkeeping() for reaction, amount in pairs(jobs_to_create) do create_orders(reaction_list[reaction].order, amount) @@ -100,65 +114,75 @@ function stockpile_settings(sp) return "No job selected", "" end - return order.entry.value, trigger_name(order) + return order.name, trigger_name(order) end -- Toggle the trigger condition for a stockpile. function toggle_trigger(sp) local saved = saved_orders[sp.id] if saved then - saved.entry.ints[entry_ints.trigger_number] = (saved.entry.ints[entry_ints.trigger_number] % #triggers) + 1 - saved.entry:save() + saved.trigger_number = (saved.trigger_number % #triggers) + 1 + persist_orders() end end function collect_orders() local result = {} - local entries = dfhack.persistent.get_all("stockflow/entry", true) - if entries then - for _, entry in ipairs(entries) do - local spid = entry.ints[entry_ints.stockpile_id] - local stockpile = df.building.find(spid) - if stockpile then - local order_number = entry.ints[entry_ints.order_number] - if reaction_list[order_number] and entry.value == reaction_list[order_number].name then - result[spid] = { - stockpile = stockpile, - entry = entry, - } - else - -- Todo: Search reaction_list for the name. - -- This can happen when loading an old save in a new version. - -- It's even possible that the reaction has been removed. - local found = false - for number, reaction in ipairs(reaction_list) do - if reaction.name == entry.value then - print("Adjusting stockflow entry for stockpile #"..stockpile.stockpile_number..": "..entry.value.." ("..order_number.." => "..number..")") - entry.ints[entry_ints.order_number] = number - entry:save() - result[spid] = { - stockpile = stockpile, - entry = entry, - } - - found = true - break - end + local dirty = false + local stored = dfhack.persistent.getSiteData(PERSIST_KEY) or {} + for spid_str, spec in pairs(stored) do + local spid = tonumber(spid_str) + local stockpile = spid and df.building.find(spid) + if stockpile and not df.building_stockpilest:is_instance(stockpile) then + stockpile = nil + end + if stockpile then + local order_number = spec.order_number + local trigger_number = spec.trigger_number or 1 + if reaction_list[order_number] and spec.name == reaction_list[order_number].name then + result[spid] = { + stockpile = stockpile, + name = spec.name, + order_number = order_number, + trigger_number = trigger_number, + } + else + -- Search reaction_list for the name. + -- This can happen when loading an old save in a new version. + -- It's even possible that the reaction has been removed. + local found = false + for number, reaction in ipairs(reaction_list) do + if reaction.name == spec.name then + print("Adjusting stockflow entry for stockpile #"..stockpile.stockpile_number..": "..spec.name.." ("..tostring(order_number).." => "..number..")") + result[spid] = { + stockpile = stockpile, + name = spec.name, + order_number = number, + trigger_number = trigger_number, + } + + dirty = true + found = true + break end + end - if not found then - print("Unmatched stockflow entry for stockpile #"..stockpile.stockpile_number..": "..entry.value.." ("..order_number..")") - end + if not found then + print("Unmatched stockflow entry for stockpile #"..stockpile.stockpile_number..": "..tostring(spec.name).." ("..tostring(order_number)..")") + dirty = true end - else - -- The stockpile no longer exists. - -- Perhaps it has been deleted, or perhaps this is a different fortress. - -- print("Missing stockflow pile "..spid) - entry:delete() end + else + -- The stockpile no longer exists. + -- Perhaps it has been deleted, or perhaps this is a different fortress. + dirty = true end end + if dirty then + dfhack.persistent.saveSiteData(PERSIST_KEY, serialize_orders(result)) + end + return result end @@ -178,6 +202,7 @@ function reaction_entry(reactions, job_type, values, name) local order = df.manager_order:new() -- These defaults differ from the newly created order's. order:assign{ + frequency = df.workquota_frequency_type.OneTime, job_type = job_type, item_type = -1, item_subtype = -1, @@ -283,7 +308,7 @@ function collect_reactions() reaction_entry(result, job_types.CatchLiveFish) -- Cutting, encrusting, and metal extraction. - local rock_types = df.global.world.raws.inorganics + local rock_types = df.global.world.raws.inorganics.all for rock_id = #rock_types-1, 0, -1 do local material = rock_types[rock_id].material local rock_name = material.state_adj.Solid @@ -819,7 +844,7 @@ function screen:onRenderBody(dc) dc:string(": Select", COLOR_WHITE) dc:seek(CenterCol, FirstRow + self.page_size + 2) - dc:key("SETUPGAME_SAVE_PROFILE_ABORT"):string(": No order", COLOR_WHITE) + dc:key("CUSTOM_SHIFT_N"):string(": No order", COLOR_WHITE) -- Reaction lines. for _, item in ipairs(self.displayed) do @@ -836,7 +861,7 @@ function screen:onInput(keys) if selected then store_order(self.stockpile, selected.index) end - elseif keys.SETUPGAME_SAVE_PROFILE_ABORT then + elseif keys.CUSTOM_SHIFT_N then self:dismiss() clear_order(self.stockpile) elseif keys.STANDARDSCROLL_UP then @@ -986,10 +1011,9 @@ function screen:refilter() end function clear_order(stockpile) - local saved = saved_orders[stockpile.id] - if saved then - saved.entry:delete() + if saved_orders[stockpile.id] then saved_orders[stockpile.id] = nil + persist_orders() end end @@ -998,23 +1022,17 @@ function store_order(stockpile, order_number) -- print("Setting stockpile #"..stockpile.stockpile_number.." to "..name.." (#"..order_number..")") local saved = saved_orders[stockpile.id] if saved then - saved.entry.value = name - saved.entry.ints[entry_ints.order_number] = order_number - saved.entry:save() + saved.name = name + saved.order_number = order_number else saved_orders[stockpile.id] = { stockpile = stockpile, - entry = dfhack.persistent.save{ - key = "stockflow/entry/"..stockpile.id, - value = name, - ints = { - stockpile.id, - order_number, - 1, - }, - }, + name = name, + order_number = order_number, + trigger_number = 1, } end + persist_orders() end -- Compare the job specification of two orders. @@ -1066,6 +1084,7 @@ end -- Place a new copy of the order onto the manager's queue. function create_orders(order, amount) local new_order = order:new() + new_order.frequency = df.workquota_frequency_type.OneTime amount = math.floor(amount) new_order.amount_left = amount new_order.amount_total = amount @@ -1104,9 +1123,9 @@ end function check_stockpiles(verbose) local result = {} for _, spec in pairs(saved_orders) do - local trigger = triggers[spec.entry.ints[entry_ints.trigger_number]] + local trigger = triggers[spec.trigger_number] if trigger and trigger.divisor then - local reaction = spec.entry.ints[entry_ints.order_number] + local reaction = spec.order_number local filled, empty = check_pile(spec.stockpile, verbose) local amount = math.floor((trigger.filled and filled or empty) / trigger.divisor) result[reaction] = (result[reaction] or 0) + amount @@ -1201,4 +1220,66 @@ function matches_stockpile(item, settings) return true end +-------------------- +-- Overlay +-------------------- + +local function selected_stockpile() + return dfhack.gui.getSelectedStockpile(true) +end + +StockflowOverlay = defclass(StockflowOverlay, overlay.OverlayWidget) +StockflowOverlay.ATTRS{ + desc='Displays and adjusts stockflow work order settings for the selected stockpile.', + default_pos={x=5, y=-7}, + default_enabled=true, + version=1, + viewscreens='dwarfmode/Stockpile/Some/Default', + visible=isEnabled, + frame={w=55, h=4}, +} + +function StockflowOverlay:init() + self:addviews{ + widgets.Panel{ + frame={t=0, l=0, r=0, b=0}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Stockflow', + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0, r=0}, + key='CUSTOM_J', + label=function() + local sp = selected_stockpile() + local name = sp and stockpile_settings(sp) or 'No job selected' + return 'Order: '..name + end, + on_activate=function() + local sp = selected_stockpile() + if sp then select_order(sp) end + end, + }, + widgets.HotkeyLabel{ + frame={t=1, l=0, r=0}, + key='CUSTOM_SHIFT_J', + label=function() + local sp = selected_stockpile() + if not sp then return 'Trigger: Never' end + local _, trigger = stockpile_settings(sp) + if #trigger == 0 then trigger = 'Never' end + return 'Trigger: '..trigger + end, + on_activate=function() + local sp = selected_stockpile() + if sp then toggle_trigger(sp) end + end, + }, + }, + }, + } +end + +OVERLAY_WIDGETS = {stockflow=StockflowOverlay} + return _ENV diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index ac48ed6fc9..518cbf62fb 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -7,8 +7,8 @@ local logistics = require('plugins.logistics') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') -local STOCKPILES_DIR = 'dfhack-config/stockpiles' -local STOCKPILES_LIBRARY_DIR = 'hack/data/stockpiles' +local STOCKPILES_DIR = dfhack.getConfigPath() .. '/stockpiles' +local STOCKPILES_LIBRARY_DIR = dfhack.getHackPath() .. '/data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' @@ -252,15 +252,14 @@ local function get_import_choices() return filenames end -local function do_import() - local sp = dfhack.gui.getSelectedStockpile(true) +local function do_import(make_opts, title) local dlg local function get_dlg() return dlg end dlg = dialogs.ListBox{ - frame_title='Import/Delete Stockpile Settings', + frame_title=title or 'Import/Delete Stockpile Settings', with_filter=true, choices=get_import_choices(), - on_select=function(_, choice) import_settings(choice.text, {id=sp and sp.id}) end, + on_select=function(_, choice) import_settings(choice.text, make_opts()) end, dismiss_on_select2=false, on_select2=function(_, choice) if choice.text:startswith('library/') then return end @@ -281,15 +280,37 @@ local function do_import() }:show() end -local function do_export() - local sp = dfhack.gui.getSelectedStockpile(true) +local function do_export(make_opts, title) dialogs.InputBox{ - frame_title='Export Stockpile Settings', + frame_title=title or 'Export Stockpile Settings', text='Please enter a filename', - on_input=function(text) export_settings(text, {id=sp and sp.id}) end, + on_input=function(text) export_settings(text, make_opts()) end, }:show() end +local function do_stockpile_import() + local sp = dfhack.gui.getSelectedStockpile(true) + do_import(function() return {id=sp and sp.id} end) +end + +local function do_stockpile_export() + local sp = dfhack.gui.getSelectedStockpile(true) + do_export(function() return {id=sp and sp.id} end) +end + +local function get_route_target() + local hsc = df.global.game.main_interface.hauling_stop_conditions + return {route_id=hsc.route_id, stop_id=hsc.stop_id} +end + +local function do_route_import() + do_import(get_route_target, 'Import/Delete Stop Settings') +end + +local function do_route_export() + do_export(get_route_target, 'Export Stop Settings') +end + -------------------- -- ConfigModal -------------------- @@ -448,13 +469,13 @@ function StockpilesOverlay:init() label='import', auto_width=true, key='CUSTOM_CTRL_I', - on_activate=do_import, + on_activate=do_stockpile_import, }, widgets.HotkeyLabel{ frame={t=0, l=16}, label='export', auto_width=true, key='CUSTOM_CTRL_E', - on_activate=do_export, + on_activate=do_stockpile_export, }, widgets.Panel{ frame={t=1, l=0}, @@ -592,6 +613,45 @@ function StockpilesOverlay:onInput(keys) return StockpilesOverlay.super.onInput(self, keys) end -OVERLAY_WIDGETS = {overlay=StockpilesOverlay} +-------------------- +-- HaulingOverlay +-------------------- + +HaulingOverlay = defclass(HaulingOverlay, overlay.OverlayWidget) +HaulingOverlay.ATTRS{ + desc='Shows a panel when minecart route stop conditions are being edited.', + default_pos={x=5, y=40}, + default_enabled=true, + version=1, + viewscreens='dwarfmode/HaulingStopConditions', + frame={w=49, h=3}, +} + +function HaulingOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='main', + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='import', + auto_width=true, + key='CUSTOM_CTRL_I', + on_activate=do_route_import, + }, widgets.HotkeyLabel{ + frame={t=0, l=16}, + label='export', + auto_width=true, + key='CUSTOM_CTRL_E', + on_activate=do_route_export, + }, + }, + }, + } +end + +OVERLAY_WIDGETS = {overlay=StockpilesOverlay, hauling=HaulingOverlay} return _ENV diff --git a/plugins/lua/stocks.lua b/plugins/lua/stocks.lua index 2bfd4bb052..a854311b84 100644 --- a/plugins/lua/stocks.lua +++ b/plugins/lua/stocks.lua @@ -6,15 +6,45 @@ local widgets = require('gui.widgets') local stocks = df.global.game.main_interface.stocks +-- these are only safe to call while the stocks page is open; guard against +-- stray activations as the page closes local function collapse_all() + if not stocks.open then return end local num_sections = #stocks.current_type_a_expanded for idx=0,num_sections-1 do stocks.current_type_a_expanded[idx] = false end stocks.i_height = num_sections * 3 + -- the collapsed list is much shorter; reset the scroll position so the + -- view isn't left pointing past the end of the list + stocks.scroll_position_item = 0 +end + +local function expand_all() + if not stocks.open then return end + local num_sections = #stocks.current_type_a_expanded + for idx=0,num_sections-1 do + stocks.current_type_a_expanded[idx] = true + end + local num_items = #stocks.current_type_i_list + stocks.i_height = (num_items + num_sections) * 3 +end + +local function all_collapsed() + if not stocks.open then return true end + for idx=0,#stocks.current_type_a_expanded-1 do + if stocks.current_type_a_expanded[idx] then return false end + end + return true +end + +local function toggle_all() + if not stocks.open then return end + if all_collapsed() then expand_all() else collapse_all() end end local function remove_empty() + if not stocks.open then return end local empties = {} for itype,v in ipairs(stocks.storeamount) do if v == 0 and stocks.badamount[itype] == 0 then @@ -27,6 +57,10 @@ local function remove_empty() for idx=#stocks.filtered_type_list-1,0,-1 do if empties[stocks.filtered_type_list[idx]] then stocks.filtered_type_list:erase(idx) end end + -- removing types shortens the type list; keep the scroll position in bounds + if stocks.scroll_position_type >= #stocks.filtered_type_list then + stocks.scroll_position_type = math.max(0, #stocks.filtered_type_list - 1) + end end -- ------------------- @@ -48,9 +82,17 @@ function StocksOverlay:init() self:addviews{ widgets.HotkeyLabel{ frame={t=0, l=0}, - label='collapse all', + label=function() + return all_collapsed() and 'expand all' or 'collapse all' + end, key='CUSTOM_CTRL_X', - on_activate=collapse_all, + on_activate=toggle_all, + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='expand all', + key='CUSTOM_CTRL_Z', + on_activate=expand_all, }, widgets.HotkeyLabel{ frame={t=2, l=0}, diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index 594fb31155..c0672f8bfb 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -26,6 +26,27 @@ function isBuildingPlanJob(job) return suspendmanager_isBuildingPlanJob(job) end +--- Return the selected construction job +local function getSelectedBuildingJob() + -- This is not relying on dfhack.gui.getSelectedJob() because we don't want + -- the job of a selected or followed unit, only of a selected building + local building = dfhack.gui.getSelectedBuilding(true) + if not building then + return nil + end + + -- Find if the building is being constructed + for _, job in ipairs(building.jobs) do + if job.job_type == df.job_type.ConstructBuilding + or (job.job_type == df.job_type.DestroyBuilding + and job.flags.suspend) then + return job + end + end + + return nil +end + function runOnce(prevent_blocking, quiet, unsuspend_everything) suspendmanager_runOnce(prevent_blocking, unsuspend_everything) if (not quiet) then @@ -69,7 +90,7 @@ function StatusOverlay:init() end function StatusOverlay:get_status_string() - local job = dfhack.gui.getSelectedJob(true) + local job = getSelectedBuildingJob() if job and job.flags.suspend then return "Suspended because: " .. suspendmanager_suspensionDescription(job) .. "." end @@ -77,8 +98,11 @@ function StatusOverlay:get_status_string() end function StatusOverlay:render(dc) - local job = dfhack.gui.getSelectedJob(true) - if not job or job.job_type ~= df.job_type.ConstructBuilding or not isEnabled() or isBuildingPlanJob(job) then + if not isEnabled() then + return + end + local job = getSelectedBuildingJob() + if not job or isBuildingPlanJob(job) then return end StatusOverlay.super.render(self, dc) @@ -110,8 +134,8 @@ function ToggleOverlay:init() end function ToggleOverlay:shouldRender() - local job = dfhack.gui.getSelectedJob(true) - return job and job.job_type == df.job_type.ConstructBuilding and not isBuildingPlanJob(job) + local job = getSelectedBuildingJob() + return job and not isBuildingPlanJob(job) end function ToggleOverlay:render(dc) @@ -133,19 +157,25 @@ end -- suspend overlay (formerly in unsuspend.lua) -local textures = dfhack.textures.loadTileset('hack/data/art/unsuspend.png', 32, 32, true) +local textures = dfhack.textures.loadTileset(dfhack.getHackPath()..'/data/art/unsuspend.png', 32, 32, true) local ok, buildingplan = pcall(require, 'plugins.buildingplan') if not ok then buildingplan = nil end +local function show_suspend_overlay() + return dfhack.screen.inGraphicsMode() or + dfhack.gui.matchFocusString('dwarfmode/Default', dfhack.gui.getDFViewscreen(true)) +end + SuspendOverlay = defclass(SuspendOverlay, overlay.OverlayWidget) SuspendOverlay.ATTRS{ desc='Annotates suspended buildings with a visible marker.', viewscreens='dwarfmode', default_enabled=true, frame={w=0, h=0}, + visible=show_suspend_overlay, overlay_onupdate_max_freq_seconds=30, } diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua index 46f3e712d9..d32a4b7570 100644 --- a/plugins/lua/tailor.lua +++ b/plugins/lua/tailor.lua @@ -17,6 +17,7 @@ end function status() dfhack.print(('tailor is %s'):format(isEnabled() and "enabled" or "disabled")) print((' %s confiscating tattered clothing'):format(tailor_getConfiscate() and "and" or "but not")) + print(('tailor %s automating dye'):format(tailor_getAutomateDye() and "is" or "is not")) print('materials preference order:') for _,name in ipairs(tailor_getMaterialPreferences()) do print((' %s'):format(name)) @@ -38,6 +39,11 @@ function setConfiscate(opt) tailor_setConfiscate(fl) end +function setAutomateDye(opt) + local fl = argparse.boolean(opt[1], "set dye") + tailor_setAutomateDye(fl) +end + function parse_commandline(...) local args, opts = {...}, {} local positionals = process_args(opts, args) @@ -55,6 +61,8 @@ function parse_commandline(...) setMaterials(positionals) elseif command == 'confiscate' then setConfiscate(positionals) + elseif command == 'dye' then + setAutomateDye(positionals) else return false end diff --git a/plugins/lua/timestream.lua b/plugins/lua/timestream.lua index 6f5b68ddf6..91398e355d 100644 --- a/plugins/lua/timestream.lua +++ b/plugins/lua/timestream.lua @@ -15,7 +15,7 @@ local function do_set(setting_name, arg) if setting_name ~= 'fps' or not numarg then qerror('must specify setting and value') end - timestream_setFps(arg) + timestream_setFps(numarg) print(('set %s to %s'):format(setting_name, timestream_getFps())) end diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 92940ef48e..2923f8e333 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -989,7 +989,9 @@ local function get_zone_status(unit_or_vermin, bld_assignments) return PASTURE_STATUS.ASSIGNED_HERE.value else local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return PASTURE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return PASTURE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return PASTURE_STATUS.PITTED.value @@ -1150,7 +1152,9 @@ local function get_cage_status(unit_or_vermin, bld_assignments) local assigned_zone_ref = get_general_ref(unit_or_vermin, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) if assigned_zone_ref then local civzone = df.building.find(assigned_zone_ref.building_id) - if civzone.type == df.civzone_type.Pen then + if not civzone or not df.building_civzonest:is_instance(civzone) then + return CAGE_STATUS.NONE.value + elseif civzone.type == df.civzone_type.Pen then return CAGE_STATUS.PASTURED.value elseif civzone.type == df.civzone_type.Pond then return CAGE_STATUS.PITTED.value diff --git a/plugins/misery.cpp b/plugins/misery.cpp index b81a4f1daa..7ecdb0d344 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -48,7 +48,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -61,19 +61,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -179,7 +179,7 @@ static void do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); int strength = STRENGTH_MULTIPLIER * config.get_int(CONFIG_FACTOR); diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp index 64cfcc301b..e471ac20d3 100644 --- a/plugins/nestboxes.cpp +++ b/plugins/nestboxes.cpp @@ -1,12 +1,15 @@ #include "Debug.h" #include "PluginManager.h" +#include "modules/Burrows.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/Persistence.h" #include "modules/World.h" +#include "df/buildingitemst.h" #include "df/building_nest_boxst.h" +#include "df/burrow.h" #include "df/item.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -33,6 +36,7 @@ static PersistentDataItem config; enum ConfigValues { CONFIG_IS_ENABLED = 0, + CONFIG_BURROW = 1 }; static const int32_t CYCLE_TICKS = 7; // need to react quickly when eggs are laid/unforbidden @@ -40,27 +44,34 @@ static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static void do_cycle(color_ostream &out); +static command_result do_command(color_ostream &out, std::vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); + + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Protect fertile eggs incubating in a nestbox.", + do_command)); return CR_OK; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) do_cycle(out); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -68,7 +79,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -84,7 +95,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -93,7 +104,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -107,16 +118,78 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } +///////////////////////////////////////////////////// +// configuration logic +// + +static void setBurrow(color_ostream &out, const string &burrow_name){ + auto burrow = Burrows::findByName(burrow_name); + if (burrow) { + config.set_int(CONFIG_BURROW, burrow->id); + } else { + config.set_int(CONFIG_BURROW, -1); + } +} + +static df::burrow* getBurrow(color_ostream &out){ + int id = config.get_int(CONFIG_BURROW); + auto burrow = df::burrow::find(id); + if (!burrow) { + config.set_int(CONFIG_BURROW, -1); + } + return burrow; +} + +static void printStatus(color_ostream &out){ + if (!is_enabled) + out.print("{} is disabled\n", plugin_name); + else { + out.print("{} is enabled\n", plugin_name); + auto burrow = getBurrow(out); + if (burrow) + { + out.print("only protecting eggs inside burrow: {}\n", burrow->name); + } + else + { + out.print("protecting all fertile eggs\n"); + } + } +} + +static command_result do_command(color_ostream &out, std::vector ¶meters){ + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (parameters.size() == 0 || parameters[0] == "status") + { + printStatus(out); + return CR_OK; + } else if (parameters.size() > 1 && parameters[0] == "burrow") { + setBurrow(out, parameters[1]); + return CR_OK; + } else if (parameters[0] == "all") { + config.set_int(CONFIG_BURROW, -1); + return CR_OK; + } else { + return CR_WRONG_USAGE; + } +} ///////////////////////////////////////////////////// // cycle logic // static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; + // see if egg protection is limited to a burrow + auto burrow = getBurrow(out); + for (df::building_nest_boxst *nb : world->buildings.other.NEST_BOX) { for (auto &contained_item : nb->contained_items) { if (contained_item->use_mode == df::building_item_role_type::PERM) @@ -125,6 +198,9 @@ static void do_cycle(color_ostream &out) { bool fertile = item->egg_flags.bits.fertile; if (item->flags.bits.forbid == fertile) continue; + if (burrow && !Burrows::isAssignedTile(burrow, Items::getPosition(item))) { + continue; + } item->flags.bits.forbid = fertile; if (fertile && item->flags.bits.in_job) { // cancel any job involving the egg @@ -133,7 +209,7 @@ static void do_cycle(color_ostream &out) { if (sref && sref->data.job) Job::removeJob(sref->data.job); } - out.print("nestboxes: %d eggs %s\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); + out.print("nestboxes: {} eggs {}\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); } } } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index 89df783245..174c645454 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "modules/Filesystem.h" +#include "modules/Job.h" #include "modules/Materials.h" #include "modules/World.h" @@ -44,8 +45,15 @@ DFHACK_PLUGIN("orders"); REQUIRE_GLOBAL(world); -static const std::string ORDERS_DIR = "dfhack-config/orders"; -static const std::string ORDERS_LIBRARY_DIR = "hack/data/orders"; +static const std::filesystem::path get_orders_dir() +{ + return Core::getInstance().getConfigPath() / "orders"; +} + +static std::filesystem::path get_orders_library_dir() +{ + return Core::getInstance().getHackPath() / "data" / "orders"; +} static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -93,7 +101,7 @@ static command_result orders_command(color_ostream & out, std::vector files; - if (0 < Filesystem::listdir_recursive(ORDERS_LIBRARY_DIR, files, 0, false)) { + std::map files; + if (0 < Filesystem::listdir_recursive(get_orders_library_dir(), files, 0, false)) { // if the library directory doesn't exist, just skip it return; } @@ -145,15 +153,15 @@ static void list_library(color_ostream &out) { return; } - for (auto it : files) + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << "library/" << name << std::endl; + auto sname = name.stem(); + out << Filesystem::as_string("library" / sname) << std::endl; } } @@ -162,17 +170,17 @@ static command_result orders_list_command(color_ostream & out) // use listdir_recursive instead of listdir even though orders doesn't // support subdirs so we can identify and ignore subdirs with ".json" names. // also listdir_recursive will alphabetize the list for us. - std::map files; - Filesystem::listdir_recursive(ORDERS_DIR, files, 0, false); + std::map files; + Filesystem::listdir_recursive(get_orders_dir(), files, 0, false); - for (auto it : files) { + for (auto& it : files) { if (it.second) continue; // skip directories - std::string name = it.first; - if (name.length() <= 5 || name.rfind(".json") != name.length() - 5) + std::filesystem::path name = it.first; + if (name.extension() != ".json") continue; // skip non-.json files - name.resize(name.length() - 5); - out << name << std::endl; + auto sname = name.stem(); + out << sname.string() << std::endl; } list_library(out); @@ -376,6 +384,7 @@ static command_result orders_export_command(color_ostream & out, const std::stri order["art"] = art; } + order["name"] = Job::getManagerOrderName(it); order["amount_left"] = it->amount_left; order["amount_total"] = it->amount_total; order["is_validated"] = bool(it->status.bits.validated); @@ -504,9 +513,9 @@ static command_result orders_export_command(color_ostream & out, const std::stri orders.append(order); } - Filesystem::mkdir(ORDERS_DIR); + Filesystem::mkdir(get_orders_dir()); - std::ofstream file(ORDERS_DIR + "/" + name + ".json"); + std::ofstream file(get_orders_dir() / ( name + ".json")); file << orders << std::endl; @@ -765,8 +774,8 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) if (it2.isMember("bearing")) { std::string bearing(it2["bearing"].asString()); - auto found = std::find_if(world->raws.inorganics.begin(), world->raws.inorganics.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); - if (found == world->raws.inorganics.end()) + auto found = std::find_if(world->raws.inorganics.all.begin(), world->raws.inorganics.all.end(), [bearing](df::inorganic_raw *raw) -> bool { return raw->id == bearing; }); + if (found == world->raws.inorganics.all.end()) { delete condition; @@ -774,7 +783,7 @@ static command_result orders_import(color_ostream &out, Json::Value &orders) continue; } - condition->metal_ore = found - world->raws.inorganics.begin(); + condition->metal_ore = found - world->raws.inorganics.all.begin(); } if (it2.isMember("reaction_class")) @@ -924,8 +933,7 @@ static command_result orders_import_command(color_ostream & out, const std::stri return CR_WRONG_USAGE; } - const std::string filename((is_library ? ORDERS_LIBRARY_DIR : ORDERS_DIR) + - "/" + fname + ".json"); + auto filename((is_library ? get_orders_library_dir() : get_orders_dir()) / (fname + ".json")); Json::Value orders; { @@ -1008,8 +1016,8 @@ static bool orders_compare(df::manager_order *a, df::manager_order *b) return a->workshop_id >= 0; } - if (a->frequency == df::manager_order::T_frequency::OneTime - || b->frequency == df::manager_order::T_frequency::OneTime) + if (a->frequency == df::workquota_frequency_type::OneTime + || b->frequency == df::workquota_frequency_type::OneTime) return a->frequency < b->frequency; return a->frequency > b->frequency; } diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index 580ca3fec6..85e5cce882 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -57,7 +57,7 @@ static int32_t interfacePct = 100; static void overlay_interpose_lua(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(event).print("calling overlay lua function: '%s'\n", fn_name); + DEBUG(event).print("calling overlay lua function: '{}'\n", fn_name); color_ostream & out = Core::getInstance().getConsole(); auto L = DFHack::Core::getInstance().getLuaState(); @@ -159,7 +159,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { Lua::CallLuaModuleFunction(out, "plugins.overlay", "rescan"); } - DEBUG(control).print("%sing interpose hooks\n", enable ? "enabl" : "disabl"); + DEBUG(control).print("{}ing interpose hooks\n", enable ? "enabl" : "disabl"); if (INTERPOSE_HOOKS_FAILED(adopt_region) || // INTERPOSE_HOOKS_FAILED(adventure_log) || diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index 3bf9e2c010..3fd1eaaad1 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -1,3 +1,5 @@ +#include + #include "Debug.h" #include "Error.h" #include "PluginManager.h" @@ -40,7 +42,7 @@ namespace DFHack { static std::vector textures; DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32, true); + textures = Textures::loadTileset(Core::getInstance().getHackPath() / "data" / "art" / "pathable.png", 32, 32, true); return CR_OK; } @@ -84,20 +86,20 @@ static void paint_screen(const PaintCtx & ctx, const unordered_set & continue; } - DEBUG(log).print("scanning map tile at offset %d, %d\n", x, y); + DEBUG(log).print("scanning map tile at offset {}, {}\n", x, y); Screen::Pen cur_tile = Screen::readTile(x, y, true); - DEBUG(log).print("tile data: ch=%d, fg=%d, bg=%d, bold=%s\n", + DEBUG(log).print("tile data: ch={}, fg={}, bg={}, bold={}\n", cur_tile.ch, cur_tile.fg, cur_tile.bg, cur_tile.bold ? "true" : "false"); - DEBUG(log).print("tile data: tile=%d, tile_mode=%d, tile_fg=%d, tile_bg=%d\n", - cur_tile.tile, cur_tile.tile_mode, cur_tile.tile_fg, cur_tile.tile_bg); + DEBUG(log).print("tile data: tile={}, tile_mode={}, tile_fg={}, tile_bg={}\n", + cur_tile.tile, static_cast(cur_tile.tile_mode), cur_tile.tile_fg, cur_tile.tile_bg); if (!cur_tile.valid()) { - DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); + DEBUG(log).print("cannot read tile at offset {}, {}\n", x, y); continue; } bool can_walk = get_can_walk(map_pos); - DEBUG(log).print("tile is %swalkable at offset %d, %d\n", + DEBUG(log).print("tile is {}walkable at offset {}, {}\n", can_walk ? "" : "not ", x, y); if (ctx.use_graphics) { @@ -146,7 +148,7 @@ static bool get_depot_coords(color_ostream &out, unordered_set * depo depot_coords->clear(); for (auto bld : world->buildings.other.TRADE_DEPOT){ - DEBUG(log,out).print("found depot at (%d, %d, %d)\n", bld->centerx, bld->centery, bld->z); + DEBUG(log,out).print("found depot at ({}, {}, {})\n", bld->centerx, bld->centery, bld->z); depot_coords->emplace(bld->centerx, bld->centery, bld->z); } @@ -161,7 +163,7 @@ static bool get_pathability_groups(color_ostream &out, unordered_set * for (auto pos : depot_coords) { auto wgroup = Maps::getWalkableGroup(pos); if (wgroup) { - DEBUG(log,out).print("walkability group at (%d, %d, %d) is %d\n", pos.x, pos.y, pos.z, wgroup); + DEBUG(log,out).print("walkability group at ({}, {}, {}) is {}\n", pos.x, pos.y, pos.z, wgroup); depot_pathability_groups->emplace(wgroup); } } @@ -384,7 +386,7 @@ static bool wagon_flood(color_ostream &out, unordered_set * wagon_pat df::coord pos = ctx.search_edge.top(); ctx.search_edge.pop(); - TRACE(log,out).print("checking tile: (%d, %d, %d); pathability group: %d\n", pos.x, pos.y, pos.z, + TRACE(log,out).print("checking tile: ({}, {}, {}); pathability group: {}\n", pos.x, pos.y, pos.z, Maps::getWalkableGroup(pos)); if (entry_tiles.contains(pos)) { diff --git a/plugins/pet-uncapper.cpp b/plugins/pet-uncapper.cpp index 168c5f0bfd..0370015dcb 100644 --- a/plugins/pet-uncapper.cpp +++ b/plugins/pet-uncapper.cpp @@ -119,22 +119,22 @@ void impregnateMany(color_ostream &out, bool verbose = false) { } if (pregnancies || verbose) { - INFO(cycle, out).print("%d pet pregnanc%s initiated\n", + INFO(cycle, out).print("{} pet pregnanc{} initiated\n", pregnancies, pregnancies == 1 ? "y" : "ies"); } } command_result do_command(color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0 || parameters[0] == "status") { - out.print("%s is %s\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); - out.print("population cap per species: %d\n", config.get_int(CONFIG_POP_CAP)); - out.print("updating pregnancies every %d ticks\n", config.get_int(CONFIG_FREQ)); - out.print("pregancies last %d ticks\n", config.get_int(CONFIG_PREG_TIME)); + out.print("{} is {}\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); + out.print("population cap per species: {}\n", config.get_int(CONFIG_POP_CAP)); + out.print("updating pregnancies every {} ticks\n", config.get_int(CONFIG_FREQ)); + out.print("pregancies last {} ticks\n", config.get_int(CONFIG_PREG_TIME)); } else if (parameters[0] == "now") { impregnateMany(out, true); } else { @@ -165,19 +165,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectorflags.is_set(plant_raw_flags::WET)) || (!is_watery && !p_raw->flags.is_set(plant_raw_flags::DRY))) { - out.printerr("Can't create plant: Plant type can't grow this %s water feature!\n" + out.printerr("Can't create plant: Plant type can't grow this {} water feature!\n" "Override with --force\n", is_watery ? "close to" : "far from"); return CR_FAILURE; } @@ -297,7 +297,7 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -332,9 +332,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt auto tt = Maps::getTileType(plant->pos); if (!tt || tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Invalid sapling tiletype at (%d, %d, %d): %s!\n", + out.printerr("Invalid sapling tiletype at ({}, {}, {}): {}\n", plant->pos.x, plant->pos.y, plant->pos.z, - tt ? ENUM_KEY_STR(tiletype, *tt).c_str() : "No map block!"); + tt ? ENUM_KEY_STR(tiletype, *tt) : "No map block!"); continue; // Bad tiletype } else if (*tt == tiletype::SaplingDead) @@ -349,9 +349,9 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt } if (do_trees) - out.print("%d saplings and %d trees%s set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); + out.print("{} saplings and {} trees{} set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); else - out.print("%d saplings%s set to grow.\n", grown, options.dry_run ? " would be" : ""); + out.print("{} saplings{} set to grow.\n", grown, options.dry_run ? " would be" : ""); return CR_OK; } @@ -426,7 +426,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } @@ -473,9 +473,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SHRUB) { - out.printerr("Bad shrub tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad shrub tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -483,9 +483,9 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl { if (tileShape(*tt) != tiletype_shape::SAPLING) { - out.printerr("Bad sapling tiletype at (%d, %d, %d): %s\n", + out.printerr("Bad sapling tiletype at ({}, {}, {}): {}\n", plant.pos.x, plant.pos.y, plant.pos.z, - ENUM_KEY_STR(tiletype, *tt).c_str()); + ENUM_KEY_STR(tiletype, *tt)); bad_tt = true; } } @@ -493,7 +493,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } else { - out.printerr("Bad plant tiletype at (%d, %d, %d): No map block!\n", + out.printerr("Bad plant tiletype at ({}, {}, {}): No map block!\n", plant.pos.x, plant.pos.y, plant.pos.z); bad_tt = true; } @@ -508,7 +508,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl if (!options.dry_run) { if (!uncat_plant(&plant)) - out.printerr("Remove plant: No block column at (%d, %d)!\n", plant.pos.x, plant.pos.y); + out.printerr("Remove plant: No block column at ({}, {})!\n", plant.pos.x, plant.pos.y); if (!bad_tt) // TODO: trees set_tt(plant.pos); @@ -518,7 +518,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl } } - out.print("Plants%s removed: %d (%d bad)\n", options.dry_run ? " that would be" : "", count, count_bad); + out.print("Plants{} removed: {} ({} bad)\n", options.dry_run ? " that would be" : "", count, count_bad); return CR_OK; } @@ -538,11 +538,11 @@ command_result df_plant(color_ostream &out, vector ¶meters) { // Print all non-grass raw IDs ("plant list") out.print("--- Shrubs ---\n"); for (auto p_raw : world->raws.plants.bushes) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); out.print("\n--- Saplings ---\n"); for (auto p_raw : world->raws.plants.trees) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } @@ -574,7 +574,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) return CR_FAILURE; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (!Core::getInstance().isMapLoaded()) @@ -599,7 +599,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) if (!pos_1.isValid()) { // Attempt to use cursor for pos if active Gui::getCursorCoords(pos_1); - DEBUG(log, out).print("Try to use cursor (%d, %d, %d) for pos_1.\n", + DEBUG(log, out).print("Try to use cursor ({}, {}, {}) for pos_1.\n", pos_1.x, pos_1.y, pos_1.z); if (!pos_1.isValid()) @@ -609,20 +609,20 @@ command_result df_plant(color_ostream &out, vector ¶meters) } } - DEBUG(log, out).print("plant_idx = %d\n", options.plant_idx); + DEBUG(log, out).print("plant_idx = {}\n", options.plant_idx); auto p_raw = vector_get(world->raws.plants.all, options.plant_idx); if (p_raw) { - DEBUG(log, out).print("Plant raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Plant raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw was grass: %d (%s)\n", options.plant_idx, p_raw->id.c_str()); + out.printerr("Plant raw was grass: {} ({})\n", options.plant_idx, p_raw->id); return CR_FAILURE; } } else { - out.printerr("Plant raw not found for create: %d\n", options.plant_idx); + out.printerr("Plant raw not found for create: {}\n", options.plant_idx); return CR_FAILURE; } } @@ -638,24 +638,24 @@ command_result df_plant(color_ostream &out, vector ¶meters) for (auto idx : filter) { - DEBUG(log, out).print("Filter/exclude test idx: %d\n", idx); + DEBUG(log, out).print("Filter/exclude test idx: {}\n", idx); auto p_raw = vector_get(world->raws.plants.all, idx); if (p_raw) { - DEBUG(log, out).print("Filter/exclude raw: %s\n", p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude raw: {}\n", p_raw->id); if (p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Filter/exclude plant raw was grass: %d (%s)\n", idx, p_raw->id.c_str()); + out.printerr("Filter/exclude plant raw was grass: {} ({})\n", idx, p_raw->id); return CR_FAILURE; } else if (options.grow && !p_raw->flags.is_set(plant_raw_flags::TREE)) { // User might copy-paste filters between grow and remove, so just log this - DEBUG(log, out).print("Filter/exclude shrub with grow: %d (%s)\n", idx, p_raw->id.c_str()); + DEBUG(log, out).print("Filter/exclude shrub with grow: {} ({})\n", idx, p_raw->id); } } else { - out.printerr("Plant raw not found for filter/exclude: %d\n", idx); + out.printerr("Plant raw not found for filter/exclude: {}\n", idx); return CR_FAILURE; } } @@ -689,12 +689,12 @@ command_result df_plant(color_ostream &out, vector ¶meters) bounds.addPos(world->map.x_count-1, world->map.y_count-1, 0); } - DEBUG(log, out).print("bounds = (%d:%d, %d:%d, %d:%d)\n", + DEBUG(log, out).print("bounds = ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); if (!bounds.isValid()) { - out.printerr("Invalid cuboid! (%d:%d, %d:%d, %d:%d)\n", + out.printerr("Invalid cuboid! ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); return CR_FAILURE; } diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 52f6151637..eca7b888de 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -73,7 +73,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, "Manage room assignments for off-map units and noble roles.", @@ -92,12 +92,12 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { EventManager::unregisterAll(plugin_self); } - DEBUG(control, out).print("now %s\n", is_enabled ? "enabled" : "disabled"); + DEBUG(control, out).print("now {}\n", is_enabled ? "enabled" : "disabled"); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -265,7 +265,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!World::isFortressMode() || !Core::getInstance().isMapLoaded()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -315,13 +315,14 @@ static void assign_nobles(color_ostream &out) { auto zone = virtual_cast(df::building::find(zone_id)); if (!zone) continue; + auto owner = Buildings::getOwner(zone); bool assigned = false; for (auto it = group_codes.rbegin(); it != group_codes.rend(); it++) { auto code = *it; vector units; Units::getUnitsByNobleRole(units, code); // if zone is already assigned to a proper unit (or their spouse), skip - if (linear_index(units, zone->assigned_unit) >= 0 || + if (linear_index(units, owner) >= 0 || linear_index(get_spouse_ids(out, units), zone->assigned_unit_id) >= 0) { assigned = true; @@ -343,17 +344,17 @@ static void assign_nobles(color_ostream &out) { zone->spec_sub_flag.bits.active = true; Buildings::setOwner(zone, unit); assigned = true; - INFO(cycle,out).print("preserve-rooms: assigning %s to a %s-associated %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str(), - toLower_cp437(code).c_str(), - ENUM_KEY_STR(civzone_type, zone->type).c_str()); + INFO(cycle,out).print("preserve-rooms: assigning {} to a {}-associated {}\n", + DF2CONSOLE(Units::getReadableName(unit)), + toLower_cp437(code), + ENUM_KEY_STR(civzone_type, zone->type)); break; } if (assigned) break; } - if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit)) { - DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: %d\n", zone_id); + if (!assigned && (zone->spec_sub_flag.bits.active || zone->assigned_unit_id != -1)) { + DEBUG(cycle,out).print("noble zone now reserved for eventual office holder: {}\n", zone_id); zone->spec_sub_flag.bits.active = false; Buildings::setOwner(zone, NULL); } @@ -402,8 +403,8 @@ static void scrub_reservations(color_ostream &out) { continue; } } - DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid %d: %s\n", hfid, - hf ? DF2CONSOLE(Units::getReadableName(hf)).c_str() : "culled"); + DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid {}: {}\n", hfid, + hf ? DF2CONSOLE(Units::getReadableName(hf)) : "culled"); hfids_to_scrub.push_back(hfid); for (int32_t zone_id : zone_ids) { if (scrub_id_from_entries(hfid, zone_id, reserved_zones)) { @@ -460,9 +461,9 @@ static void handle_missing_assignments(color_ostream &out, if (spouse_hf && share_with_spouse) { if (spouse && Units::isActive(spouse) && !Units::isDead(spouse) && active_unit_ids.contains(spouse->id)) { - DEBUG(cycle,out).print("assigning zone %d (%s) to spouse %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), - DF2CONSOLE(Units::getReadableName(spouse)).c_str()); + DEBUG(cycle,out).print("assigning zone {} ({}) to spouse {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), + DF2CONSOLE(Units::getReadableName(spouse))); Buildings::setOwner(zone, spouse); continue; } @@ -470,22 +471,22 @@ static void handle_missing_assignments(color_ostream &out, if (hf->died_year > -1) continue; // register the hf ids for reassignment and reserve the room - DEBUG(cycle,out).print("registering primary unit for reassignment to zone %d (%s): %d %s\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), hf->unit_id, - DF2CONSOLE(Units::getReadableName(hf)).c_str()); + DEBUG(cycle,out).print("registering primary unit for reassignment to zone {} ({}): {} {}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), hf->unit_id, + DF2CONSOLE(Units::getReadableName(hf))); pending_reassignment[hfid].push_back(zone_id); reserved_zones[zone_id].push_back(hfid); if (share_with_spouse && spouse) { - DEBUG(cycle,out).print("registering spouse unit for reassignment to zone %d (%s): hfid=%d\n", - zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), spouse_hfid); + DEBUG(cycle,out).print("registering spouse unit for reassignment to zone {} ({}): hfid={}\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type), spouse_hfid); pending_reassignment[spouse_hfid].push_back(zone_id); reserved_zones[zone_id].push_back(spouse_hfid); } - INFO(cycle,out).print("preserve-rooms: reserving %s for the return of %s%s%s\n", - toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)).c_str(), - DF2CONSOLE(Units::getReadableName(hf)).c_str(), + INFO(cycle,out).print("preserve-rooms: reserving {} for the return of {}{}\n", + toLower_cp437(ENUM_KEY_STR(civzone_type, zone->type)), + DF2CONSOLE(Units::getReadableName(hf)), spouse_hf ? " or their spouse, " : "", - spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)).c_str() : ""); + spouse_hf ? DF2CONSOLE(Units::getReadableName(spouse_hf)) : ""); zone->spec_sub_flag.bits.active = false; } @@ -503,14 +504,19 @@ static void process_rooms(color_ostream &out, for (auto zone : vec) { auto idx = linear_index(df::global::world->buildings.all, (df::building*)(zone)); if (idx == -1) { - WARN(cycle, out).print("invalid building pointer %p in building vector\n", zone); + WARN(cycle, out).print("invalid building pointer {} in building vector\n", static_cast(zone)); continue; } - if (!zone->assigned_unit) { + if (zone->assigned_unit_id == -1) { handle_missing_assignments(out, active_unit_ids, &it, it_end, share_with_spouse, zone->id); continue; } - auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); + auto owner = Buildings::getOwner(zone); + if (!owner) { + DEBUG(cycle, out).print("building {} has owner id {} but no such unit exists\n", zone->id, zone->assigned_unit_id); + continue; + } + auto hf = df::historical_figure::find(owner->hist_figure_id); if (!hf) continue; int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; @@ -524,7 +530,7 @@ static void process_rooms(color_ostream &out, static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; - TRACE(cycle,out).print("running %s cycle\n", plugin_name); + TRACE(cycle,out).print("running {} cycle\n", plugin_name); if (config.get_bool(CONFIG_TRACK_MISSIONS)) { unordered_set active_unit_ids; @@ -536,7 +542,7 @@ static void do_cycle(color_ostream &out) { process_rooms(out, active_unit_ids, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); process_rooms(out, active_unit_ids, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); - DEBUG(cycle,out).print("tracking zone assignments: bedrooms: %zd, offices: %zd, dining halls: %zd, tombs: %zd\n", + DEBUG(cycle,out).print("tracking zone assignments: bedrooms: {}, offices: {}, dining halls: {}, tombs: {}\n", last_known_assignments_bedroom.size(), last_known_assignments_office.size(), last_known_assignments_dining.size(), last_known_assignments_tomb.size()); @@ -578,24 +584,24 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit || unit->hist_figure_id < 0) return; - TRACE(event,out).print("unit %d (%s) arrived on map (hfid: %d, in pending: %d)\n", - unit->id, DF2CONSOLE(Units::getReadableName(unit)).c_str(), unit->hist_figure_id, + TRACE(event,out).print("unit {} ({}) arrived on map (hfid: {}, in pending: {})\n", + unit->id, DF2CONSOLE(Units::getReadableName(unit)), unit->hist_figure_id, pending_reassignment.contains(unit->hist_figure_id)); auto hfid = unit->hist_figure_id; auto it = pending_reassignment.find(hfid); if (it == pending_reassignment.end()) return; - INFO(event,out).print("preserve-rooms: restoring room ownership for %s\n", - DF2CONSOLE(Units::getReadableName(unit)).c_str()); + INFO(event,out).print("preserve-rooms: restoring room ownership for {}\n", + DF2CONSOLE(Units::getReadableName(unit))); for (auto zone_id : it->second) { reserved_zones.erase(zone_id); auto zone = virtual_cast(df::building::find(zone_id)); if (!zone) continue; zone->spec_sub_flag.bits.active = true; - if (zone->assigned_unit || spouse_has_sharable_room(out, hfid, zone->type)) + if (zone->assigned_unit_id != -1 || spouse_has_sharable_room(out, hfid, zone->type)) continue; - DEBUG(event,out).print("reassigning zone %d\n", zone->id); + DEBUG(event,out).print("reassigning zone {}\n", zone->id); Buildings::setOwner(zone, unit); } pending_reassignment.erase(it); @@ -611,8 +617,8 @@ static void preserve_rooms_cycle(color_ostream &out) { } static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control,out).print("preserve_rooms_setFeature: enabled=%d, feature=%s\n", - enabled, feature.c_str()); + DEBUG(control,out).print("preserve_rooms_setFeature: enabled={}, feature={}\n", + enabled, feature); if (feature == "track-missions") { config.set_bool(CONFIG_TRACK_MISSIONS, enabled); if (is_enabled && enabled) @@ -627,7 +633,7 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f } static bool preserve_rooms_getFeature(color_ostream &out, string feature) { - TRACE(control,out).print("preserve_rooms_getFeature: feature=%s\n", feature.c_str()); + TRACE(control,out).print("preserve_rooms_getFeature: feature={}\n", feature); if (feature == "track-missions") return config.get_bool(CONFIG_TRACK_MISSIONS); if (feature == "track-roles") @@ -636,7 +642,7 @@ static bool preserve_rooms_getFeature(color_ostream &out, string feature) { } static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { - DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature=%s\n", feature.c_str()); + DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature={}\n", feature); if (feature == "track-missions") { vector zone_ids; std::transform(reserved_zones.begin(), reserved_zones.end(), std::back_inserter(zone_ids), [](auto & elem){ return elem.first; }); @@ -661,7 +667,7 @@ static int preserve_rooms_assignToRole(lua_State *L) { virtual_cast(df::building::find(zone_id)); if (!zone) return 0; - DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id=%d\n", zone->id); + DEBUG(control,*out).print("preserve_rooms_assignToRole: zone_id={}\n", zone->id); vector group_codes; Lua::GetVector(L, group_codes); @@ -680,22 +686,30 @@ static int preserve_rooms_assignToRole(lua_State *L) { return 0; } -static string preserve_rooms_getRoleAssignment(color_ostream &out) { - auto zone = Gui::getSelectedCivZone(out, true); +static string get_role_assignment(color_ostream &out, df::building_civzonest * zone) { if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getRoleAssignment: zone_id=%d\n", zone->id); + TRACE(control,out).print("get_role_assignment: zone_id={}\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end() || it->second.empty()) return ""; return it->second[0]; } + +static string preserve_rooms_getRoleAssignmentForZone(color_ostream &out, df::building_civzonest * zone) { + return get_role_assignment(out, zone); +} + +static string preserve_rooms_getRoleAssignment(color_ostream &out) { + return get_role_assignment(out, Gui::getSelectedCivZone(out, true)); +} + static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - TRACE(control,out).print("preserve_rooms_isReserved: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_isReserved: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); return it != reserved_zones.end() && it->second.size() > 0; } @@ -704,7 +718,7 @@ static string preserve_rooms_getReservationName(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return ""; - TRACE(control,out).print("preserve_rooms_getReservationName: zone_id=%d\n", zone->id); + TRACE(control,out).print("preserve_rooms_getReservationName: zone_id={}\n", zone->id); auto it = reserved_zones.find(zone->id); if (it != reserved_zones.end() && it->second.size() > 0) { if (auto hf = df::historical_figure::find(it->second.front())) { @@ -718,7 +732,7 @@ static bool preserve_rooms_clearReservation(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; - DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id=%d\n", zone->id); + DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id={}\n", zone->id); clear_reservation(out, zone->id, zone); return true; } @@ -768,6 +782,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(preserve_rooms_getFeature), DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignment), + DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignmentForZone), DFHACK_LUA_FUNCTION(preserve_rooms_isReserved), DFHACK_LUA_FUNCTION(preserve_rooms_getReservationName), DFHACK_LUA_FUNCTION(preserve_rooms_clearReservation), diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 17f06c6c92..a695b22e31 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -53,25 +53,25 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & params) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot use %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot use {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (params.size() == 0 || params[0] == "status") { - out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); + out.print("{} is currently {}\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (is_enabled) { out.print("tracked tomb assignments:\n"); std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ auto& [unit_id, building_id] = p; auto* unit = df::unit::find(unit_id); std::string name = unit ? DF2CONSOLE(Units::getReadableName(unit)) : "UNKNOWN UNIT" ; - out.print("%s (id %d) -> building %d\n", name.c_str(), unit_id, building_id); + out.print("{} (id {}) -> building {}\n", name, unit_id, building_id); }); } return CR_OK; } if (params[0] == "now") { if (!is_enabled) { - out.printerr("Cannot update %s when not enabled", plugin_name); + out.printerr("Cannot update {} when not enabled", plugin_name); return CR_FAILURE; } update_tomb_assignments(out); @@ -96,13 +96,13 @@ static void do_disable() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -110,7 +110,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else do_disable(); } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -118,7 +118,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); // PluginManager handles unregistering our handler from EventManager, // so we don't have to do that here @@ -136,7 +136,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); if (is_enabled) do_enable(out); @@ -146,7 +146,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED && is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; do_disable(); @@ -176,16 +176,16 @@ void onUnitDeath(color_ostream& out, void* ptr) { int32_t building_id = it->second; if (!assign_to_tomb(unit, building_id)) { if (unit) { - WARN(event, out).print("%s died - but failed to assign them back to their tomb %d\n", + WARN(event, out).print("{} died - but failed to assign them back to their tomb {}\n", DF2CONSOLE(Units::getReadableName(unit)).c_str(), building_id); } else { - WARN(event, out).print("Unit %d died - but failed to assign them back to their tomb %d\n", unit_id, building_id); + WARN(event, out).print("Unit {} died - but failed to assign them back to their tomb {}\n", unit_id, building_id); } return; } // success, print status update and remove assignment from our memo-list - INFO(event, out).print("%s died - assigning them back to their tomb\n", + INFO(event, out).print("{} died - assigning them back to their tomb\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); tomb_assignments.erase(it); } @@ -198,17 +198,18 @@ static void update_tomb_assignments(color_ostream &out) { // check tomb civzones for assigned units for (auto* tomb : world->buildings.other.ZONE_TOMB) { if (!tomb || !tomb->flags.bits.exists) continue; - if (!tomb->assigned_unit) continue; - if (Units::isDead(tomb->assigned_unit)) continue; // we only care about living units + if (tomb->assigned_unit_id == -1) continue; + auto unit = Buildings::getOwner(tomb); + if (!unit || Units::isDead(unit)) continue; // we only care about living units auto it = tomb_assignments.find(tomb->assigned_unit_id); if (it == tomb_assignments.end()) { tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); - DEBUG(cycle, out).print("%s new tomb assignment, unit %d to tomb %d\n", + DEBUG(cycle, out).print("{} new tomb assignment, unit {} to tomb {}\n", plugin_name, tomb->assigned_unit_id, tomb->id); } else if (it->second != tomb->id) { - DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", + DEBUG(cycle, out).print("{} tomb assignment to {} changed, (old: {}, new: {})\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); it->second = tomb->id; } @@ -220,17 +221,17 @@ static void update_tomb_assignments(color_ostream &out) { auto bld = df::building::find(building_id); if (!bld) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } auto tomb = virtual_cast(bld); if (!tomb || !tomb->flags.bits.exists) { - DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("{} tomb missing: {} - removing\n", plugin_name, building_id); return true; } if (tomb->assigned_unit_id != unit_id) { - DEBUG(cycle, out).print("%s unit %d unassigned from tomb %d - removing\n", plugin_name, unit_id, building_id); + DEBUG(cycle, out).print("{} unit {} unassigned from tomb {} - removing\n", plugin_name, unit_id, building_id); return true; } @@ -248,7 +249,7 @@ static bool assign_to_tomb(df::unit * unit, int32_t building_id) { if (!bld) return false; auto tomb = virtual_cast(bld); - if (!tomb || tomb->assigned_unit) return false; + if (!tomb || tomb->assigned_unit_id != -1) return false; Buildings::setOwner(tomb, unit); return true; diff --git a/plugins/probe.cpp b/plugins/probe.cpp index 31c2df5edc..bd71346fb7 100644 --- a/plugins/probe.cpp +++ b/plugins/probe.cpp @@ -1,11 +1,16 @@ +#include +#include +#include + #include "LuaTools.h" +#include "MiscUtils.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/Gui.h" -#include "modules/Materials.h" #include "modules/MapCache.h" #include "modules/Maps.h" +#include "modules/Materials.h" #include "df/block_square_event_grassst.h" #include "df/block_square_event_world_constructionst.h" @@ -15,6 +20,8 @@ #include "df/civzone_type.h" #include "df/construction_type.h" #include "df/furnace_type.h" +#include "df/global_objects.h" +#include "df/inorganic_raw.h" #include "df/item.h" #include "df/map_block.h" #include "df/region_map_entry.h" @@ -41,12 +48,12 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) if (!unit) return CR_FAILURE; - out.print("Creature %d, race %d (0x%x), civ %d (0x%x)\n", + out.print("Creature {}, race {} (0x{:x}), civ {} (0x{:x})\n", unit->id, unit->race, unit->race, unit->civ_id, unit->civ_id); for (auto inv_item : unit->inventory) { df::item* item = inv_item->item; - if (inv_item->mode == df::unit_inventory_item::T_mode::Worn) { + if (inv_item->mode == df::inv_item_role_type::Worn) { out << " wears item: #" << item->id; if (item->flags.bits.owned) out << " (owned)"; @@ -62,34 +69,32 @@ static command_result df_cprobe(color_ostream &out, vector & parameters) } static void describeTile(color_ostream &out, df::tiletype tiletype) { - out.print("%d", tiletype); + out.print("{}", static_cast(tiletype)); if (tileName(tiletype)) - out.print(" = %s", tileName(tiletype)); - out.print(" (%s)", ENUM_KEY_STR(tiletype, tiletype).c_str()); + out.print(" = {}", tileName(tiletype)); + out.print(" ({})", ENUM_KEY_STR(tiletype, tiletype).c_str()); out.print("\n"); df::tiletype_shape shape = tileShape(tiletype); df::tiletype_material material = tileMaterial(tiletype); df::tiletype_special special = tileSpecial(tiletype); df::tiletype_variant variant = tileVariant(tiletype); - out.print("%-10s: %4d %s\n","Class" ,shape, - ENUM_KEY_STR(tiletype_shape, shape).c_str()); - out.print("%-10s: %4d %s\n","Material" , - material, ENUM_KEY_STR(tiletype_material, material).c_str()); - out.print("%-10s: %4d %s\n","Special" , - special, ENUM_KEY_STR(tiletype_special, special).c_str()); - out.print("%-10s: %4d %s\n" ,"Variant" , - variant, ENUM_KEY_STR(tiletype_variant, variant).c_str()); - out.print("%-10s: %s\n" ,"Direction", + out.print("{:>10}: {:4} {}\n","Class" ,static_cast(shape), + ENUM_KEY_STR(tiletype_shape, shape)); + out.print("{:>10}: {:4} {}\n","Material" , static_cast(material), + ENUM_KEY_STR(tiletype_material, material)); + out.print("{:>10}: {:4} {}\n","Special" , static_cast(special), + ENUM_KEY_STR(tiletype_special, special)); + out.print("{:>10}: {:4} {}\n","Variant" , static_cast(variant), + ENUM_KEY_STR(tiletype_variant, variant)); + out.print("{:>10}: {}\n" ,"Direction", tileDirection(tiletype).getStr()); out.print("\n"); } static command_result df_probe(color_ostream &out, vector & parameters) { - DFHack::Materials *Materials = Core::getInstance().getMaterials(); - vector inorganic; - bool hasmats = Materials->CopyInorganicMaterials(inorganic); + auto& inorganic = world->raws.inorganics.all; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); @@ -122,14 +127,14 @@ static command_result df_probe(color_ostream &out, vector & parameters) uint32_t blockY = cursor.y / 16; uint32_t tileY = cursor.y % 16; - MapExtras::Block * b = mc.BlockAt(cursor/16); + MapExtras::Block * b = mc.BlockAtTile(cursor); if (!b || !b->is_valid()) { out.printerr("No data.\n"); return CR_OK; } auto &block = *b->getRaw(); - out.print("block addr: %p\n\n", &block); + out.print("block addr: {}\n\n", static_cast(&block)); df::tiletype tiletype = mc.tiletypeAt(cursor); df::tile_designation &des = block.designation[tileX][tileY]; @@ -143,8 +148,8 @@ static command_result df_probe(color_ostream &out, vector & parameters) out.print("base: "); describeTile(out, mc.baseTiletypeAt(cursor)); - out.print("temperature1: %d U\n", mc.temperature1At(cursor)); - out.print("temperature2: %d U\n", mc.temperature2At(cursor)); + out.print("temperature1: {} U\n", mc.temperature1At(cursor)); + out.print("temperature2: {} U\n", mc.temperature2At(cursor)); int offset = block.region_offset[des.bits.biome]; int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1); @@ -173,29 +178,25 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << "geolayer: " << des.bits.geolayer_index << std::endl; int16_t base_rock = mc.layerMaterialAt(cursor); - if (base_rock != -1) { + if (base_rock != -1) + { out << "Layer material: " << std::dec << base_rock; - if(hasmats) - out << " / " << inorganic[base_rock].id - << " / " - << inorganic[base_rock].name - << std::endl; - else - out << std::endl; + out << " / " << inorganic[base_rock]->id + << " / " + << inorganic[base_rock]->material.stone_name + << std::endl; } int16_t vein_rock = mc.veinMaterialAt(cursor); - if (vein_rock != -1) { + if (vein_rock != -1) + { out << "Vein material (final): " << std::dec << vein_rock; - if(hasmats) - out << " / " << inorganic[vein_rock].id - << " / " - << inorganic[vein_rock].name - << " (" - << ENUM_KEY_STR(inclusion_type,b->veinTypeAt(cursor)) - << ")" - << std::endl; - else - out << std::endl; + out << " / " << inorganic[vein_rock]->id + << " / " + << inorganic[vein_rock]->material.stone_name + << " (" + << ENUM_KEY_STR(inclusion_type, b->veinTypeAt(cursor)) + << ")" + << std::endl; } MaterialInfo minfo(mc.baseMaterialAt(cursor)); if (minfo.isValid()) @@ -223,12 +224,12 @@ static command_result df_probe(color_ostream &out, vector & parameters) if (des.bits.water_stagnant) out << "stagnant" << std::endl; - #define PRINT_FLAG( FIELD, BIT ) out.print("%-16s= %c\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) + #define PRINT_FLAG( FIELD, BIT ) out.print("{:<16} = {}\n", #BIT , ( FIELD.bits.BIT ? 'Y' : ' ' ) ) - out.print("%-16s= %s\n", "dig", enum_item_key(des.bits.dig).c_str()); + out.print("{:<16} = {}\n", "dig", enum_item_key(des.bits.dig)); PRINT_FLAG(occ, dig_marked); PRINT_FLAG(occ, dig_auto); - out.print("%-16s= %s\n", "traffic", enum_item_key(des.bits.traffic).c_str()); + out.print("{:<16} = {}\n", "traffic", enum_item_key(des.bits.traffic)); PRINT_FLAG(occ, carve_track_north); PRINT_FLAG(occ, carve_track_south); PRINT_FLAG(occ, carve_track_east); @@ -241,7 +242,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, water_table ); PRINT_FLAG( des, rained ); PRINT_FLAG( occ, monster_lair); - out.print("%-16s= %d\n", "fog_of_war", fog_of_war); + out.print("{:<16} = {}\n", "fog_of_war", fog_of_war); df::coord2d pc(blockX, blockY); @@ -251,19 +252,20 @@ static command_result df_probe(color_ostream &out, vector & parameters) PRINT_FLAG( des, feature_local ); if(local.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.local_feature); - out.print(" (%2d)", local.type); - out.print(" addr %p ", local.origin); - out.print(" %s\n", sa_feature(local.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.local_feature); + out.print(" ({:2})", static_cast(local.type)); + out.print(" addr {}", static_cast(local.origin)); + out.print(" {}\n", sa_feature(local.type)); } PRINT_FLAG( des, feature_global ); if(global.type != -1) { - out.print("%-16s", ""); - out.print(" %4d", block.global_feature); - out.print(" (%2d)", global.type); - out.print(" %s\n", sa_feature(global.type)); + out.print("{:<16}", ""); + out.print(" {:4}", block.global_feature); + out.print(" ({:2})", static_cast(global.type)); + out.print(" {}", static_cast(global.origin)); + out.print(" {}\n", sa_feature(global.type)); } out << "local feature idx: " << block.local_feature << std::endl; @@ -272,7 +274,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) out << std::endl; out << "Occupancy:" << std::endl; - out.print("%-16s= %s\n", "building", enum_item_key(occ.bits.building).c_str()); + out.print("{:<16} = {}\n", "building", enum_item_key(occ.bits.building)); PRINT_FLAG(occ, unit); PRINT_FLAG(occ, unit_grounded); PRINT_FLAG(occ, item); @@ -308,17 +310,6 @@ static command_result df_probe(color_ostream &out, vector & parameters) return CR_OK; } -union Subtype { - int16_t subtype; - df::civzone_type civzone_type; - df::furnace_type furnace_type; - df::workshop_type workshop_type; - df::construction_type construction_type; - df::shop_type shop_type; - df::siegeengine_type siegeengine_type; - df::trap_type trap_type; -}; - static command_result df_bprobe(color_ostream &out, vector & parameters) { auto bld = Gui::getSelectedBuilding(out); if (!bld) @@ -328,70 +319,70 @@ static command_result df_bprobe(color_ostream &out, vector & parameters) bld->getName(&name); auto bld_type = bld->getType(); - Subtype subtype{bld->getSubtype()}; + int16_t subtype{bld->getSubtype()}; int32_t custom = bld->getCustomType(); - out.print("Building %i, \"%s\", type %s (%i)", + out.print("Building {:<4}, \"{}\", type {} ({})", bld->id, - name.c_str(), - ENUM_KEY_STR(building_type, bld_type).c_str(), - bld_type); + name, + ENUM_KEY_STR(building_type, bld_type), + static_cast(bld_type)); switch (bld_type) { case df::building_type::Civzone: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(civzone_type, subtype.civzone_type).c_str(), - subtype.subtype); + out.print(", subtype {} ({})", + ENUM_KEY_STR(civzone_type, static_cast(subtype)), + subtype); break; case df::building_type::Furnace: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(furnace_type, subtype.furnace_type).c_str(), - subtype.subtype); - if (subtype.furnace_type == df::furnace_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(furnace_type, static_cast(subtype)), + subtype); + if (static_cast(subtype) == df::furnace_type::Custom) + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Workshop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(workshop_type, subtype.workshop_type).c_str(), - subtype.subtype); - if (subtype.workshop_type == df::workshop_type::Custom) - out.print(", custom type %s (%i)", - world->raws.buildings.all[custom]->code.c_str(), + out.print(", subtype {} ({})", + ENUM_KEY_STR(workshop_type, static_cast(subtype)), + subtype); + if (subtype == static_cast(df::workshop_type::Custom)) + out.print(", custom type {} ({})", + world->raws.buildings.all[custom]->code, custom); break; case df::building_type::Construction: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(construction_type, subtype.construction_type).c_str(), - subtype.subtype); + out.print(", subtype {} ({})", + ENUM_KEY_STR(construction_type, static_cast(subtype)), + subtype); break; case df::building_type::Shop: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(shop_type, subtype.shop_type).c_str(), - subtype.subtype); + out.print(", subtype {} ({})", + ENUM_KEY_STR(shop_type, static_cast(subtype)), + subtype); break; case df::building_type::SiegeEngine: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(siegeengine_type, subtype.siegeengine_type).c_str(), - subtype.subtype); + out.print(", subtype {} ({})", + ENUM_KEY_STR(siegeengine_type, static_cast(subtype)), + subtype); break; case df::building_type::Trap: - out.print(", subtype %s (%i)", - ENUM_KEY_STR(trap_type, subtype.trap_type).c_str(), - subtype.subtype); + out.print(", subtype {} ({})", + ENUM_KEY_STR(trap_type, static_cast(subtype)), + subtype); break; case df::building_type::NestBox: { df::building_nest_boxst* nestbox = virtual_cast(bld); if (nestbox) - out.print(", claimed:(%i), items:%zu", nestbox->claimed_by, nestbox->contained_items.size()); + out.print(", claimed:({}), items:({})", nestbox->claimed_by, nestbox->contained_items.size()); break; } default: - if (subtype.subtype != -1) - out.print(", subtype %i", subtype.subtype); + if (subtype != -1) + out.print(", subtype {}", subtype); break; } diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 159b557e4f..427d8d34e9 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -223,12 +223,14 @@ void printVeins(color_ostream &con, MatMap &mat_map, MatMap gems; MatMap rest; + auto & inorganics = world->raws.inorganics.all; + for (const auto &kv : mat_map) { - df::inorganic_raw *gloss = vector_get(world->raws.inorganics, kv.first); + df::inorganic_raw *gloss = vector_get(inorganics, kv.first); if (!gloss) { - con.printerr("invalid material gloss: %hi\n", kv.first); + con.printerr("invalid material gloss: {}\n", kv.first); continue; } @@ -242,17 +244,17 @@ void printVeins(color_ostream &con, MatMap &mat_map, if (options.ores) { con << "Ores:" << std::endl; - printMats(con, ores, world->raws.inorganics, options); + printMats(con, ores, inorganics, options); } if (options.gems) { con << "Gems:" << std::endl; - printMats(con, gems, world->raws.inorganics, options); + printMats(con, gems, inorganics, options); } if (options.veins) { con << "Other vein stone:" << std::endl; - printMats(con, rest, world->raws.inorganics, options); + printMats(con, rest, inorganics, options); } } @@ -296,7 +298,7 @@ static df::world_region_details *get_details(df::world_data *data, df::coord2d p bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_region_details *details, int x, int y) { if (x < 0 || y < 0 || x > 15 || y > 15) { - out.printerr("Invalid embark coordinates: x=%i, y=%i\n", x, y); + out.printerr("Invalid embark coordinates: x={}, y={}\n", x, y); return false; } // Find actual biome @@ -383,7 +385,8 @@ bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_ if (!sea_found) { - out.printerr("Could not find magma sea; depth may be incorrect.\n"); + if (world->worldgen.worldgen_parms.have_bottom_layer_1) + out.printerr("Could not find magma sea; depth may be incorrect.\n"); tile.min_z = tile.base_z; } @@ -426,7 +429,7 @@ bool estimate_materials(color_ostream &out, EmbarkTileLayout &tile, MatMap &laye if (!geo_biome) { - out.printerr("Region geo-biome not found: (%d,%d)\n", + out.printerr("Region geo-biome not found: ({}, {})\n", tile.biome_pos.x, tile.biome_pos.y); return false; } @@ -604,13 +607,11 @@ static command_result embark_prospector(color_ostream &out, // Print the report if (options.layers) { out << "Layer materials:" << std::endl; - printMats(out, layerMats, world->raws.inorganics, options); + printMats(out, layerMats, world->raws.inorganics.all, options); } if (options.hidden) { - DFHack::Materials *mats = Core::getInstance().getMaterials(); printVeins(out, veinMats, options); - mats->Finish(); } out << "Embark depth: " << (world_bottom.upper_z-world_bottom.lower_z+1) << " "; @@ -633,8 +634,6 @@ static command_result map_prospector(color_ostream &con, Maps::getSize(x_max, y_max, z_max); MapExtras::MapCache map; - DFHack::Materials *mats = Core::getInstance().getMaterials(); - DFHack::t_feature blockFeatureGlobal; DFHack::t_feature blockFeatureLocal; @@ -837,7 +836,7 @@ static command_result map_prospector(color_ostream &con, if (options.layers) { con << "Layer materials:" << std::endl; - printMats(con, layerMats, world->raws.inorganics, options); + printMats(con, layerMats, world->raws.inorganics.all, options); } if (options.features) { @@ -892,9 +891,6 @@ static command_result map_prospector(color_ostream &con, printMats(con, treeMats, world->raws.plants.all, options); } - // Cleanup - mats->Finish(); - return CR_OK; } diff --git a/plugins/regrass.cpp b/plugins/regrass.cpp index d40e1bc6dc..a17f5fb7e2 100644 --- a/plugins/regrass.cpp +++ b/plugins/regrass.cpp @@ -111,7 +111,7 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc else if (block->occupancy[tx][ty].bits.building > (options.buildings ? tile_building_occ::Passable : tile_building_occ::None)) { // Avoid stockpiles and planned/passable buildings unless enabled. - TRACE(log, out).print("Invalid tile: Building (%s)\n", + TRACE(log, out).print("Invalid tile: Building ({})\n", ENUM_KEY_STR(tile_building_occ, block->occupancy[tx][ty].bits.building).c_str()); return false; } @@ -180,7 +180,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b for (auto p_raw : world->raws.plants.grasses) { // Sorted by df::plant_raw::index if (p_raw->flags.is_set(plant_raw_flags::BIOME_SUBTERRANEAN_WATER)) { - TRACE(log, out).print("Cave grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Cave grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -209,7 +209,7 @@ static void grasses_for_tile(color_ostream &out, vector &vec, df::map_b (evil || !flags.is_set(plant_raw_flags::EVIL)) && (savage || !flags.is_set(plant_raw_flags::SAVAGE))) { - TRACE(log, out).print("Surface grass: %s\n", p_raw->id.c_str()); + TRACE(log, out).print("Surface grass: {}\n", p_raw->id); vec.push_back(p_raw->index); } } @@ -241,7 +241,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d } else if (gr_ev->amount[tx][ty] > 0) { // Refill first non-zero grass. - DEBUG(log, out).print("Refilling existing grass (ID %d).\n", gr_ev->plant_index); + DEBUG(log, out).print("Refilling existing grass (ID {}).\n", gr_ev->plant_index); gr_ev->amount[tx][ty] = 100; return true; } @@ -263,12 +263,12 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (auto gr_ev : consider_grev) { // These grass events are already present. if (erase_from_vector(new_grasses, gr_ev->plant_index)) { - TRACE(log, out).print("Grass (ID %d) already present.\n", gr_ev->plant_index); + TRACE(log, out).print("Grass (ID {}) already present.\n", gr_ev->plant_index); } } for (auto id : new_grasses) { - DEBUG(log, out).print("Allocating new grass event (ID %d).\n", id); + DEBUG(log, out).print("Allocating new grass event (ID {}).\n", id); auto gr_ev = df::allocate(); if (!gr_ev) { out.printerr("Failed to allocate new grass event! Aborting loop.\n"); @@ -297,7 +297,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d for (size_t i = consider_grev.size(); i-- > 0;) { // Iterate backwards and remove invalid events from consideration. if (!vector_contains(valid_grasses, consider_grev[i]->plant_index)) { - TRACE(log, out).print("Removing grass (ID %d) from consideration.\n", + TRACE(log, out).print("Removing grass (ID {}) from consideration.\n", consider_grev[i]->plant_index); consider_grev.erase(consider_grev.begin() + i); } @@ -327,14 +327,14 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d if (options.max_grass) { // Don't need random choice. - DEBUG(log, out).print("Done with max regrass, %s.\n", + DEBUG(log, out).print("Done with max regrass, {}.\n", success ? "success" : "failure"); return success; } TRACE(log, out).print("Selecting random valid grass event...\n"); if (auto rand_grev = vector_get_random(consider_grev)) { - DEBUG(log, out).print("Refilling random grass (ID %d).\n", rand_grev->plant_index); + DEBUG(log, out).print("Refilling random grass (ID {}).\n", rand_grev->plant_index); rand_grev->amount[tx][ty] = 100; return true; } @@ -346,11 +346,11 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Regrass single tile. Return 1 if tile success, else 0. CHECK_NULL_POINTER(block); if (!is_valid_tile_coord(df::coord2d(tx, ty))) { - out.printerr("(%d, %d) not in range 0-15!\n", tx, ty); + out.printerr("({}, {}) not in range 0-15!\n", tx, ty); return 0; } - DEBUG(log, out).print("Regrass tile (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass tile ({}, {}, {})\n", block->map_pos.x + tx, block->map_pos.y + ty, block->map_pos.z); if (!regrass_events(out, options, block, tx, ty)) return 0; @@ -377,7 +377,7 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo { // Ramp or stairs. auto new_mat = (rand() & 1) ? tiletype_material::GRASS_LIGHT : tiletype_material::GRASS_DARK; auto new_tt = findTileType(shape, new_mat, tiletype_variant::NONE, tiletype_special::NONE, nullptr); - DEBUG(log, out).print("Tiletype to %s.\n", ENUM_KEY_STR(tiletype, new_tt).c_str()); + DEBUG(log, out).print("Tiletype to {}.\n", ENUM_KEY_STR(tiletype, new_tt)); block->tiletype[tx][ty] = new_tt; } return 1; @@ -385,12 +385,12 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo int regrass_cuboid(color_ostream &out, const regrass_options &options, const cuboid &bounds) { // Regrass all tiles in the defined cuboid. - DEBUG(log, out).print("Regrassing cuboid (%d:%d, %d:%d, %d:%d), clipped to map.\n", + DEBUG(log, out).print("Regrassing cuboid ({}:{}, {}:{}, {}:{})\n", bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); int count = 0; bounds.forBlock([&](df::map_block *block, cuboid intersect) { - DEBUG(log, out).print("Cuboid regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Cuboid regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); intersect.forCoord([&](df::coord pos) { count += regrass_tile(out, options, block, pos.x&15, pos.y&15); @@ -404,7 +404,7 @@ int regrass_cuboid(color_ostream &out, const regrass_options &options, const cub int regrass_block(color_ostream &out, const regrass_options &options, df::map_block *block) { // Regrass all tiles in a single block. CHECK_NULL_POINTER(block); - DEBUG(log, out).print("Regrass block at (%d, %d, %d)\n", + DEBUG(log, out).print("Regrass block at ({}, {}, {})\n", block->map_pos.x, block->map_pos.y, block->map_pos.z); int count = 0; @@ -443,11 +443,11 @@ command_result df_regrass(color_ostream &out, vector ¶meters) else if (options.forced_plant == -2) { // Print all grass raw IDs. for (auto p_raw : world->raws.plants.grasses) - out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + out.print("{}: {}\n", p_raw->index, p_raw->id); return CR_OK; } - DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + DEBUG(log, out).print("pos_1 = ({}, {}, {})\npos_2 = ({}, {}, {})\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); if (options.block && options.zlevel) { @@ -464,17 +464,17 @@ command_result df_regrass(color_ostream &out, vector ¶meters) } if (options.force) { - DEBUG(log, out).print("forced_plant = %d\n", options.forced_plant); + DEBUG(log, out).print("forced_plant = {}\n", options.forced_plant); auto p_raw = vector_get(world->raws.plants.all, options.forced_plant); if (p_raw) { if (!p_raw->flags.is_set(plant_raw_flags::GRASS)) { - out.printerr("Plant raw wasn't grass: %d (%s)\n", options.forced_plant, p_raw->id.c_str()); + out.printerr("Plant raw wasn't grass: {} ({})\n", options.forced_plant, p_raw->id); return CR_FAILURE; } - out.print("Forced grass: %s\n", p_raw->id.c_str()); + out.print("Forced grass: {}\n", p_raw->id); } else { - out.printerr("Plant raw not found for --force: %d\n", options.forced_plant); + out.printerr("Plant raw not found for --force: {}\n", options.forced_plant); return CR_FAILURE; } } @@ -484,7 +484,7 @@ command_result df_regrass(color_ostream &out, vector ¶meters) { // Specified z-levels or viewport z. auto z1 = pos_1.isValid() ? pos_1.z : Gui::getViewportPos().z; auto z2 = pos_2.isValid() ? pos_2.z : z1; - DEBUG(log, out).print("Regrassing z-levels %d to %d...\n", z1, z2); + DEBUG(log, out).print("Regrassing z-levels {} to {}...\n", z1, z2); count = regrass_cuboid(out, options, cuboid(0, 0, z1, world->map.x_count-1, world->map.y_count-1, z2)); } @@ -519,6 +519,6 @@ command_result df_regrass(color_ostream &out, vector ¶meters) DEBUG(log, out).print("Regrassing map...\n"); count = regrass_map(out, options); } - out.print("Regrew %d tiles of grass.\n", count); + out.print("Regrew {} tiles of grass.\n", count); return CR_OK; } diff --git a/plugins/remotefortressreader/building_reader.cpp b/plugins/remotefortressreader/building_reader.cpp index 7cc715147b..62475ce1aa 100644 --- a/plugins/remotefortressreader/building_reader.cpp +++ b/plugins/remotefortressreader/building_reader.cpp @@ -529,7 +529,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re default: break; } - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.raised) remote_build->set_active(1); else remote_build->set_active(0); @@ -560,18 +560,31 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto facing = actual->facing; switch (facing) { - case df::building_siegeenginest::Left: - remote_build->set_direction(WEST); - break; - case df::building_siegeenginest::Up: + using enum df::enums::siegeengine_orientation::siegeengine_orientation; + case North: remote_build->set_direction(NORTH); break; - case df::building_siegeenginest::Right: + case Northeast: + remote_build->set_direction(NORTHEAST); + break; + case East: remote_build->set_direction(EAST); break; - case df::building_siegeenginest::Down: + case Southeast: + remote_build->set_direction(SOUTHEAST); + break; + case South: remote_build->set_direction(SOUTH); break; + case Southwest: + remote_build->set_direction(SOUTHWEST); + break; + case West: + remote_build->set_direction(WEST); + break; + case Northwest: + remote_build->set_direction(NORTHWEST); + break; default: break; } @@ -671,7 +684,7 @@ void CopyBuilding(int buildingIndex, RemoteFortressReader::BuildingInstance * re auto actual = strict_virtual_cast(local_build); if (actual) { - if (actual->gate_flags.bits.closed) + if (actual->gate_flags.bits.retracted) remote_build->set_active(1); else remote_build->set_active(0); diff --git a/plugins/remotefortressreader/item_reader.cpp b/plugins/remotefortressreader/item_reader.cpp index 1353243b2b..58c84b9075 100644 --- a/plugins/remotefortressreader/item_reader.cpp +++ b/plugins/remotefortressreader/item_reader.cpp @@ -14,7 +14,6 @@ #include "df/art_image_property.h" #include "df/art_image_property_intransitive_verbst.h" #include "df/art_image_property_transitive_verbst.h" -#include "df/art_image_ref.h" #include "df/descriptor_shape.h" #include "df/instrument_piece.h" #include "df/instrument_register.h" @@ -240,19 +239,19 @@ void CopyItem(RemoteFortressReader::Item * NetItem, df::item * DfItem) GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), statue->image.id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), statue->image.id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == statue->image.id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == statue->image.id) + chunk = world->art_image_chunks.all[i]; } } - if (chunk && chunk->images[statue->image.subid]) + if (chunk && chunk->images[statue->image.subid].art_image) { - CopyImage(chunk->images[statue->image.subid], NetItem->mutable_image()); + CopyImage(chunk->images[statue->image.subid].art_image, NetItem->mutable_image()); } @@ -663,11 +662,11 @@ DFHack::command_result GetItemList(DFHack::color_ostream &stream, const DFHack:: { send_instrument->add_tuning_parm(*(instrument->tuning_parm[j])); } - for (size_t j = 0; j < instrument->registers.size(); j++) + for (size_t j = 0; j < instrument->timbre.registers.size(); j++) { auto reg = send_instrument->add_registers(); - reg->set_pitch_range_min(instrument->registers[j]->pitch_range_min); - reg->set_pitch_range_max(instrument->registers[j]->pitch_range_max); + reg->set_pitch_range_min(instrument->timbre.registers[j]->pitch_range_min); + reg->set_pitch_range_max(instrument->timbre.registers[j]->pitch_range_max); } send_instrument->set_description(DF2UTF(instrument->description)); } diff --git a/plugins/remotefortressreader/proto/RemoteFortressReader.proto b/plugins/remotefortressreader/proto/RemoteFortressReader.proto index a092a12097..b7dc7bdc88 100644 --- a/plugins/remotefortressreader/proto/RemoteFortressReader.proto +++ b/plugins/remotefortressreader/proto/RemoteFortressReader.proto @@ -141,7 +141,11 @@ enum BuildingDirection EAST = 1; SOUTH = 2; WEST = 3; - NONE = 4; + NORTHEAST = 4; + SOUTHEAST = 5; + SOUTHWEST = 6; + NORTHWEST = 7; + NONE = 8; } enum TileDigDesignation diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index ae6683dee9..3145ca1add 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -42,6 +42,7 @@ #include "df/body_part_layer_raw.h" #include "df/body_part_raw.h" #include "df/bp_appearance_modifier.h" +#include "df/buildingitemst.h" #include "df/builtin_mats.h" #include "df/building_wellst.h" @@ -64,6 +65,7 @@ #include "df/inorganic_raw.h" #include "df/item.h" #include "df/job.h" +#include "df/job_postingst.h" #include "df/job_type.h" #include "df/job_item.h" #include "df/job_material_category.h" @@ -97,6 +99,7 @@ #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_loadgamest.h" #include "df/viewscreen_savegamest.h" @@ -194,7 +197,6 @@ const char* growth_locations[] = { #include "df/art_image.h" #include "df/art_image_chunk.h" -#include "df/art_image_ref.h" command_result loadArtImageChunk(color_ostream &out, std::vector & parameters) { if (parameters.size() != 1) @@ -210,8 +212,8 @@ command_result loadArtImageChunk(color_ostream &out, std::vector & if (GetArtImageChunk) { int index = atoi(parameters[0].c_str()); - auto chunk = GetArtImageChunk(&(world->art_image_chunks), index); - out.print("Loaded chunk id: %d\n", chunk->id); + auto chunk = GetArtImageChunk(&(world->art_image_chunks.all), index); + out.print("Loaded chunk id: {}\n", chunk->id); } return CR_OK; } @@ -302,15 +304,16 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) return CR_OK; } -uint16_t fletcher16(uint8_t const *data, size_t bytes) +uint16_t fletcher16(const void *data_, size_t bytes) { + auto data = static_cast(data_); uint16_t sum1 = 0xff, sum2 = 0xff; while (bytes) { size_t tlen = bytes > 20 ? 20 : bytes; bytes -= tlen; do { - sum2 += sum1 += *data++; + sum2 += sum1 += static_cast(*data++); } while (--tlen); sum1 = (sum1 & 0xff) + (sum1 >> 8); sum2 = (sum2 & 0xff) + (sum2 >> 8); @@ -333,7 +336,7 @@ void ConvertDfColor(int16_t index, RemoteFortressReader::ColorDefinition * out) out->set_blue(gps->uccolor[index][2]); } -void ConvertDfColor(int16_t in[3], RemoteFortressReader::ColorDefinition * out) +void ConvertDfColor(std::array& in, RemoteFortressReader::ColorDefinition * out) { int index = in[0] | (8 * in[2]); ConvertDfColor(index, out); @@ -621,22 +624,22 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) for (size_t i = 0; i < world->map.map_blocks.size(); i++) { df::map_block * block = world->map.map_blocks[i]; - fletcher16((uint8_t*)(block->tiletype), 16 * 16 * sizeof(df::enums::tiletype::tiletype)); + fletcher16((block->tiletype).data(), 16 * 16 * sizeof(df::enums::tiletype::tiletype)); } clock_t end = clock(); double elapsed_secs = double(end - start) / CLOCKS_PER_SEC; - stream.print("Checking all hashes took %f seconds.", elapsed_secs); + stream.print("Checking all hashes took {} seconds.", elapsed_secs); return CR_OK; } void CopyMat(RemoteFortressReader::MatPair * mat, int type, int index) { - if (type >= MaterialInfo::FIGURE_BASE && type < MaterialInfo::PLANT_BASE) + if (type >= df::builtin_mats::HIST_FIG_1 && type <= df::builtin_mats::HIST_FIG_200) { df::historical_figure * figure = df::historical_figure::find(index); if (figure) { - type -= MaterialInfo::GROUP_SIZE; + type = (type - df::builtin_mats::HIST_FIG_1) + df::builtin_mats::CREATURE_1; index = figure->race; } } @@ -652,7 +655,7 @@ bool IsTiletypeChanged(DFCoord pos) uint16_t hash; df::map_block * block = Maps::getBlock(pos); if (block) - hash = fletcher16((uint8_t*)(block->tiletype), 16 * 16 * (sizeof(df::enums::tiletype::tiletype))); + hash = fletcher16((block->tiletype).data(), 16 * 16 * (sizeof(df::enums::tiletype::tiletype))); else hash = 0; if (hashes[pos] != hash) @@ -670,7 +673,7 @@ bool IsDesignationChanged(DFCoord pos) uint16_t hash; df::map_block * block = Maps::getBlock(pos); if (block) - hash = fletcher16((uint8_t*)(block->designation), 16 * 16 * (sizeof(df::tile_designation))); + hash = fletcher16((block->designation).data(), 16 * 16 * (sizeof(df::tile_designation))); else hash = 0; if (waterHashes[pos] != hash) @@ -809,27 +812,27 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage return CR_OK; } - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; // df::world_history *history = &world->history; MaterialInfo mat; - for (size_t i = 0; i < raws->inorganics.size(); i++) + for (size_t i = 0; i < raws->inorganics.all.size(); i++) { - mat.decode(0, i); + mat.decode(df::builtin_mats::INORGANIC, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(0); + mat_def->mutable_mat_pair()->set_mat_type(df::builtin_mats::INORGANIC); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; - if (size_t(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)]) < raws->descriptors.colors.size()) + if (size_t(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)]) < raws->descriptors.colors.size()) { - ConvertDFColorDescriptor(raws->inorganics[i]->material.state_color[GetState(&raws->inorganics[i]->material)], mat_def->mutable_state_color()); + ConvertDFColorDescriptor(raws->inorganics.all[i]->material.state_color[GetState(&raws->inorganics.all[i]->material)], mat_def->mutable_state_color()); } } - for (int i = 0; i < 19; i++) + for (int i = 0; i < df::builtin_mats::CREATURE_1; i++) { int k = -1; - if (i == 7) - k = 1;// for coal. + if (i == df::builtin_mats::COAL) + k = 1;// for coke and charcoal for (int j = -1; j <= k; j++) { mat.decode(i, j); @@ -849,9 +852,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::creature_raw * creature = raws->creatures.all[i]; for (size_t j = 0; j < creature->material.size(); j++) { - mat.decode(j + MaterialInfo::CREATURE_BASE, i); + mat.decode(j + df::builtin_mats::CREATURE_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 19); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::CREATURE_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -866,9 +869,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage df::plant_raw * plant = raws->plants.all[i]; for (size_t j = 0; j < plant->material.size(); j++) { - mat.decode(j + 419, i); + mat.decode(j + df::builtin_mats::PLANT_1, i); MaterialDefinition *mat_def = out->add_material_list(); - mat_def->mutable_mat_pair()->set_mat_type(j + 419); + mat_def->mutable_mat_pair()->set_mat_type(j + df::builtin_mats::PLANT_1); mat_def->mutable_mat_pair()->set_mat_index(i); mat_def->set_id(mat.getToken()); mat_def->set_name(DF2UTF(mat.toString())); //find the name at cave temperature; @@ -890,7 +893,7 @@ static command_result GetGrowthList(color_ostream &stream, const EmptyMessage *i - df::world_raws *raws = &world->raws; + auto *raws = &world->raws; if (!raws) return CR_OK;//'. @@ -1154,7 +1157,7 @@ void CopyDesignation(df::map_block * DfBlock, RemoteFortressReader::MapBlock * N void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (projectile == NULL) @@ -1187,7 +1190,7 @@ void CopyProjectiles(RemoteFortressReader::MapBlock * NetBlock) { bool isProj = false; auto vehicle = world->vehicles.active[i]; - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(projectile, df::proj_itemst, proj->item); if (!projectile) @@ -1515,14 +1518,14 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in GET_ART_IMAGE_CHUNK GetArtImageChunk = reinterpret_cast(Core::getInstance().vinfo->getAddress("get_art_image_chunk")); if (GetArtImageChunk) { - chunk = GetArtImageChunk(&(world->art_image_chunks), engraving->art_id); + chunk = GetArtImageChunk(&(world->art_image_chunks.all), engraving->art_id); } else { - for (size_t i = 0; i < world->art_image_chunks.size(); i++) + for (size_t i = 0; i < world->art_image_chunks.all.size(); i++) { - if (world->art_image_chunks[i]->id == engraving->art_id) - chunk = world->art_image_chunks[i]; + if (world->art_image_chunks.all[i]->id == engraving->art_id) + chunk = world->art_image_chunks.all[i]; } } if (!chunk) @@ -1534,8 +1537,8 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in ConvertDFCoord(engraving->pos, netEngraving->mutable_pos()); netEngraving->set_quality(engraving->quality); netEngraving->set_tile(engraving->tile); - if (chunk->images[engraving->art_subid]) { - CopyImage(chunk->images[engraving->art_subid], netEngraving->mutable_image()); + if (chunk->images[engraving->art_subid].art_image) { + CopyImage(chunk->images[engraving->art_subid].art_image, netEngraving->mutable_image()); } netEngraving->set_floor(engraving->flags.bits.floor); netEngraving->set_west(engraving->flags.bits.west); @@ -1677,8 +1680,8 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques continue; } - using df::global::cur_year; - using df::global::cur_year_tick; + //using df::global::cur_year; + //using df::global::cur_year_tick; send_unit->set_age(Units::getAge(unit, false)); @@ -1772,7 +1775,7 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques if (unit->flags1.bits.projectile) { - for (auto proj = world->proj_list.next; proj != NULL; proj = proj->next) + for (auto proj = world->projectiles.all.next; proj != NULL; proj = proj->next) { STRICT_VIRTUAL_CAST_VAR(item, df::proj_unitst, proj->item); if (item == NULL) @@ -1985,27 +1988,23 @@ static command_result GetWorldMap(color_ostream &stream, const EmptyMessage *in, out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); auto poles = data->flip_latitude; -#if DF_VERSION_INT > 34011 switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2097,14 +2096,14 @@ static void SetRegionTile(RegionTile * out, df::region_map_entry * e1) auto plantMat = out->add_plant_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } else if (pop->type == world_population_type::Tree) { auto plantMat = out->add_tree_materials(); plantMat->set_mat_index(pop->plant); - plantMat->set_mat_type(419); + plantMat->set_mat_type(df::builtin_mats::PLANT_1); } } #if DF_VERSION_INT >= 43005 @@ -2133,28 +2132,24 @@ static command_result GetWorldMapNew(color_ostream &stream, const EmptyMessage * out->set_world_height(height); out->set_name(DF2UTF(Translation::translateName(&(data->name), false))); out->set_name_english(DF2UTF(Translation::translateName(&(data->name), true))); -#if DF_VERSION_INT > 34011 auto poles = data->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif for (int yy = 0; yy < height; yy++) for (int xx = 0; xx < width; xx++) { @@ -2268,32 +2263,27 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w out->set_map_y(pos_y); out->set_world_width(17); out->set_world_height(17); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); -#if DF_VERSION_INT > 34011 auto poles = worldData->flip_latitude; switch (poles) { - case df::world_data::None: + case df::pole_type::None: out->set_world_poles(WorldPoles::NO_POLES); break; - case df::world_data::North: + case df::pole_type::North: out->set_world_poles(WorldPoles::NORTH_POLE); break; - case df::world_data::South: + case df::pole_type::South: out->set_world_poles(WorldPoles::SOUTH_POLE); break; - case df::world_data::Both: + case df::pole_type::Both: out->set_world_poles(WorldPoles::BOTH_POLES); break; default: break; } -#else - out->set_world_poles(WorldPoles::NO_POLES); -#endif df::world_region_details * south = NULL; df::world_region_details * east = NULL; @@ -2376,8 +2366,7 @@ static void CopyLocalMap(df::world_data * worldData, df::world_region_details* w int pos_y = worldRegionDetails->pos.y; out->set_map_x(pos_x); out->set_map_y(pos_y); - char name[256]; - sprintf(name, "Region %d, %d", pos_x, pos_y); + std::string name = fmt::format("Region {}, {}", pos_x, pos_y); out->set_name_english(name); out->set_name(name); @@ -2637,15 +2626,15 @@ static command_result GetPartialCreatureRaws(color_ostream &stream, const ListRe send_creature->set_index(i); send_creature->set_creature_id(orig_creature->creature_id); - send_creature->add_name(orig_creature->name[0]); - send_creature->add_name(orig_creature->name[1]); - send_creature->add_name(orig_creature->name[2]); + send_creature->add_name(DF2UTF(orig_creature->name[0])); + send_creature->add_name(DF2UTF(orig_creature->name[1])); + send_creature->add_name(DF2UTF(orig_creature->name[2])); - send_creature->add_general_baby_name(orig_creature->general_baby_name[0]); - send_creature->add_general_baby_name(orig_creature->general_baby_name[1]); + send_creature->add_general_baby_name(DF2UTF(orig_creature->general_baby_name[0])); + send_creature->add_general_baby_name(DF2UTF(orig_creature->general_baby_name[1])); - send_creature->add_general_child_name(orig_creature->general_child_name[0]); - send_creature->add_general_child_name(orig_creature->general_child_name[1]); + send_creature->add_general_child_name(DF2UTF(orig_creature->general_child_name[0])); + send_creature->add_general_child_name(DF2UTF(orig_creature->general_child_name[1])); send_creature->set_creature_tile(orig_creature->creature_tile); send_creature->set_creature_soldier_tile(orig_creature->creature_soldier_tile); @@ -2665,15 +2654,15 @@ static command_result GetPartialCreatureRaws(color_ostream &stream, const ListRe send_caste->set_caste_id(orig_caste->caste_id); - send_caste->add_caste_name(orig_caste->caste_name[0]); - send_caste->add_caste_name(orig_caste->caste_name[1]); - send_caste->add_caste_name(orig_caste->caste_name[2]); + send_caste->add_caste_name(DF2UTF(orig_caste->caste_name[0])); + send_caste->add_caste_name(DF2UTF(orig_caste->caste_name[1])); + send_caste->add_caste_name(DF2UTF(orig_caste->caste_name[2])); - send_caste->add_baby_name(orig_caste->baby_name[0]); - send_caste->add_baby_name(orig_caste->baby_name[1]); + send_caste->add_baby_name(DF2UTF(orig_caste->baby_name[0])); + send_caste->add_baby_name(DF2UTF(orig_caste->baby_name[1])); - send_caste->add_child_name(orig_caste->child_name[0]); - send_caste->add_child_name(orig_caste->child_name[1]); + send_caste->add_child_name(DF2UTF(orig_caste->child_name[0])); + send_caste->add_child_name(DF2UTF(orig_caste->child_name[1])); send_caste->set_gender(orig_caste->sex); for (size_t partIndex = 0; partIndex < orig_caste->body_info.body_parts.size(); partIndex++) @@ -2792,7 +2781,7 @@ static command_result GetPartialCreatureRaws(color_ostream &stream, const ListRe } } - send_caste->set_description(orig_caste->description); + send_caste->set_description(DF2UTF(orig_caste->description)); send_caste->set_adult_size(orig_caste->misc.adult_size); } diff --git a/plugins/rendermax/renderer_light.cpp b/plugins/rendermax/renderer_light.cpp index 469c849091..b905cd93c5 100644 --- a/plugins/rendermax/renderer_light.cpp +++ b/plugins/rendermax/renderer_light.cpp @@ -1185,12 +1185,12 @@ void lightingEngineViewscreen::loadSettings() int ret=luaL_loadfile(s,settingsfile.c_str()); if(ret==LUA_ERRFILE) { - out.printerr("File not found:%s\n",settingsfile.c_str()); + out.printerr("File not found:{}\n",settingsfile); lua_pop(s,1); } else if(ret==LUA_ERRSYNTAX) { - out.printerr("Syntax error:\n\t%s\n",lua_tostring(s,-1)); + out.printerr("Syntax error:\n\t{}\n",lua_tostring(s,-1)); } else { @@ -1202,38 +1202,38 @@ void lightingEngineViewscreen::loadSettings() lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu materials loaded\n",matDefs.size()); + out.print("{} materials loaded\n",matDefs.size()); lua_pushcfunction(s, parseSpecial); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu day light colors loaded\n",dayColors.size()); + out.print("{} day light colors loaded\n",dayColors.size()); lua_pushcfunction(s, parseBuildings); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu buildings loaded\n",buildingDefs.size()); + out.print("{} buildings loaded\n",buildingDefs.size()); lua_pushcfunction(s, parseCreatures); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu creatures loaded\n",creatureDefs.size()); + out.print("{} creatures loaded\n",creatureDefs.size()); lua_pushcfunction(s, parseItems); lua_pushlightuserdata(s, this); lua_pushvalue(s,env); Lua::SafeCall(out,s,2,0); - out.print("%zu items loaded\n",itemDefs.size()); + out.print("{} items loaded\n",itemDefs.size()); } } } catch(std::exception& e) { - out.printerr("%s",e.what()); + out.printerr("{}\n",e.what()); } lua_pop(s,1); } diff --git a/plugins/rendermax/rendermax.cpp b/plugins/rendermax/rendermax.cpp index 5515bb39a2..39e59e4f6b 100644 --- a/plugins/rendermax/rendermax.cpp +++ b/plugins/rendermax/rendermax.cpp @@ -433,7 +433,7 @@ static command_result rendermax(color_ostream &out, vector & parameters else if(cmd=="disable") { if(current_mode==MODE_DEFAULT) - out.print("%s\n","Not installed, doing nothing."); + out.print("{}\n","Not installed, doing nothing."); else removeOld(); gps->force_full_display_count++; diff --git a/plugins/rendermax/rendermax.lua b/plugins/rendermax/rendermax.lua index f1cfa90203..6aacbde64f 100644 --- a/plugins/rendermax/rendermax.lua +++ b/plugins/rendermax/rendermax.lua @@ -164,7 +164,7 @@ function colorFrom16(col16) return {col[0],col[1],col[2]} end function addGems() - for k,v in pairs(df.global.world.raws.inorganics) do + for k,v in pairs(df.global.world.raws.inorganics.all) do if v.material.flags.IS_GEM then addMaterial("INORGANIC:"..v.id,colorFrom16(v.material.tile_color[0]+v.material.tile_color[2]*8)) end diff --git a/plugins/reveal.cpp b/plugins/reveal.cpp index 1feb72bbc6..e5bd2479f5 100644 --- a/plugins/reveal.cpp +++ b/plugins/reveal.cpp @@ -16,6 +16,7 @@ #include "df/map_block.h" #include "df/world.h" +#include #include using std::string; diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 994ac067ac..7eecabd2c5 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -69,7 +69,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { if (watched_seeds.count(id)) return watched_seeds[id]; string keyname = SEED_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(control,out).print("creating new persistent key for seed type %d\n", id); + DEBUG(control,out).print("creating new persistent key for seed type {}\n", id); watched_seeds.emplace(id, World::GetPersistentSiteData(keyname, true)); watched_seeds[id].set_int(SEED_CONFIG_ID, id); return watched_seeds[id]; @@ -77,7 +77,7 @@ static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { static void remove_seed_config(color_ostream &out, int id) { if (!watched_seeds.count(id)) return; - DEBUG(control,out).print("removing persistent key for seed type %d\n", id); + DEBUG(control,out).print("removing persistent key for seed type {}\n", id); World::DeletePersistentData(watched_seeds[id]); watched_seeds.erase(id); } @@ -90,12 +90,12 @@ static bool validate_seed_config(color_ostream& out, PersistentDataItem c) int seed_id = c.get_int(SEED_CONFIG_ID); auto plant = df::plant_raw::find(seed_id); if (!plant) { - WARN(control,out).print("discarded invalid seed id: %d\n", seed_id); + WARN(control,out).print("discarded invalid seed id: {}\n", seed_id); return false; } bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); if (!valid) { - DEBUG(control, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); + DEBUG(control, out).print("invalid configuration for {} discarded\n", plant->id); } return valid; } @@ -108,7 +108,7 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds = NULL, int3 static void seedwatch_setTarget(color_ostream &out, string name, int32_t num); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( @@ -144,19 +144,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -304,7 +304,7 @@ static void scan_seeds(color_ostream &out, unordered_map *acce } static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_t *num_disabled_seed_types) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -325,14 +325,14 @@ static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_ if (accessible_counts[id] <= target && (Kitchen::isPlantCookeryAllowed(id) || Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("disabling seed mat: %d\n", id); + DEBUG(cycle,out).print("disabling seed mat: {}\n", id); if (num_disabled_seed_types) ++*num_disabled_seed_types; Kitchen::denyPlantSeedCookery(id); } else if (target + TARGET_BUFFER < accessible_counts[id] && (!Kitchen::isPlantCookeryAllowed(id) || !Kitchen::isSeedCookeryAllowed(id))) { - DEBUG(cycle,out).print("enabling seed mat: %d\n", id); + DEBUG(cycle,out).print("enabling seed mat: {}\n", id); if (num_enabled_seed_types) ++*num_enabled_seed_types; Kitchen::allowPlantSeedCookery(id); @@ -351,7 +351,7 @@ static void set_target(color_ostream &out, int32_t id, int32_t target) { else { if (id < 0 || (size_t)id >= world->raws.plants.all.size()) { WARN(control,out).print( - "cannot set target for unknown plant id: %d\n", id); + "cannot set target for unknown plant id: {}\n", id); return; } PersistentDataItem &c = ensure_seed_config(out, id); @@ -385,7 +385,7 @@ static void seedwatch_setTarget(color_ostream &out, string name, int32_t num) { if (!world_plant_ids.count(token)) { token = toUpper_cp437(token); if (!world_plant_ids.count(token)) { - out.printerr("%s has not been found as a material.\n", token.c_str()); + out.printerr("{} has not been found as a material.\n", token); return; } } diff --git a/plugins/showmood.cpp b/plugins/showmood.cpp index 198dd190e0..977c767aeb 100644 --- a/plugins/showmood.cpp +++ b/plugins/showmood.cpp @@ -31,7 +31,7 @@ REQUIRE_GLOBAL(world); command_result df_showmood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -65,7 +65,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.printerr("Dwarf with strange mood does not have a mood type!\n"); continue; } - out.print("%s is currently ", DF2CONSOLE(out, Units::getReadableName(unit)).c_str()); + out.print("{} is currently ", DF2CONSOLE(out, Units::getReadableName(unit))); switch (unit->mood) { case mood_type::Macabre: @@ -143,7 +143,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) out.print("do something else..."); break; } - out.print(" and become a legendary %s", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); + out.print(" and become a legendary {}", ENUM_ATTR_STR(job_skill, caption_noun, unit->job.mood_skill)); if (unit->mood == mood_type::Possessed) out.print(" (but not really)"); break; @@ -160,7 +160,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) { string name; building->getName(&name); - out.print("claimed a %s and wants", name.c_str()); + out.print("claimed a {} and wants", name.c_str()); } else out.print("not yet claimed a workshop but will want"); @@ -169,7 +169,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) for (size_t i = 0; i < job->job_items.elements.size(); i++) { df::job_item *item = job->job_items.elements[i]; - out.print("Item %zu: ", i + 1); + out.print("Item {}: ", i + 1); MaterialInfo matinfo(item->mat_type, item->mat_index); @@ -178,37 +178,37 @@ command_result df_showmood (color_ostream &out, vector & parameters) switch (item->item_type) { case item_type::BOULDER: - out.print("%s boulder", mat_name.c_str()); + out.print("{} boulder", mat_name); break; case item_type::BLOCKS: - out.print("%s blocks", mat_name.c_str()); + out.print("{} blocks", mat_name); break; case item_type::WOOD: - out.print("%s logs", mat_name.c_str()); + out.print("{} logs", mat_name); break; case item_type::BAR: if (matinfo.isInorganicWildcard()) mat_name = "metal"; if (matinfo.inorganic && matinfo.inorganic->flags.is_set(inorganic_flags::WAFERS)) - out.print("%s wafers", mat_name.c_str()); + out.print("{} wafers", mat_name); else - out.print("%s bars", mat_name.c_str()); + out.print("{} bars", mat_name); break; case item_type::SMALLGEM: - out.print("%s cut gems", mat_name.c_str()); + out.print("{} cut gems", mat_name); break; case item_type::ROUGH: if (matinfo.isAnyInorganic()) { if (matinfo.isInorganicWildcard()) mat_name = "any"; - out.print("%s rough gems", mat_name.c_str()); + out.print("{} rough gems", mat_name); } else - out.print("raw %s", mat_name.c_str()); + out.print("raw {}", mat_name); break; case item_type::SKIN_TANNED: - out.print("%s leather", mat_name.c_str()); + out.print("{} leather", mat_name); break; case item_type::CLOTH: if (matinfo.isNone()) @@ -220,50 +220,51 @@ command_result df_showmood (color_ostream &out, vector & parameters) else if (item->flags2.bits.yarn) mat_name = "any yarn"; } - out.print("%s cloth", mat_name.c_str()); + out.print("{} cloth", mat_name); break; case item_type::REMAINS: - out.print("%s remains", mat_name.c_str()); + out.print("{} remains", mat_name); break; case item_type::CORPSE: - out.print("%s %scorpse", mat_name.c_str(), (item->flags1.bits.murdered ? "murdered " : "")); + out.print("{} {}corpse", mat_name, (item->flags1.bits.murdered ? "murdered " : "")); break; case item_type::NONE: if (item->flags2.bits.body_part) { if (item->flags2.bits.bone) - out.print("%s bones", mat_name.c_str()); + out.print("{} bones", mat_name); else if (item->flags2.bits.shell) - out.print("%s shells", mat_name.c_str()); + out.print("{} shells", mat_name); else if (item->flags2.bits.horn) - out.print("%s horns", mat_name.c_str()); + out.print("{} horns", mat_name); else if (item->flags2.bits.pearl) - out.print("%s pearls", mat_name.c_str()); + out.print("{} pearls", mat_name); else if (item->flags2.bits.ivory_tooth) - out.print("%s ivory/teeth", mat_name.c_str()); + out.print("{} ivory/teeth", mat_name); else - out.print("%s unknown body parts (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("{} unknown body parts ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); } else - out.print("indeterminate %s item (%s:%s:%s)", - mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("indeterminate {} item ({}:{}:{})", + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; default: { ItemTypeInfo itinfo(item->item_type, item->item_subtype); - out.print("item %s material %s flags (%s:%s:%s)", - itinfo.toString().c_str(), mat_name.c_str(), - bitfield_to_string(item->flags1).c_str(), - bitfield_to_string(item->flags2).c_str(), - bitfield_to_string(item->flags3).c_str()); + out.print("item {} material {} flags ({}:{}:{})", + itinfo.toString(), + mat_name, + bitfield_to_string(item->flags1), + bitfield_to_string(item->flags2), + bitfield_to_string(item->flags3)); break; } } @@ -280,7 +281,7 @@ command_result df_showmood (color_ostream &out, vector & parameters) if (job->items[j]->job_item_idx == int32_t(i)) count_got += 1; } - out.print(", got %i of %i\n", count_got, + out.print(", got {} of {}\n", count_got, item->quantity < divisor ? item->quantity : item->quantity/divisor); } } diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp index ca30fc76b4..3d9fbd537d 100644 --- a/plugins/siege-engine.cpp +++ b/plugins/siege-engine.cpp @@ -1503,7 +1503,7 @@ static void releaseTiredWorker(EngineInfo *engine, df::job *job, df::unit *worke if (Job::removeWorker(job)) { color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("Released tired operator %d from siege engine.\n", worker->id); + out.print("Released tired operator {} from siege engine.\n", worker->id); if (process_jobs) *process_jobs = true; diff --git a/plugins/smooth-movement/CMakeLists.txt b/plugins/smooth-movement/CMakeLists.txt new file mode 100644 index 0000000000..acb0650a7e --- /dev/null +++ b/plugins/smooth-movement/CMakeLists.txt @@ -0,0 +1,5 @@ +dfhack_plugin(smooth-movement smooth-movement.cpp LINK_LIBRARIES lua) +if(BUILD_PLUGINS) + target_include_directories(smooth-movement PRIVATE ${SDL2_INCLUDE_DIRS}) +endif() +dfhack_test(smooth-movement-test test_visual_animation_manager.cpp) diff --git a/plugins/smooth-movement/smooth-movement.cpp b/plugins/smooth-movement/smooth-movement.cpp new file mode 100644 index 0000000000..bf32590fc2 --- /dev/null +++ b/plugins/smooth-movement/smooth-movement.cpp @@ -0,0 +1,978 @@ +// SPDX-License-Identifier: Zlib + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Core.h" +#include "MemAccess.h" +#include "PluginManager.h" +#include "VTableInterpose.h" + +#include "modules/DFSDL.h" + +#include "df/coord2d.h" +#include "df/global_objects.h" +#include "df/graphic.h" +#include "df/graphic_viewportst.h" +#include "df/renderer_2d_base.h" +#include "df/texture_fullid.h" +#include "df/viewport_spatter_flag.h" + +#include "visual_animation.h" + +using namespace DFHack; + +DFHACK_PLUGIN("smooth-movement"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(gps); +REQUIRE_GLOBAL(window_x); +REQUIRE_GLOBAL(window_y); +REQUIRE_GLOBAL(window_z); + +namespace { + +using tile_coverage = std::set; + +// Runtime harness for the engine-owned visual state; gameplay data is never +// read. +decltype(&SDL_RenderCopyF) render_copy_f = nullptr; +decltype(&SDL_RenderCopyExF) render_copy_ex_f = nullptr; +decltype(&SDL_RenderFillRect) render_fill_rect = nullptr; +decltype(&SDL_GetRenderDrawColor) get_render_draw_color = nullptr; +decltype(&SDL_SetRenderDrawColor) set_render_draw_color = nullptr; + +visual_animation_manager animation_manager; +tile_coverage previous_coverage; +uint64_t visual_context_revision = 0; +const void *previous_viewport = nullptr; +struct visual_context { + int32_t z_level; + df::coord2d viewport_dimensions; + df::coord2d clip_min; + df::coord2d clip_max; + int32_t zoom; + df::coord2d origin; + df::coord2d screen_dimensions; + + bool operator==(const visual_context &) const = default; +}; + +visual_context previous_visual_context{}; +bool has_visual_context = false; +// Map scroll (window_x/window_y) is tracked separately from the reset +// signature: a pure pan is followed (movements are translated) instead of +// triggering a full reset, so it must NOT bump the context revision. It only +// invalidates the viewport-space blackout coverage from the prior frame. +df::coord2d previous_pan = df::coord2d(0, 0); +bool has_pan_context = false; +bool flip_enabled = false; +bool zlevel_enabled = false; + +// DF's native renderer draws 32-pixel tiles at viewport zoom 128. +constexpr int32_t native_tile_pixels = 32; +constexpr int32_t native_viewport_zoom = 128; + +void update_visual_context(const df::renderer_2d_base *renderer, const df::graphic_viewportst *vp) { + // window_x/window_y are deliberately excluded: a horizontal/vertical scroll + // is followed, not reset. window_z (z-level) stays, since a z change is not + // followable. + const visual_context context = { + .z_level = window_z ? *window_z : 0, + .viewport_dimensions = df::coord2d(vp->dim_x, vp->dim_y), + // DF clip arrays hold inclusive minimum and maximum coordinates. + .clip_min = df::coord2d(vp->clipx[0], vp->clipy[0]), + .clip_max = df::coord2d(vp->clipx[1], vp->clipy[1]), + .zoom = renderer->viewport_zoom_factor, + .origin = df::coord2d(renderer->origin_x, renderer->origin_y), + .screen_dimensions = df::coord2d(gps->dimx, gps->dimy), + }; + const bool changed = + !has_visual_context || previous_viewport != vp || previous_visual_context != context; + if (changed) { + ++visual_context_revision; + previous_coverage.clear(); + } + previous_viewport = vp; + previous_visual_context = context; + has_visual_context = true; + + // On a pure pan the reset signature is unchanged, but last frame's blackout + // coverage is in the old viewport frame, so discard it (the engine repaints + // the whole scrolled viewport anyway). + const df::coord2d pan(window_x ? *window_x : 0, window_y ? *window_y : 0); + if (!has_pan_context || previous_pan != pan) + previous_coverage.clear(); + previous_pan = pan; + has_pan_context = true; +} + +using viewport_layer_member = int32_t *df::graphic_viewportst::*; + +struct visual_layer_buffer { + viewport_visual_layer layer; + viewport_layer_member current; + viewport_layer_member previous; +}; + +constexpr size_t visual_layer_count = static_cast(viewport_visual_layer::count); +constexpr std::array visual_layer_buffers = { + visual_layer_buffer{viewport_visual_layer::right, + &df::graphic_viewportst::screentexpos_right_creature, + &df::graphic_viewportst::screentexpos_right_creature_old}, + visual_layer_buffer{viewport_visual_layer::center, &df::graphic_viewportst::screentexpos, + &df::graphic_viewportst::screentexpos_old}, + visual_layer_buffer{viewport_visual_layer::left, + &df::graphic_viewportst::screentexpos_left_creature, + &df::graphic_viewportst::screentexpos_left_creature_old}, + visual_layer_buffer{viewport_visual_layer::upright, + &df::graphic_viewportst::screentexpos_upright_creature, + &df::graphic_viewportst::screentexpos_upright_creature_old}, + visual_layer_buffer{viewport_visual_layer::up, + &df::graphic_viewportst::screentexpos_up_creature, + &df::graphic_viewportst::screentexpos_up_creature_old}, + visual_layer_buffer{viewport_visual_layer::upleft, + &df::graphic_viewportst::screentexpos_upleft_creature, + &df::graphic_viewportst::screentexpos_upleft_creature_old}, + visual_layer_buffer{viewport_visual_layer::vehicle, + &df::graphic_viewportst::screentexpos_vehicle, + &df::graphic_viewportst::screentexpos_vehicle_old}, + visual_layer_buffer{viewport_visual_layer::item, &df::graphic_viewportst::screentexpos_item, + &df::graphic_viewportst::screentexpos_item_old}, + visual_layer_buffer{viewport_visual_layer::designation, + &df::graphic_viewportst::screentexpos_designation, + &df::graphic_viewportst::screentexpos_designation_old}}; + +constexpr bool valid_visual_layer_buffers() { + uint16_t layers = 0; + for (const auto &buffer : visual_layer_buffers) { + const uint16_t layer = uint16_t(1U << static_cast(buffer.layer)); + if (layers & layer) + return false; + layers |= layer; + } + return layers == uint16_t((1U << visual_layer_count) - 1); +} + +static_assert(valid_visual_layer_buffers()); + +template auto visual_layers(Viewport *vp, bool previous = false) { + using layer_pointer = std::conditional_t, const int32_t *, int32_t *>; + std::array layers{}; + for (const auto &buffer : visual_layer_buffers) + layers[static_cast(buffer.layer)] = + vp->*(previous ? buffer.previous : buffer.current); + return layers; +} + +viewport_visual_animation_input animation_input(df::graphic_viewportst *vp) { + const df::graphic_viewportst *const_viewport = vp; + return {vp, + df::coord2d(vp->dim_x, vp->dim_y), + visual_context_revision, + visual_layers(const_viewport), + visual_layers(const_viewport, true), + df::coord2d(window_x ? *window_x : 0, window_y ? *window_y : 0)}; +} + +// The layer buffers are freed and nulled without clearing the active flag. +bool viewport_readable(df::graphic_viewportst *vp) { + return vp != nullptr && vp->flag.bits.active && animation_input(vp).valid(); +} + +int32_t tile_size(int32_t zoom) { + return std::max(1, zoom * native_tile_pixels / native_viewport_zoom); +} + +int32_t tile_pixel(int32_t tile, int32_t origin, int32_t zoom) { + return tile_size(zoom) * tile + origin; +} + +bool inside_clip(const df::graphic_viewportst *vp, df::coord2d pos) { + return pos.x >= vp->clipx[0] && pos.x <= vp->clipx[1] && pos.y >= vp->clipy[0] && + pos.y <= vp->clipy[1]; +} + +bool inside_clip(const df::graphic_viewportst *vp, int32_t x, int32_t y) { + return inside_clip(vp, df::coord2d(x, y)); +} + +bool has_fire(const df::graphic_viewportst *vp, int32_t x, int32_t y) { + return vp->screentexpos_spatter_flag != nullptr && + vp->screentexpos_spatter_flag[x * vp->dim_y + y].bits.fire_frame_type != 0; +} + +template class scoped_value_restorest { + T &value; + T saved; + + public: + explicit scoped_value_restorest(T &value, T replacement = T{}) + : value(value), saved(std::exchange(value, std::move(replacement))) { + static_assert(std::is_nothrow_move_assignable_v); + } + + ~scoped_value_restorest() noexcept { value = std::move(saved); } + + scoped_value_restorest(const scoped_value_restorest &) = delete; + scoped_value_restorest &operator=(const scoped_value_restorest &) = delete; + scoped_value_restorest(scoped_value_restorest &&) = delete; + scoped_value_restorest &operator=(scoped_value_restorest &&) = delete; +}; + +template void with_zeroed_values(const Callback &callback) { callback(); } + +template +void with_zeroed_values(const Callback &callback, T &value, Values &...values) { + scoped_value_restorest zero(value); + with_zeroed_values(callback, values...); +} + +struct render_proxy { + viewport_visual_layer layer; + DFHack::Coord2d source; + df::coord2d target; + int32_t texpos; + float progress; + SDL_Texture *texture; + bool mirrored = false; + int32_t mirror_shift = 0; + tile_coverage coverage; + visual_movement_id movement_id = no_visual_movement; +}; + +struct render_coverage { + tile_coverage all; + std::array(visual_render_group::count)> groups; + std::unordered_map selected; +}; + +struct viewport_render { + df::graphic_viewportst *viewport; + std::vector proxies; + render_coverage coverage; +}; + +constexpr uint16_t visual_layer_bit(viewport_visual_layer layer) { + return uint16_t(1U << static_cast(layer)); +} + +uint16_t selected_mask(const std::unordered_map &selected, int32_t index) { + const auto found = selected.find(index); + return found == selected.end() ? 0 : found->second; +} + +template +void with_suppressed_visual_layers(const std::array &layers, + int32_t index, uint16_t mask, const Callback &callback) { + if constexpr (Layer == visual_layer_count) + callback(); + else if (mask & (1U << Layer)) { + scoped_value_restorest zero(layers[Layer][index]); + with_suppressed_visual_layers(layers, index, mask, callback); + } else + with_suppressed_visual_layers(layers, index, mask, callback); +} + +template +void with_base_suppressed(df::graphic_viewportst *vp, int32_t index, const Callback &callback) { + with_zeroed_values(callback, vp->screentexpos_background[index], + vp->screentexpos_floor_flag[index], vp->screentexpos_background_two[index], + vp->screentexpos_liquid_flag[index], vp->screentexpos_spatter_flag[index], + vp->screentexpos_spatter[index], vp->screentexpos_ramp_flag[index], + vp->screentexpos_shadow_flag[index], vp->screentexpos_building_one[index]); +} + +template +void with_main_suppressed(df::graphic_viewportst *vp, int32_t index, const Callback &callback) { + with_base_suppressed(vp, index, + [&] { with_zeroed_values(callback, vp->screentexpos_vermin[index]); }); +} + +template +void with_upper_suppressed(df::graphic_viewportst *vp, int32_t index, const Callback &callback) { + with_main_suppressed(vp, index, [&] { + with_zeroed_values(callback, vp->screentexpos_building_two[index], + vp->screentexpos_projectile[index], vp->screentexpos_high_flow[index], + vp->screentexpos_top_shadow[index], vp->screentexpos_signpost[index]); + }); +} + +void redraw_viewport_tile(df::renderer_2d_base *renderer, const viewport_render &viewport, + int32_t x, int32_t y, bool defer_interface) { + df::graphic_viewportst *vp = viewport.viewport; + const int32_t index = x * vp->dim_y + y; + const auto redraw = [&] { renderer->update_viewport_tile(vp, x, y); }; + const auto stage = [&] { + with_suppressed_visual_layers(visual_layers(vp), index, + selected_mask(viewport.coverage.selected, index), redraw); + }; + // The interface layer is the shading for levels below the camera. + // A staged tile has a sprite drawn over it afterwards, so draw_interface_only + // places it instead. + if (!defer_interface || vp->screentexpos_interface == nullptr) + stage(); + else + with_zeroed_values(stage, vp->screentexpos_interface[index]); +} + +// Every buffer the interface-only pass zeroes has to exist before it can be +// zeroed. +bool interface_pass_readable(const df::graphic_viewportst *vp) { + return vp != nullptr && vp->screentexpos_interface != nullptr && + vp->screentexpos_background != nullptr && vp->screentexpos_floor_flag != nullptr && + vp->screentexpos_background_two != nullptr && vp->screentexpos_liquid_flag != nullptr && + vp->screentexpos_spatter_flag != nullptr && vp->screentexpos_spatter != nullptr && + vp->screentexpos_ramp_flag != nullptr && vp->screentexpos_shadow_flag != nullptr && + vp->screentexpos_building_one != nullptr && vp->screentexpos_vermin != nullptr && + vp->screentexpos_building_two != nullptr && vp->screentexpos_projectile != nullptr && + vp->screentexpos_high_flow != nullptr && vp->screentexpos_signpost != nullptr; +} + +// Runs after the proxies so the shading covers them rather than sitting +// underneath. +void draw_interface_only(df::renderer_2d_base *renderer, df::graphic_viewportst *vp, + df::coord2d pos) { + if (!interface_pass_readable(vp)) + return; + const int32_t index = pos.x * vp->dim_y + pos.y; + const auto redraw = [&] { renderer->update_viewport_tile(vp, pos.x, pos.y); }; + const auto without_visuals = [&] { + with_suppressed_visual_layers(visual_layers(vp), index, + uint16_t((1U << visual_layer_count) - 1), redraw); + }; + with_zeroed_values(without_visuals, vp->screentexpos_background[index], + vp->screentexpos_floor_flag[index], vp->screentexpos_background_two[index], + vp->screentexpos_liquid_flag[index], vp->screentexpos_spatter_flag[index], + vp->screentexpos_spatter[index], vp->screentexpos_ramp_flag[index], + vp->screentexpos_shadow_flag[index], vp->screentexpos_building_one[index], + vp->screentexpos_vermin[index], vp->screentexpos_building_two[index], + vp->screentexpos_projectile[index], vp->screentexpos_high_flow[index], + vp->screentexpos_signpost[index]); +} + +void redraw_world_tile(df::renderer_2d_base *renderer, + const std::vector &viewports, + const tile_coverage &staged, int32_t x, int32_t y) { + // The stage pass repaints everything above the lowest across the staged + // tiles, after the proxies. + const bool staged_tile = staged.count(df::coord2d(x, y)) != 0; + for (const viewport_render &viewport : viewports) { + if (inside_clip(viewport.viewport, x, y)) + redraw_viewport_tile(renderer, viewport, x, y, staged_tile); + if (staged_tile) + break; + } +} + +constexpr uint16_t visual_layers_through_group(visual_render_group group) { + uint16_t mask = 0; + for (const auto &descriptor : visual_layer_descriptors) + if (descriptor.render_group != visual_render_group::designation && + static_cast(descriptor.render_group) <= static_cast(group)) + mask |= visual_layer_bit(descriptor.layer); + return mask; +} + +void redraw_above(df::renderer_2d_base *renderer, df::graphic_viewportst *vp, int32_t x, int32_t y, + visual_render_group group, + const std::unordered_map &selected) { + const int32_t index = x * vp->dim_y + y; + const auto redraw = [&] { renderer->update_viewport_tile(vp, x, y); }; + const auto suppress_visuals = [&] { + const auto stage = [&] { + with_suppressed_visual_layers( + visual_layers(vp), index, + selected_mask(selected, index) | visual_layers_through_group(group), redraw); + }; + // The interface layer sits above every group, so each group's redraw would + // paint it again. draw_interface_only places it once, after the sprites. + if (vp->screentexpos_interface == nullptr) + stage(); + else + with_zeroed_values(stage, vp->screentexpos_interface[index]); + }; + if (group == visual_render_group::item || group == visual_render_group::vehicle) + with_base_suppressed(vp, index, suppress_visuals); + else if (group == visual_render_group::main) + with_main_suppressed(vp, index, suppress_visuals); + else + with_upper_suppressed(vp, index, suppress_visuals); +} + +SDL_Texture *cached_texture(df::renderer_2d_base *renderer, int32_t texpos, + bool transparent_background = true) { + if (texpos == 0) + return nullptr; + df::texture_fullid texture_id; + texture_id.texpos = texpos; + texture_id.r = texture_id.g = texture_id.b = 1.0f; + texture_id.br = texture_id.bg = texture_id.bb = 0.0f; + texture_id.flag = + transparent_background ? df::texture_fullid_flag::mask_transparent_background : 0; + const auto texture = renderer->tile_cache.tile_cache.find(texture_id); + return texture == renderer->tile_cache.tile_cache.end() + ? nullptr + : static_cast(texture->second); +} + +// The render_copy_ex_f null check is defensive only, not a graceful-degradation +// path. `bind` aborts load_sdl on any missing symbol and plugin_enable then +// refuses the render hook. +void render_copy_maybe_mirrored(SDL_Renderer *renderer, SDL_Texture *texture, + const SDL_FRect &destination, bool mirrored) { + if (mirrored && render_copy_ex_f != nullptr) { + render_copy_ex_f(renderer, texture, nullptr, &destination, 0.0, nullptr, + SDL_FLIP_HORIZONTAL); + return; + } + render_copy_f(renderer, texture, nullptr, &destination); +} + +void draw_proxy(df::renderer_2d_base *renderer, const render_proxy &proxy) { + const int32_t zoom = renderer->viewport_zoom_factor; + const DFHack::Coord2d target{ + tile_pixel(proxy.target.x, renderer->origin_x, zoom), + tile_pixel(proxy.target.y, renderer->origin_y, zoom)}; + const float tile_size = float(::tile_size(zoom)); + const float source_x = target.x + (proxy.source.x - proxy.target.x) * tile_size; + const float source_y = target.y + (proxy.source.y - proxy.target.y) * tile_size; + const float mirror_offset = float(proxy.mirror_shift) * tile_size; + const SDL_FRect destination = { + source_x + (target.x - source_x) * proxy.progress + mirror_offset, + source_y + (target.y - source_y) * proxy.progress, tile_size, tile_size}; + render_copy_maybe_mirrored(static_cast(renderer->sdl_renderer), proxy.texture, + destination, proxy.mirrored); +} + +std::vector collect_proxies(df::renderer_2d_base *renderer, + df::graphic_viewportst *vp, bool movement_enabled) { + std::vector proxies; + auto layers = visual_layers(vp); + auto previous_layers = visual_layers(vp, true); + for (const auto visual_layer : visual_layer_draw_order) { + const size_t layer = static_cast(visual_layer); + for (int32_t y = 0; y < vp->dim_y; ++y) { + for (int32_t x = 0; x < vp->dim_x; ++x) { + const int32_t index = x * vp->dim_y + y; + const int32_t texpos = layers[layer][index]; + if (texpos == 0 || !movement_enabled) + continue; + const auto movement = animation_manager.get_movement( + vp, static_cast(layer), x, y); + if (!movement.active) + continue; + const DFHack::Coord2d target{float(x), float(y)}; + const auto movement_delta = movement.source - target; + const int32_t inherited_source_x = + inherited_visual_source_tile(x, movement.source.x, x); + const int32_t inherited_source_y = + inherited_visual_source_tile(y, movement.source.y, y); + const bool inherited_source_in_bounds = + inherited_source_x >= 0 && inherited_source_x < vp->dim_x && + inherited_source_y >= 0 && inherited_source_y < vp->dim_y; + if (!visual_layer_moves_independently(visual_layer)) { + bool anchored = false; + for (const render_proxy &anchor : proxies) { + // Creature fragments lie no more than one tile from their center layer. + if (anchor.layer == viewport_visual_layer::center && + std::abs(anchor.target.x - x) <= 1 && + std::abs(anchor.target.y - y) <= 1 && + (visual_layer != viewport_visual_layer::designation + ? anchor.movement_id == movement.movement_id + : (anchor.source - + DFHack::Coord2d{float(anchor.target.x), float(anchor.target.y)} == + movement_delta && + anchor.progress == movement.progress))) + anchored = true; + } + if (!anchored) + continue; + } + if ((visual_layer == viewport_visual_layer::item || + visual_layer == viewport_visual_layer::designation) && + movement.inherited) { + if (visual_layer == viewport_visual_layer::item && + vp->screentexpos_old[index] != 0) + continue; + if (!inherited_source_in_bounds) + continue; + const int32_t source = inherited_source_x * vp->dim_y + inherited_source_y; + if (!visual_moved_between_tiles(visual_layer, layers[layer], + previous_layers[layer], source, index)) + continue; + } + if (!visual_layer_moves_independently(visual_layer) && + visual_layer != viewport_visual_layer::designation && movement.inherited) { + const bool fragment_moved = + inherited_source_in_bounds && + visual_moved_between_tiles( + visual_layer, layers[layer], previous_layers[layer], + inherited_source_x * vp->dim_y + inherited_source_y, index); + if (!fragment_moved) { + const auto &descriptor = visual_layer_descriptor(visual_layer); + const DFHack::Coord2d fragment_target{x, y}; + bool owns_fragment = false; + for (const render_proxy &anchor : proxies) + if (anchor.layer == viewport_visual_layer::center && + DFHack::Coord2d{anchor.target.x, anchor.target.y} == + fragment_target + + DFHack::Coord2d{descriptor.anchor_offset} && + anchor.movement_id == movement.movement_id) + owns_fragment = true; + if (!owns_fragment) + continue; + } + } + + // Items, vehicles and designations keep their vanilla orientation. + const auto &mirror_descriptor = visual_layer_descriptor(visual_layer); + const visual_render_group group = get_visual_render_group(visual_layer); + const bool mirror_eligible = + flip_enabled && + (group == visual_render_group::main || group == visual_render_group::upper); + // Facing is read from the anchor tile so every fragment of one creature + // agrees. + const df::coord2d anchor(x + mirror_descriptor.anchor_offset.x, + y + mirror_descriptor.anchor_offset.y); + const bool mirrored = mirror_eligible && + animation_manager.get_facing(vp, anchor) != native_sprite_facing; + // The anchor's own layer has no offset, so it flips in place. + const int32_t mirror_shift = + mirrored ? mirrored_tile_x(x, x + mirror_descriptor.anchor_offset.x) - x : 0; + render_proxy proxy = {static_cast(layer), + movement.source, + df::coord2d(x, y), + texpos, + movement.progress, + nullptr, + mirrored, + mirror_shift, + {}}; + bool blocked = false; + const int32_t coverage_min_x = int32_t(std::floor(std::min(proxy.source.x, float(x)))); + const int32_t coverage_max_x = int32_t(std::ceil(std::max(proxy.source.x, float(x)))); + const int32_t coverage_min_y = int32_t(std::floor(std::min(proxy.source.y, float(y)))); + const int32_t coverage_max_y = int32_t(std::ceil(std::max(proxy.source.y, float(y)))); + for (int32_t coverage_x = coverage_min_x; coverage_x <= coverage_max_x; + ++coverage_x) { + for (int32_t coverage_y = coverage_min_y; coverage_y <= coverage_max_y; + ++coverage_y) { + if (!inside_clip(vp, coverage_x, coverage_y)) { + blocked = true; + break; + } + if (get_visual_render_group(proxy.layer) == visual_render_group::main && + has_fire(vp, coverage_x, coverage_y)) { + blocked = true; + break; + } + proxy.coverage.emplace(coverage_x, coverage_y); + } + if (blocked) + break; + } + if (blocked) + continue; + if (proxy.mirror_shift != 0) { + tile_coverage mirrored_coverage; + for (const auto &tile : proxy.coverage) + mirrored_coverage.emplace(tile.x + proxy.mirror_shift, tile.y); + for (const auto &tile : mirrored_coverage) { + if (!inside_clip(vp, tile.x, tile.y)) { + blocked = true; + break; + } + if (get_visual_render_group(proxy.layer) == visual_render_group::main && + has_fire(vp, tile.x, tile.y)) { + blocked = true; + break; + } + proxy.coverage.insert(tile); + } + if (blocked) + continue; + } + + proxy.texture = cached_texture(renderer, texpos); + if (proxy.texture == nullptr) + continue; + proxy.movement_id = movement.movement_id; + proxies.push_back(std::move(proxy)); + } + } + } + + // A creature that has stopped still needs its mirrored sprite painted each + // frame. Otherwise the engine repaints it natively and the two orientations + // alternate between steps. A fragment's tile is its anchor minus the layer's + // centre offset, inverting the moving path. + if (flip_enabled) { + for (int32_t anchor_x = 0; anchor_x < vp->dim_x; ++anchor_x) { + for (int32_t anchor_y = 0; anchor_y < vp->dim_y; ++anchor_y) { + const df::coord2d anchor(anchor_x, anchor_y); + if (animation_manager.get_facing(vp, anchor) == native_sprite_facing) + continue; + for (const auto visual_layer : visual_layer_draw_order) { + const visual_render_group group = get_visual_render_group(visual_layer); + if (group != visual_render_group::main && + group != visual_render_group::upper) + continue; + const auto &descriptor = visual_layer_descriptor(visual_layer); + const int32_t x = anchor.x - descriptor.anchor_offset.x; + const int32_t y = anchor.y - descriptor.anchor_offset.y; + if (x < 0 || x >= vp->dim_x || y < 0 || y >= vp->dim_y) + continue; + const size_t layer = static_cast(visual_layer); + const int32_t texpos = layers[layer][x * vp->dim_y + y]; + if (texpos == 0) + continue; + bool already_drawn = false; + for (const render_proxy &existing : proxies) + if (existing.layer == visual_layer && existing.target.x == x && + existing.target.y == y) + already_drawn = true; + if (already_drawn) + continue; + + // source == target at progress 1.0 draws in place, moved only by + // mirror_shift. + render_proxy proxy = {visual_layer, + {float(x), float(y)}, + df::coord2d(x, y), + texpos, + 1.0f, + nullptr, + true, + mirrored_tile_x(x, anchor.x) - x, + {}}; + // The sprite lands on x+mirror_shift, so that interval must be + // repaintable. The shift has either sign, so order the interval ends + // first. + const int32_t coverage_first = std::min(x, x + proxy.mirror_shift); + const int32_t coverage_last = std::max(x, x + proxy.mirror_shift); + bool blocked = false; + for (int32_t coverage_x = coverage_first; coverage_x <= coverage_last; + ++coverage_x) { + if (!inside_clip(vp, coverage_x, y) || + (group == visual_render_group::main && has_fire(vp, coverage_x, y))) { + blocked = true; + break; + } + proxy.coverage.emplace(coverage_x, y); + } + if (blocked) + continue; + + proxy.texture = cached_texture(renderer, texpos); + if (proxy.texture == nullptr) + continue; + proxies.push_back(std::move(proxy)); + } + } + } + } + return proxies; +} + +render_coverage collect_coverage(const std::vector &proxies, int32_t dim_y) { + render_coverage coverage; + for (const render_proxy &proxy : proxies) { + coverage.all.insert(proxy.coverage.begin(), proxy.coverage.end()); + // TODO: use std::mdspan when C++23 available + coverage.selected[proxy.target.x * dim_y + proxy.target.y] |= visual_layer_bit(proxy.layer); + auto &group = coverage.groups[static_cast(get_visual_render_group(proxy.layer))]; + group.insert(proxy.coverage.begin(), proxy.coverage.end()); + } + return coverage; +} + +std::vector active_viewports() { + std::vector viewports; + if (gps == nullptr) + return viewports; + for (size_t lower = std::size(gps->lower_viewport); lower-- > 0;) { + df::graphic_viewportst *vp = gps->lower_viewport[lower]; + if (viewport_readable(vp)) + viewports.push_back(vp); + } + if (viewport_readable(gps->main_viewport)) + viewports.push_back(gps->main_viewport); + return viewports; +} + +std::vector +collect_viewport_renders(df::renderer_2d_base *renderer, + const std::vector &viewports) { + std::vector renders; + renders.reserve(viewports.size()); + for (df::graphic_viewportst *vp : viewports) { + const bool movement_enabled = zlevel_enabled || vp == gps->main_viewport; + viewport_render render = {vp, collect_proxies(renderer, vp, movement_enabled), {}}; + render.coverage = collect_coverage(render.proxies, vp->dim_y); + renders.push_back(std::move(render)); + } + return renders; +} + +tile_coverage collect_viewport_coverage(const std::vector &viewports) { + tile_coverage coverage; + for (const viewport_render &viewport : viewports) + coverage.insert(viewport.coverage.all.begin(), viewport.coverage.all.end()); + return coverage; +} + +void draw_interpolation_stages(df::renderer_2d_base *renderer, df::graphic_viewportst *vp, + const std::vector &proxies, + const render_coverage &coverage) { + for (size_t index = 0; index < coverage.groups.size(); ++index) { + const auto group = static_cast(index); + for (const render_proxy &proxy : proxies) + if (get_visual_render_group(proxy.layer) == group) + draw_proxy(renderer, proxy); + if (group == visual_render_group::designation) + continue; + for (const auto &[x, y] : coverage.groups[index]) + redraw_above(renderer, vp, x, y, group, coverage.selected); + } +} + +void redraw_viewport_tiles(df::renderer_2d_base *renderer, const viewport_render &viewport, + const tile_coverage &coverage) { + df::graphic_viewportst *vp = viewport.viewport; + for (const df::coord2d &tile : coverage) { + if (!inside_clip(vp, tile)) + continue; + redraw_viewport_tile(renderer, viewport, tile.x, tile.y, true); + } +} + +void draw_viewport_interpolation_stages(df::renderer_2d_base *renderer, + const std::vector &viewports, + const tile_coverage &coverage) { + for (size_t index = 0; index < viewports.size(); ++index) { + // A lower z-level's proxy must be covered by the next viewport's fog and + // terrain. Reapply that viewport before its own proxies, matching DF's + // lower-to-main draw order. + if (index > 0) + redraw_viewport_tiles(renderer, viewports[index], coverage); + const viewport_render &viewport = viewports[index]; + draw_interpolation_stages(renderer, viewport.viewport, viewport.proxies, viewport.coverage); + // A viewport shades everything drawn beneath it, so this covers every + // staged tile. Restricting it to the tiles this viewport has sprites on + // would not deepen with distance. + for (const df::coord2d &tile : coverage) { + if (inside_clip(viewport.viewport, tile)) + draw_interface_only(renderer, viewport.viewport, tile); + } + } +} + +bool has_mirrored_viewport_facing(const std::vector &viewports) { + for (const df::graphic_viewportst *vp : viewports) + if (animation_manager.has_mirrored_facing(vp)) + return true; + return false; +} + +void render_interpolated_world(df::renderer_2d_base *renderer) { + df::graphic_viewportst *vp = gps ? gps->main_viewport : nullptr; + const std::vector viewports = active_viewports(); + + if (vp != nullptr) + update_visual_context(renderer, vp); + const uint32_t now_ms = Core::getInstance().p->getTickCount(); + animation_manager.begin_frame(now_ms); + for (df::graphic_viewportst *viewport : viewports) + animation_manager.synchronize_viewport(animation_input(viewport)); + animation_manager.end_frame(); + + if (!viewport_readable(vp) || renderer->sdl_renderer == nullptr) + return; + const bool animation_redraw = + zlevel_enabled ? animation_manager.requires_full_redraw() + : animation_manager.has_active_movement(vp) || !previous_coverage.empty(); + if (!animation_redraw && (!flip_enabled || !has_mirrored_viewport_facing(viewports))) + return; + + std::vector viewport_renders = collect_viewport_renders(renderer, viewports); + tile_coverage coverage = collect_viewport_coverage(viewport_renders); + + SDL_Renderer *sdl_renderer = static_cast(renderer->sdl_renderer); + const int32_t zoom = renderer->viewport_zoom_factor; + const int32_t tile_size = ::tile_size(zoom); + + tile_coverage redraw_coverage = coverage; + redraw_coverage.insert(previous_coverage.begin(), previous_coverage.end()); + Uint8 old_r = 0, old_g = 0, old_b = 0, old_a = 255; + get_render_draw_color(sdl_renderer, &old_r, &old_g, &old_b, &old_a); + set_render_draw_color(sdl_renderer, 0, 0, 0, 255); + for (const auto &[x, y] : redraw_coverage) { + if (!inside_clip(vp, x, y)) + continue; + const SDL_Rect tile_rect = {tile_pixel(x, renderer->origin_x, zoom), + tile_pixel(y, renderer->origin_y, zoom), tile_size, tile_size}; + render_fill_rect(sdl_renderer, &tile_rect); + } + set_render_draw_color(sdl_renderer, old_r, old_g, old_b, old_a); + + for (const auto &[x, y] : redraw_coverage) { + if (inside_clip(vp, x, y)) + redraw_world_tile(renderer, viewport_renders, coverage, x, y); + } + draw_viewport_interpolation_stages(renderer, viewport_renders, coverage); + + previous_coverage = std::move(coverage); +} + +struct renderer_hook : df::renderer_2d_base { + typedef df::renderer_2d_base interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, update_all, ()); +}; + +IMPLEMENT_VMETHOD_INTERPOSE(renderer_hook, update_all); + +void renderer_hook::interpose_fn_update_all() { + // update_all is the existing UI stage, so world correction must run first. + render_interpolated_world(this); + INTERPOSE_NEXT(update_all)(); +} + +void clear_sdl_bindings() { + render_copy_f = nullptr; + render_copy_ex_f = nullptr; + render_fill_rect = nullptr; + get_render_draw_color = nullptr; + set_render_draw_color = nullptr; +} + +bool load_sdl(color_ostream &out) { + clear_sdl_bindings(); + DFLibrary *sdl_handle = DFSDL::obtain_library_handle(); +#define bind(name, target) \ + target = reinterpret_cast(LookupPlugin(sdl_handle, #name)); \ + if (target == nullptr) { \ + out.printerr("smooth-movement: SDL2 function unavailable: " #name "\n"); \ + clear_sdl_bindings(); \ + return false; \ + } + bind(SDL_RenderCopyF, render_copy_f); + bind(SDL_RenderCopyExF, render_copy_ex_f); + bind(SDL_RenderFillRect, render_fill_rect); + bind(SDL_GetRenderDrawColor, get_render_draw_color); + bind(SDL_SetRenderDrawColor, set_render_draw_color); +#undef bind + return true; +} + +void reset_state() { + animation_manager = visual_animation_manager(); + previous_coverage.clear(); + visual_context_revision = 0; + previous_viewport = nullptr; + previous_visual_context = {}; + has_visual_context = false; + previous_pan = {}; + has_pan_context = false; + flip_enabled = false; + zlevel_enabled = false; +} + +command_result status_command(color_ostream &out, std::vector ¶meters) { + if (parameters.empty()) { + out.print("smooth-movement: {}\n", is_enabled ? "enabled" : "disabled"); + out.print("sprite flipping: {}\n", flip_enabled ? "on" : "off"); + out.print("lower z-level animation: {}\n", zlevel_enabled ? "on" : "off"); + return CR_OK; + } + if (parameters[0] == "flip") { + if (parameters.size() == 1) { + out.print("sprite flipping: {}\n", flip_enabled ? "on" : "off"); + return CR_OK; + } + // A toggle changes the screen without changing anything DF knows, so DF + // will not repaint. OFF matters most: the render path stops touching tiles + // it painted every frame. The last mirrored frame would persist. Same flush + // plugin_enable(false) uses. + if (parameters.size() == 2 && parameters[1] == "on") { + flip_enabled = true; + if (gps != nullptr) + ++gps->force_full_display_count; + out.print("smooth-movement: sprite flipping enabled\n"); + return CR_OK; + } + if (parameters.size() == 2 && parameters[1] == "off") { + flip_enabled = false; + if (gps != nullptr) + ++gps->force_full_display_count; + out.print("smooth-movement: sprite flipping disabled\n"); + return CR_OK; + } + return CR_WRONG_USAGE; + } + if (parameters[0] == "zlevel") { + if (parameters.size() == 1) { + out.print("lower z-level animation: {}\n", zlevel_enabled ? "on" : "off"); + return CR_OK; + } + if (parameters.size() == 2 && parameters[1] == "on") + zlevel_enabled = true; + else if (parameters.size() == 2 && parameters[1] == "off") + zlevel_enabled = false; + else + return CR_WRONG_USAGE; + if (gps != nullptr) + ++gps->force_full_display_count; + out.print("smooth-movement: lower z-level animation {}\n", + zlevel_enabled ? "enabled" : "disabled"); + return CR_OK; + } + return CR_WRONG_USAGE; +} + +} // namespace + +DFhackCExport command_result plugin_init(color_ostream &, std::vector &commands) { + commands.emplace_back("smooth-movement", "Smoothly animate movement in the fortress viewport.", + status_command); + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (is_enabled == enable) + return CR_OK; + if (enable) { + reset_state(); + if (!load_sdl(out)) + return CR_FAILURE; + if (!INTERPOSE_HOOK(renderer_hook, update_all).apply()) { + out.printerr("smooth-movement: could not hook the 2D renderer\n"); + clear_sdl_bindings(); + return CR_FAILURE; + } + } else { + INTERPOSE_HOOK(renderer_hook, update_all).remove(); + reset_state(); + clear_sdl_bindings(); + if (gps != nullptr) + ++gps->force_full_display_count; + } + is_enabled = enable; + out.print("smooth-movement: {}\n", enable ? "enabled" : "disabled"); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + return plugin_enable(out, false); +} diff --git a/plugins/smooth-movement/test_visual_animation_manager.cpp b/plugins/smooth-movement/test_visual_animation_manager.cpp new file mode 100644 index 0000000000..53dd7b22b2 --- /dev/null +++ b/plugins/smooth-movement/test_visual_animation_manager.cpp @@ -0,0 +1,820 @@ +// SPDX-License-Identifier: Zlib + +#ifdef NDEBUG +#undef NDEBUG +#endif + +#include +#include +#include +#include + +#include "visual_animation.h" + +namespace { + +template +struct test_grid { + static constexpr int32_t dimension = Dimension; + + std::array tiles{}; + + int32_t &at(int32_t x, int32_t y) { return tiles[x * dimension + y]; } + int32_t &operator[](size_t index) { return tiles[index]; } + const int32_t *data() const { return tiles.data(); } + void fill(int32_t value) { tiles.fill(value); } +}; + +viewport_visual_animation_input make_input(const void *viewport, int32_t dimension, + const int32_t *empty) { + viewport_visual_animation_input input; + input.viewport = static_cast(viewport); + input.dimensions = df::coord2d(dimension, dimension); + input.context_revision = 1; + input.current.fill(empty); + input.previous.fill(empty); + return input; +} + +template +viewport_visual_animation_input make_input(const void *viewport, + const test_grid &empty) { + return make_input(viewport, Dimension, empty.data()); +} + +void set_layer(viewport_visual_animation_input &input, viewport_visual_layer layer, + const int32_t *current, const int32_t *previous) { + const size_t index = static_cast(layer); + input.current[index] = current; + input.previous[index] = previous; +} + +template +void set_layer(viewport_visual_animation_input &input, viewport_visual_layer layer, + const test_grid ¤t, const test_grid &previous) { + set_layer(input, layer, current.data(), previous.data()); +} + +template +bool moved_between_tiles(viewport_visual_layer layer, const test_grid ¤t, + const test_grid &previous, df::coord2d source, + df::coord2d target) { + return visual_moved_between_tiles(layer, current.data(), previous.data(), + source.x * Dimension + source.y, + target.x * Dimension + target.y); +} + +template +void clear_layers(viewport_visual_animation_input &input, const test_grid &empty) { + input.current.fill(empty.data()); + input.previous.fill(empty.data()); +} + +void run_frame(visual_animation_manager &manager, const viewport_visual_animation_input &input, + uint32_t now_ms) { + manager.begin_frame(now_ms); + manager.synchronize_viewport(input); + manager.end_frame(); +} + +} // namespace + +int main() { + visual_animation_manager manager; + manager.begin_frame(1000); + assert(manager.get_frame_time_ms() == 1000); + assert(manager.get_frame_delta_ms() == 0); + + manager.begin_frame(1020); + assert(manager.get_frame_time_ms() == 1020); + assert(manager.get_frame_delta_ms() == 20); + + manager.begin_frame(1020); + assert(manager.get_frame_delta_ms() == 0); + + visual_animation_manager rollover; + rollover.begin_frame(std::numeric_limits::max() - 5); + rollover.begin_frame(3); + assert(rollover.get_frame_delta_ms() == 9); + // Facing rule: only a horizontal component changes facing. + assert(facing_after_move(1, visual_facing::west) == visual_facing::east); + assert(facing_after_move(-1, visual_facing::east) == visual_facing::west); + // dy is not an input, so a diagonal is only ever the sign of dx. + // The four real diagonals run end to end through the manager further down. + assert(facing_after_move(1, visual_facing::east) == visual_facing::east); + assert(facing_after_move(-1, visual_facing::west) == visual_facing::west); + // Pure vertical and idle carry the previous facing (sticky). + assert(facing_after_move(0, visual_facing::west) == visual_facing::west); + assert(facing_after_move(0, visual_facing::east) == visual_facing::east); + + constexpr int32_t mirror_anchor_x = 5; + constexpr int32_t mirror_right_x = mirror_anchor_x + 1; + constexpr int32_t mirror_left_x = mirror_anchor_x - 1; + assert(mirrored_tile_x(mirror_anchor_x, mirror_anchor_x) == mirror_anchor_x); + assert(mirrored_tile_x(mirror_right_x, mirror_anchor_x) == mirror_left_x); + assert(mirrored_tile_x(mirror_left_x, mirror_anchor_x) == mirror_right_x); + // The formula is a general reflection, so it holds for offsets no layer can + // express. + assert(mirrored_tile_x(mirror_anchor_x + 3, mirror_anchor_x) == mirror_anchor_x - 3); + // The horizontal anchor offset bounds the real mirror shift to -2, 0 or +2. + for (const auto &descriptor : visual_layer_descriptors) + assert(descriptor.anchor_offset.x >= -1 && descriptor.anchor_offset.x <= 1); + // Reflection is self-inverse. + assert(mirrored_tile_x(mirrored_tile_x(mirror_right_x, mirror_anchor_x), mirror_anchor_x) == + mirror_right_x); + assert(mirrored_tile_x(mirrored_tile_x(mirror_anchor_x + 3, mirror_anchor_x), + mirror_anchor_x) == mirror_anchor_x + 3); + + { + constexpr int32_t dim = 4; + constexpr int32_t creature_texpos = 77; + constexpr uint32_t initial_frame_ms = 1000; + constexpr uint32_t frame_interval_ms = 16; + int32_t empty[dim * dim] = {}; + int32_t before[dim * dim] = {}; + int32_t west_after[dim * dim] = {}; + const int viewport_token{}; + const void *viewport = &viewport_token; + + before[2 * dim + 2] = creature_texpos; + west_after[1 * dim + 2] = creature_texpos; // moved west: x 2 -> 1 + + // Moving west sets west facing on the target tile. + { + visual_animation_manager manager; + auto input = make_input(viewport, dim, empty); + set_layer(input, viewport_visual_layer::center, before, empty); + run_frame(manager, input, initial_frame_ms); + assert(!manager.has_active_movement(viewport)); + set_layer(input, viewport_visual_layer::center, west_after, before); + run_frame(manager, input, initial_frame_ms + frame_interval_ms); + assert(manager.has_active_movement(viewport)); + assert(manager.get_facing(viewport, 1, 2) == visual_facing::west); + } + + // Moving east sets east facing. + // East is neither the grid default nor the source facing, so the assertion + // is not vacuous. + { + visual_animation_manager manager; + auto input = make_input(viewport, dim, empty); + set_layer(input, viewport_visual_layer::center, before, empty); + run_frame(manager, input, initial_frame_ms); + set_layer(input, viewport_visual_layer::center, west_after, before); + run_frame(manager, input, initial_frame_ms + frame_interval_ms); + assert(manager.get_facing(viewport, 1, 2) == visual_facing::west); + set_layer(input, viewport_visual_layer::center, before, west_after); + run_frame(manager, input, initial_frame_ms + 2 * frame_interval_ms); + assert(manager.get_facing(viewport, 2, 2) == visual_facing::east); + } + + // The mirrored flag gates the render path's early return. + // It must rise only for a genuinely mirrored creature and fall when that + // tile empties. + { + visual_animation_manager manager; + auto input = make_input(viewport, dim, empty); + set_layer(input, viewport_visual_layer::center, before, empty); + run_frame(manager, input, initial_frame_ms); + assert(!manager.has_mirrored_facing(viewport)); + // Moving west matches the art, so nothing is mirrored yet. + set_layer(input, viewport_visual_layer::center, west_after, before); + run_frame(manager, input, initial_frame_ms + frame_interval_ms); + assert(manager.get_facing(viewport, 1, 2) == visual_facing::west); + assert(!manager.has_mirrored_facing(viewport)); + // Moving east faces away from the art and raises the flag. + set_layer(input, viewport_visual_layer::center, before, west_after); + run_frame(manager, input, initial_frame_ms + 2 * frame_interval_ms); + assert(manager.get_facing(viewport, 2, 2) == visual_facing::east); + assert(manager.has_mirrored_facing(viewport)); + // The creature leaves: the tile clears and so does the flag. + set_layer(input, viewport_visual_layer::center, empty, before); + run_frame(manager, input, initial_frame_ms + 3 * frame_interval_ms); + assert(manager.get_facing(viewport, 2, 2) == native_sprite_facing); + assert(!manager.has_mirrored_facing(viewport)); + assert(!manager.has_mirrored_facing(nullptr)); + } + + // Pure vertical movement carries the existing facing to the new tile. + { + visual_animation_manager manager; + auto input = make_input(viewport, dim, empty); + set_layer(input, viewport_visual_layer::center, before, empty); + run_frame(manager, input, initial_frame_ms); + set_layer(input, viewport_visual_layer::center, west_after, before); + run_frame(manager, input, initial_frame_ms + frame_interval_ms); + assert(manager.get_facing(viewport, 1, 2) == visual_facing::west); + int32_t up[dim * dim] = {}; + up[1 * dim + 1] = creature_texpos; // north: (1,2) -> (1,1), no horizontal component + set_layer(input, viewport_visual_layer::center, up, west_after); + run_frame(manager, input, initial_frame_ms + 2 * frame_interval_ms); + assert(manager.get_facing(viewport, 1, 1) == visual_facing::west); + } + + // Out-of-range and unknown viewports fall back to the native facing. + { + visual_animation_manager manager; + auto input = make_input(viewport, dim, empty); + set_layer(input, viewport_visual_layer::center, before, empty); + run_frame(manager, input, initial_frame_ms); + assert(manager.get_facing(viewport, -1, 0) == native_sprite_facing); + assert(manager.get_facing(viewport, dim, 0) == native_sprite_facing); + assert(manager.get_facing(nullptr, 0, 0) == native_sprite_facing); + } + + // Each rendered z-level has its own viewport buffers; tracking one must not + // suppress another. + { + const int lower_token = 0; + const int main_token = 0; + const void *lower_viewport = &lower_token; + const void *main_viewport = &main_token; + visual_animation_manager z_levels; + auto lower_input = make_input(lower_viewport, dim, empty); + auto main_input = make_input(main_viewport, dim, empty); + set_layer(lower_input, viewport_visual_layer::center, before, empty); + set_layer(main_input, viewport_visual_layer::center, before, empty); + z_levels.begin_frame(initial_frame_ms); + z_levels.synchronize_viewport(lower_input); + z_levels.synchronize_viewport(main_input); + z_levels.end_frame(); + set_layer(lower_input, viewport_visual_layer::center, west_after, before); + set_layer(main_input, viewport_visual_layer::center, west_after, before); + z_levels.begin_frame(initial_frame_ms + frame_interval_ms); + z_levels.synchronize_viewport(lower_input); + z_levels.synchronize_viewport(main_input); + z_levels.end_frame(); + assert( + z_levels.get_movement(lower_viewport, viewport_visual_layer::center, 1, 2).active); + assert( + z_levels.get_movement(main_viewport, viewport_visual_layer::center, 1, 2).active); + } + } + + // All four diagonals through the manager, so every step carries a real dy as + // well as a dx. The chain alternates direction, so no assertion can pass by + // inheriting the previous facing. + { + constexpr int32_t diag_dim = 5; + constexpr int32_t creature_texpos = 77; + constexpr uint32_t initial_frame_ms = 1000; + constexpr uint32_t frame_interval_ms = 16; + int32_t diag_empty[diag_dim * diag_dim] = {}; + int32_t start[diag_dim * diag_dim] = {}; + int32_t north_east[diag_dim * diag_dim] = {}; + int32_t south_west[diag_dim * diag_dim] = {}; + int32_t south_east[diag_dim * diag_dim] = {}; + int32_t north_west[diag_dim * diag_dim] = {}; + const int diag_token = 0; + const void *diag_viewport = &diag_token; + + start[2 * diag_dim + 2] = creature_texpos; // (2,2) + north_east[3 * diag_dim + 1] = creature_texpos; // (2,2) -> (3,1): dx +1, dy -1 + south_west[2 * diag_dim + 2] = creature_texpos; // (3,1) -> (2,2): dx -1, dy +1 + south_east[3 * diag_dim + 3] = creature_texpos; // (2,2) -> (3,3): dx +1, dy +1 + north_west[2 * diag_dim + 2] = creature_texpos; // (3,3) -> (2,2): dx -1, dy -1 + + visual_animation_manager diagonal; + auto input = make_input(diag_viewport, diag_dim, diag_empty); + set_layer(input, viewport_visual_layer::center, start, diag_empty); + run_frame(diagonal, input, initial_frame_ms); + assert(diagonal.get_facing(diag_viewport, 2, 2) == native_sprite_facing); + + set_layer(input, viewport_visual_layer::center, north_east, start); + run_frame(diagonal, input, initial_frame_ms + frame_interval_ms); + assert(diagonal.get_facing(diag_viewport, 3, 1) == visual_facing::east); + + // West is the grid default, so the westward legs also assert a movement was + // registered. Otherwise an untracked step leaving the tile at its default + // would pass. + set_layer(input, viewport_visual_layer::center, south_west, north_east); + run_frame(diagonal, input, initial_frame_ms + 2 * frame_interval_ms); + assert(diagonal.get_movement(diag_viewport, viewport_visual_layer::center, 2, 2).active); + assert(diagonal.get_facing(diag_viewport, 2, 2) == visual_facing::west); + + set_layer(input, viewport_visual_layer::center, south_east, south_west); + run_frame(diagonal, input, initial_frame_ms + 3 * frame_interval_ms); + assert(diagonal.get_facing(diag_viewport, 3, 3) == visual_facing::east); + + set_layer(input, viewport_visual_layer::center, north_west, south_east); + run_frame(diagonal, input, initial_frame_ms + 4 * frame_interval_ms); + assert(diagonal.get_movement(diag_viewport, viewport_visual_layer::center, 2, 2).active); + assert(diagonal.get_facing(diag_viewport, 2, 2) == visual_facing::west); + } + + // Facing is keyed by SCREEN tile, so a scroll moves the creatures out from + // under it. The two outcomes differ fundamentally: a landed shift has a known + // delta and is translated. An abandoned shift never identifies a delta, so + // the grid can only be dropped. + { + using pan_grid = test_grid<4>; + constexpr int32_t creature_texpos = 77; + constexpr uint32_t frame_interval_ms = 16; + pan_grid pan_empty; + pan_grid at_one; + pan_grid at_two; + pan_grid unmatched_a; + pan_grid unmatched_b; + const int pan_token = 0; + const void *pan_viewport = &pan_token; + + at_one.at(1, 1) = creature_texpos; + at_two.at(2, 1) = creature_texpos; // steps east, x 1 -> 2, so it faces east + unmatched_a.at(2, 1) = 78; + unmatched_b.at(2, 1) = 79; + + // LANDED: the shift is recognized, so facing follows the buffers. + { + visual_animation_manager landed; + auto input = make_input(pan_viewport, pan_empty); + set_layer(input, viewport_visual_layer::center, at_one, pan_empty); + run_frame(landed, input, 1000); + set_layer(input, viewport_visual_layer::center, at_two, at_one); + run_frame(landed, input, 1000 + frame_interval_ms); + assert(landed.get_facing(pan_viewport, 2, 1) == visual_facing::east); + assert(landed.has_mirrored_facing(pan_viewport)); + + // Frame A: the scroll is announced but the buffers have not moved yet. + input.pan = df::coord2d(1, 0); + set_layer(input, viewport_visual_layer::center, at_two, at_two); + run_frame(landed, input, 1000 + 2 * frame_interval_ms); + assert(landed.get_facing(pan_viewport, 2, 1) == visual_facing::east); + + // Frame B: the buffers shift east by one and the majority-match test + // recognizes it. + set_layer(input, viewport_visual_layer::center, at_one, at_two); + run_frame(landed, input, 1000 + 3 * frame_interval_ms); + assert(landed.get_facing(pan_viewport, 1, 1) == visual_facing::east); + assert(landed.get_facing(pan_viewport, 2, 1) == native_sprite_facing); + assert(landed.has_mirrored_facing(pan_viewport)); + } + + // ABANDONED: the shift never shows up in the buffers, so no delta is ever + // identified. The grid must be back at the default while the tile is still + // OCCUPIED. The empty-tile sweep cannot reach that case, so only an + // explicit reset clears it. + { + visual_animation_manager abandoned; + auto input = make_input(pan_viewport, pan_empty); + set_layer(input, viewport_visual_layer::center, at_one, pan_empty); + run_frame(abandoned, input, 2000); + set_layer(input, viewport_visual_layer::center, at_two, at_one); + run_frame(abandoned, input, 2000 + frame_interval_ms); + assert(abandoned.get_facing(pan_viewport, 2, 1) == visual_facing::east); + + // Changed buffers keep the failed majority-match test running every + // frame. It tolerates four before giving up on the fifth. + input.pan = df::coord2d(1, 0); + constexpr int32_t pending_frame_count = 4; + for (int32_t frame = 0; frame < pending_frame_count; ++frame) { + set_layer(input, viewport_visual_layer::center, at_two, + frame % 2 == 0 ? unmatched_a : unmatched_b); + run_frame(abandoned, input, 2000 + uint32_t(frame + 2) * frame_interval_ms); + // Still pending, so the facing survives. + // The assertion after the giving-up frame therefore tests the reset, + // not an empty grid. + assert(abandoned.get_facing(pan_viewport, 2, 1) == visual_facing::east); + assert(abandoned.has_mirrored_facing(pan_viewport)); + } + set_layer(input, viewport_visual_layer::center, at_two, unmatched_a); + run_frame(abandoned, input, + 2000 + uint32_t(pending_frame_count + 2) * frame_interval_ms); + assert(abandoned.get_facing(pan_viewport, 2, 1) == native_sprite_facing); + assert(!abandoned.has_mirrored_facing(pan_viewport)); + } + } + + // Regression: A and B move in the same frame, chained -- A's target tile is + // B's source tile. A's write must not corrupt B's read of its own pre-frame + // facing. A is sent east first, so its facing differs from the default B + // carries and a swap shows. + { + constexpr int32_t chase_dim = 5; + constexpr int32_t creature_texpos = 77; + constexpr int32_t companion_texpos = 88; + int32_t chase_empty[chase_dim * chase_dim] = {}; + int32_t frame_a[chase_dim * chase_dim] = {}; + int32_t frame_b[chase_dim * chase_dim] = {}; + int32_t frame_c[chase_dim * chase_dim] = {}; + const int chase_token = 0; + const void *chase_viewport = &chase_token; + + frame_a[1 * chase_dim + 1] = creature_texpos; // A at (1,1) + frame_a[2 * chase_dim + 2] = companion_texpos; // B at (2,2), stationary + frame_b[2 * chase_dim + 1] = creature_texpos; // A steps east: (1,1) -> (2,1) + frame_b[2 * chase_dim + 2] = companion_texpos; // B unchanged + + // Both step south in the same frame: shared_movement_delta needs two tiles + // on the same delta. + frame_c[2 * chase_dim + 2] = creature_texpos; // A: (2,1) -> (2,2) + frame_c[2 * chase_dim + 3] = companion_texpos; // B: (2,2) -> (2,3) + + visual_animation_manager manager; + auto input = make_input(chase_viewport, chase_dim, chase_empty); + set_layer(input, viewport_visual_layer::center, frame_a, chase_empty); + run_frame(manager, input, 1000); + set_layer(input, viewport_visual_layer::center, frame_b, frame_a); + run_frame(manager, input, 1016); + assert(manager.get_facing(chase_viewport, 2, 1) == visual_facing::east); + assert(manager.get_facing(chase_viewport, 2, 2) == native_sprite_facing); + + set_layer(input, viewport_visual_layer::center, frame_c, frame_b); + run_frame(manager, input, 1032); + assert(manager.get_facing(chase_viewport, 2, 2) == visual_facing::east); + // B carries its own default forward, not A's, though A wrote (2,2) earlier + // in the same pass. + assert(manager.get_facing(chase_viewport, 2, 3) == native_sprite_facing); + } + + // Regression: a vacated source tile is reoccupied the same frame by an + // UNTRACKED creature. Nothing targets that tile, so no target write clears + // it, and it is not empty either. Only an explicit, order-independent source + // clear restores the default there. + { + constexpr int32_t gap_dim = 5; + constexpr int32_t creature_texpos = 77; + constexpr int32_t companion_texpos = 88; + constexpr int32_t untracked_texpos = 55; + int32_t gap_empty[gap_dim * gap_dim] = {}; + int32_t frame_a[gap_dim * gap_dim] = {}; + int32_t frame_b[gap_dim * gap_dim] = {}; + int32_t frame_c[gap_dim * gap_dim] = {}; + const int gap_token = 0; + const void *gap_viewport = &gap_token; + + // E is a companion so that D's later departure shares a delta with E's own + // move. shared_movement_delta engages only once two tiles move on the same + // delta. + frame_a[1 * gap_dim + 1] = creature_texpos; // D at (1,1) + frame_a[2 * gap_dim + 2] = companion_texpos; // E at (2,2), stationary companion + frame_b[2 * gap_dim + 1] = + creature_texpos; // D steps east: (1,1) -> (2,1), facing differs from default + frame_b[2 * gap_dim + 2] = companion_texpos; // E unchanged + + // D and E both step south, chained, and F appears at D's just-vacated tile + // the same frame. previous[(2,1)] was occupied by D, so the empty-cell + // fallback cannot see F. No shared-delta source matches F's texpos either, + // so its arrival registers no movement. + frame_c[2 * gap_dim + 2] = creature_texpos; // D: (2,1) -> (2,2) + frame_c[2 * gap_dim + 3] = companion_texpos; // E: (2,2) -> (2,3) + frame_c[2 * gap_dim + 1] = untracked_texpos; // F appears at (2,1), untracked + + visual_animation_manager manager; + auto input = make_input(gap_viewport, gap_dim, gap_empty); + set_layer(input, viewport_visual_layer::center, frame_a, gap_empty); + run_frame(manager, input, 1000); + set_layer(input, viewport_visual_layer::center, frame_b, frame_a); + run_frame(manager, input, 1016); + assert(manager.get_facing(gap_viewport, 2, 1) == visual_facing::east); + + set_layer(input, viewport_visual_layer::center, frame_c, frame_b); + run_frame(manager, input, 1032); + assert(!manager.get_movement(gap_viewport, viewport_visual_layer::center, 2, 1).active); + // F must not inherit D's stale east facing, though the tile is occupied + // rather than empty. + assert(manager.get_facing(gap_viewport, 2, 1) == native_sprite_facing); + assert(manager.get_facing(gap_viewport, 2, 2) == visual_facing::east); + assert(manager.get_facing(gap_viewport, 2, 3) == native_sprite_facing); + } + + constexpr uint32_t animation_duration_ms = 100; + assert(animation_progress(animation_duration_ms, 0, animation_duration_ms) == 1.0f); + assert(inherited_visual_source_tile(0, 0, 1) == -1); + assert(inherited_visual_source_tile(2, 0, 1) == 1); + assert(visual_layer_descriptor(viewport_visual_layer::right).anchor_offset.x == -1); + assert(visual_layer_descriptor(viewport_visual_layer::left).anchor_offset.x == 1); + assert(visual_layer_descriptor(viewport_visual_layer::upright).anchor_offset.x == -1 && + visual_layer_descriptor(viewport_visual_layer::upright).anchor_offset.y == 1); + assert(visual_layer_descriptor(viewport_visual_layer::up).anchor_offset.x == 0 && + visual_layer_descriptor(viewport_visual_layer::up).anchor_offset.y == 1); + assert(visual_layer_descriptor(viewport_visual_layer::upleft).anchor_offset.x == 1 && + visual_layer_descriptor(viewport_visual_layer::upleft).anchor_offset.y == 1); + + // A multi-tile fragment follows its exact anchor in a crowded scene. + { + constexpr int32_t crowded_dimension = 5; + constexpr int32_t anchor_texpos = 11; + constexpr int32_t neighbor_texpos = 22; + const DFHack::Coord2d anchor_source(1, 2); + const DFHack::Coord2d anchor_target(2, 2); + const DFHack::Coord2d neighbor_source(3, 2); + const DFHack::Coord2d neighbor_target(3, 1); + constexpr uint32_t initial_time_ms = 1000; + constexpr uint32_t movement_time_ms = 1016; + test_grid empty; + test_grid before; + test_grid after; + before.at(anchor_source.x, anchor_source.y) = anchor_texpos; + before.at(neighbor_source.x, neighbor_source.y) = neighbor_texpos; + after.at(anchor_target.x, anchor_target.y) = anchor_texpos; + after.at(neighbor_target.x, neighbor_target.y) = neighbor_texpos; + const int viewport_token = 0; + const void *crowded_viewport = &viewport_token; + visual_animation_manager crowded; + auto crowded_input = make_input(crowded_viewport, empty); + set_layer(crowded_input, viewport_visual_layer::center, before, empty); + run_frame(crowded, crowded_input, initial_time_ms); + set_layer(crowded_input, viewport_visual_layer::center, after, before); + run_frame(crowded, crowded_input, movement_time_ms); + const auto anchor = crowded.get_movement(crowded_viewport, + viewport_visual_layer::center, + anchor_target.x, anchor_target.y); + const auto fragment_target = + anchor_target - DFHack::Coord2d( + visual_layer_descriptor(viewport_visual_layer::right).anchor_offset); + const auto fragment = crowded.get_movement(crowded_viewport, + viewport_visual_layer::right, + fragment_target.x, fragment_target.y); + assert(anchor.active && fragment.active && fragment.inherited); + assert(fragment.movement_id == anchor.movement_id); + assert(fragment.source == DFHack::Coord2d(anchor_target)); + assert(fragment.progress == anchor.progress); + } + + using small_grid = test_grid<3>; + constexpr int32_t creature_texpos = 42; + small_grid empty; + small_grid current; + small_grid previous; + const void *viewport = reinterpret_cast(uintptr_t(1)); + auto input = make_input(viewport, empty); + set_layer(input, viewport_visual_layer::center, current, previous); + + visual_animation_manager movement; + constexpr uint32_t movement_start_time_ms = 2000; + run_frame(movement, input, movement_start_time_ms - 10); + assert(!movement.requires_full_redraw()); + + previous.at(0, 1) = creature_texpos; + current.at(1, 1) = creature_texpos; + run_frame(movement, input, movement_start_time_ms); + auto render = movement.get_movement(viewport, viewport_visual_layer::center, 1, 1); + assert(render.active); + assert(render.source.x == 0 && render.source.y == 1); + assert(render.progress == 0.0f); + assert(movement.requires_full_redraw()); + + previous = current; + set_layer(input, viewport_visual_layer::center, current, previous); + run_frame(movement, input, movement_start_time_ms + animation_duration_ms / 2); + render = movement.get_movement(viewport, viewport_visual_layer::center, 1, 1); + assert(render.active); + assert(render.progress == 0.5f); + + run_frame(movement, input, movement_start_time_ms + animation_duration_ms); + assert(!movement.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + assert(movement.requires_full_redraw()); + + run_frame(movement, input, movement_start_time_ms + animation_duration_ms + 20); + assert(!movement.requires_full_redraw()); + + visual_animation_manager ambiguous; + run_frame(ambiguous, input, 2990); + previous.fill(0); + previous[0 * 3 + 1] = 42; + previous[1 * 3 + 0] = 42; + set_layer(input, viewport_visual_layer::center, current, previous); + run_frame(ambiguous, input, 3000); + assert(!ambiguous.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + + // A handler and led animal form an occupied chain: each enters the other's + // old space. + visual_animation_manager convoy; + current.fill(0); + previous.fill(0); + run_frame(convoy, input, 3490); + previous[0 * 3 + 1] = 41; + previous[1 * 3 + 1] = 42; + current[1 * 3 + 1] = 41; + current[2 * 3 + 1] = 42; + run_frame(convoy, input, 3500); + const auto animal = convoy.get_movement(viewport, viewport_visual_layer::center, 1, 1); + const auto handler = convoy.get_movement(viewport, viewport_visual_layer::center, 2, 1); + assert(animal.active && animal.source.x == 0 && animal.source.y == 1); + assert(handler.active && handler.source.x == 1 && handler.source.y == 1); + + // A multi-tile fragment can use its own movement or its mapped center tile as + // proof of ownership. + for (const auto layer : + {viewport_visual_layer::right, viewport_visual_layer::left, viewport_visual_layer::upright, + viewport_visual_layer::up, viewport_visual_layer::upleft}) { + current.fill(0); + previous.fill(0); + previous[0 * 3 + 1] = 50; + current[1 * 3 + 1] = 50; + assert(moved_between_tiles(layer, current, previous, {0, 1}, {1, 1})); + previous[1 * 3 + 1] = 50; + assert(!moved_between_tiles(layer, current, previous, {0, 1}, {1, 1})); + } + + visual_animation_manager context; + current.fill(0); + previous.fill(0); + run_frame(context, input, 4000); + previous[0 * 3 + 1] = 42; + current[1 * 3 + 1] = 42; + run_frame(context, input, 4010); + assert(context.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + + ++input.context_revision; + run_frame(context, input, 4020); + assert(!context.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + + // Camera-pan handling. window_x/window_y change at input time but the buffers + // shift on a later render frame, so the manager must (a) NOT create movements + // from the buffer shift itself (the floating-sprite bug), and (b) translate + // in-flight movements on the frame the shift lands. + small_grid pan_current; + small_grid pan_previous; + small_grid pan_empty; + auto pan_input = make_input(viewport, pan_empty); + set_layer(pan_input, viewport_visual_layer::center, pan_current, pan_previous); + + // FLOAT REGRESSION: a stationary creature, pan announced at frame A, buffers + // shift at frame B. Frame B's buffers look exactly like a real move + // ((1,1)->(0,1) with a unique source) — the manager must recognize it as the + // pending pan and create NO movement. + visual_animation_manager floaty; + pan_previous[1 * 3 + 1] = 42; + pan_current[1 * 3 + 1] = 42; + run_frame(floaty, pan_input, 4990); + pan_input.pan = df::coord2d(1, 0); // frame A: window scrolled, buffers unchanged + run_frame(floaty, pan_input, 5000); + assert(!floaty.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + pan_previous[1 * 3 + 1] = 42; // frame B: buffers apply the shift + pan_current.fill(0); + pan_current[0 * 3 + 1] = 42; + run_frame(floaty, pan_input, 5010); + assert(!floaty.get_movement(viewport, viewport_visual_layer::center, 0, 1).active); + + // FOLLOW: an in-flight movement survives the announce frame untouched and is + // translated on the frame the buffers shift, so the sprite tracks the + // scrolled world. + visual_animation_manager panner; + pan_current.fill(0); + pan_previous.fill(0); + pan_input.pan = df::coord2d(0, 0); + run_frame(panner, pan_input, 5990); + pan_previous.at(0, 1) = creature_texpos; // creature steps (0,1) -> (1,1) + pan_current.at(1, 1) = creature_texpos; + run_frame(panner, pan_input, 6000); + auto moved = panner.get_movement(viewport, viewport_visual_layer::center, 1, 1); + assert(moved.active && moved.source.x == 0 && moved.source.y == 1); + + pan_input.pan = df::coord2d(1, 0); // frame A: pan announced, buffers unchanged + pan_previous[0 * 3 + 1] = 0; + pan_previous[1 * 3 + 1] = 42; // previous now matches current (stationary at (1,1)) + run_frame(panner, pan_input, 6010); + moved = panner.get_movement(viewport, viewport_visual_layer::center, 1, 1); + assert(moved.active && moved.source.x == 0); // untouched: still anchored to the old frame + + pan_current.fill(0); // frame B: buffers shift east by one + pan_current[0 * 3 + 1] = 42; + run_frame(panner, pan_input, 6020); + assert(!panner.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + auto followed = panner.get_movement(viewport, viewport_visual_layer::center, 0, 1); + assert(followed.active && followed.source.x == -1 && followed.source.y == 1); + + // SAME-FRAME: pan announced and buffers shifted in the same call — translated + // immediately. + visual_animation_manager same_frame; + pan_current.fill(0); + pan_previous.fill(0); + pan_input.pan = df::coord2d(0, 0); + run_frame(same_frame, pan_input, 6990); + pan_previous[0 * 3 + 1] = 42; + pan_current[1 * 3 + 1] = 42; + run_frame(same_frame, pan_input, 7000); + assert(same_frame.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + pan_input.pan = df::coord2d(1, 0); + pan_previous = pan_current; + pan_current.fill(0); + pan_current[0 * 3 + 1] = 42; + run_frame(same_frame, pan_input, 7010); + followed = same_frame.get_movement(viewport, viewport_visual_layer::center, 0, 1); + assert(followed.active && followed.source.x == -1); + + // A change that is NOT a pure pan (context revision bump) still resets, even + // with in-flight work. + visual_animation_manager reset_on_zoom; + pan_current.fill(0); + pan_previous.fill(0); + pan_input.pan = df::coord2d(0, 0); + pan_input.context_revision = 1; + run_frame(reset_on_zoom, pan_input, 8000); + pan_previous[0 * 3 + 1] = 42; + pan_current[1 * 3 + 1] = 42; + run_frame(reset_on_zoom, pan_input, 8010); + assert(reset_on_zoom.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + pan_input.context_revision = 2; // e.g. zoom / z-level / resize + run_frame(reset_on_zoom, pan_input, 8020); + assert(!reset_on_zoom.get_movement(viewport, viewport_visual_layer::center, 1, 1).active); + + // Status fragments inherit nearby center motion even while their texture + // flashes. + small_grid status_current; + small_grid status_previous; + current.fill(0); + previous.fill(0); + input.context_revision = 1; + clear_layers(input, empty); + set_layer(input, viewport_visual_layer::center, current, previous); + set_layer(input, viewport_visual_layer::item, status_current, status_previous); + set_layer(input, viewport_visual_layer::designation, status_current, status_previous); + visual_animation_manager companion; + constexpr uint32_t companion_start_time_ms = 9000; + run_frame(companion, input, companion_start_time_ms - 10); + previous[0 * 3 + 1] = 42; + current[1 * 3 + 1] = 42; + status_previous[0 * 3 + 0] = 90; + status_current[1 * 3 + 0] = 91; + assert(moved_between_tiles(viewport_visual_layer::designation, status_current, status_previous, + {0, 0}, {1, 0})); + status_previous[0 * 3 + 0] = 0; + assert(moved_between_tiles(viewport_visual_layer::designation, status_current, status_previous, + {0, 0}, {1, 0})); + assert(!moved_between_tiles(viewport_visual_layer::item, status_current, status_previous, + {0, 0}, {1, 0})); + status_previous[1 * 3 + 0] = 80; + assert(!moved_between_tiles(viewport_visual_layer::designation, status_current, status_previous, + {0, 0}, {1, 0})); + status_previous[0 * 3 + 0] = 90; + status_previous[1 * 3 + 0] = 0; + run_frame(companion, input, companion_start_time_ms); + auto status = companion.get_movement(viewport, viewport_visual_layer::designation, 1, 0); + assert(status.active && !status.inherited && status.source.x == 0 && status.source.y == 0); + const auto carried_item = companion.get_movement(viewport, viewport_visual_layer::item, 1, 0); + assert(carried_item.active && carried_item.inherited); + previous = current; + status_current[1 * 3 + 0] = 92; + run_frame(companion, input, companion_start_time_ms + animation_duration_ms / 2); + status = companion.get_movement(viewport, viewport_visual_layer::designation, 1, 0); + assert(status.active && status.progress == 0.5f); + + // Divergent nearby creature movements make companion ownership ambiguous, so + // the overlay snaps. + current.fill(0); + previous.fill(0); + status_current.fill(0); + status_previous.fill(0); + visual_animation_manager crowd; + run_frame(crowd, input, 9990); + previous[0 * 3 + 0] = 41; + current[0 * 3 + 1] = 41; + previous[2 * 3 + 2] = 42; + current[2 * 3 + 1] = 42; + status_current[1 * 3 + 1] = 99; + run_frame(crowd, input, 10000); + assert(!crowd.get_movement(viewport, viewport_visual_layer::designation, 1, 1).active); + status_previous[1 * 3 + 0] = 90; + status_current[1 * 3 + 1] = 91; + run_frame(crowd, input, 10010); + status = crowd.get_movement(viewport, viewport_visual_layer::designation, 1, 1); + assert(status.active); + assert(!status.inherited); + assert(status.source.x == 1 && status.source.y == 0); + + // Wheelbarrows use the item layer and the same independent adjacent-movement + // detection. + current.fill(0); + previous.fill(0); + clear_layers(input, empty); + set_layer(input, viewport_visual_layer::item, current, previous); + visual_animation_manager item; + run_frame(item, input, 10990); + previous[0 * 3 + 1] = 77; + current[1 * 3 + 1] = 77; + run_frame(item, input, 11000); + const auto item_move = item.get_movement(viewport, viewport_visual_layer::item, 1, 1); + assert(item_move.active && item_move.source.x == 0 && item_move.source.y == 1); + + // Minecart graphics can change texpos while moving; vehicle identity is tile + // occupancy. + current.fill(0); + previous.fill(0); + clear_layers(input, empty); + set_layer(input, viewport_visual_layer::vehicle, current, previous); + visual_animation_manager vehicle; + constexpr int32_t vehicle_texpos_before = 77; + constexpr int32_t vehicle_texpos_during_move = 78; + constexpr int32_t vehicle_texpos_stationary = 79; + constexpr int32_t vehicle_texpos_after_move = 80; + constexpr uint32_t vehicle_start_time_ms = 12000; + run_frame(vehicle, input, vehicle_start_time_ms - 10); + previous.at(0, 1) = vehicle_texpos_before; + current.at(1, 1) = vehicle_texpos_during_move; + run_frame(vehicle, input, vehicle_start_time_ms); + assert(vehicle.get_movement(viewport, viewport_visual_layer::vehicle, 1, 1).active); + previous = current; + current.at(1, 1) = vehicle_texpos_stationary; + run_frame(vehicle, input, vehicle_start_time_ms + animation_duration_ms / 2); + const auto cart = vehicle.get_movement(viewport, viewport_visual_layer::vehicle, 1, 1); + assert(cart.active && cart.progress == 0.5f); + previous = current; + current.fill(0); + current.at(2, 1) = vehicle_texpos_after_move; + run_frame(vehicle, input, vehicle_start_time_ms + animation_duration_ms / 2 + 10); + const auto chained = vehicle.get_movement(viewport, viewport_visual_layer::vehicle, 2, 1); + assert(chained.active && chained.source.x > 0.0f && chained.source.x < 1.0f && + chained.progress == 0.0f); +} diff --git a/plugins/smooth-movement/visual_animation.h b/plugins/smooth-movement/visual_animation.h new file mode 100644 index 0000000000..53596d995f --- /dev/null +++ b/plugins/smooth-movement/visual_animation.h @@ -0,0 +1,894 @@ +// SPDX-License-Identifier: Zlib + +#ifndef SMOOTH_MOVEMENT_VISUAL_ANIMATION_H +#define SMOOTH_MOVEMENT_VISUAL_ANIMATION_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CoordTemplate.h" +#include "DataDefs.h" +#include "df/coord2d.h" + +namespace df { +struct graphic_viewportst; +} + +enum class viewport_visual_layer : uint8_t { + right, + center, + left, + upright, + up, + upleft, + vehicle, + item, + designation, + count +}; + +enum class visual_render_group : uint8_t { item, vehicle, main, upper, designation, count }; + +struct visual_layer_descriptor { + viewport_visual_layer layer; + visual_render_group render_group; + bool moves_independently; + bool matches_any_previous; + DFHack::Coord2d anchor_offset; +}; + +const std::array visual_layer_descriptors = { + visual_layer_descriptor{.layer = viewport_visual_layer::right, + .render_group = visual_render_group::main, + .moves_independently = false, + .matches_any_previous = false, + .anchor_offset = {-1, 0}}, + visual_layer_descriptor{.layer = viewport_visual_layer::center, + .render_group = visual_render_group::main, + .moves_independently = true, + .matches_any_previous = false, + .anchor_offset = {0, 0}}, + visual_layer_descriptor{.layer = viewport_visual_layer::left, + .render_group = visual_render_group::main, + .moves_independently = false, + .matches_any_previous = false, + .anchor_offset = {1, 0}}, + visual_layer_descriptor{.layer = viewport_visual_layer::upright, + .render_group = visual_render_group::upper, + .moves_independently = false, + .matches_any_previous = false, + .anchor_offset = {-1, 1}}, + visual_layer_descriptor{.layer = viewport_visual_layer::up, + .render_group = visual_render_group::upper, + .moves_independently = false, + .matches_any_previous = false, + .anchor_offset = {0, 1}}, + visual_layer_descriptor{.layer = viewport_visual_layer::upleft, + .render_group = visual_render_group::upper, + .moves_independently = false, + .matches_any_previous = false, + .anchor_offset = {1, 1}}, + visual_layer_descriptor{.layer = viewport_visual_layer::vehicle, + .render_group = visual_render_group::vehicle, + .moves_independently = true, + .matches_any_previous = true, + .anchor_offset = {0, 0}}, + visual_layer_descriptor{.layer = viewport_visual_layer::item, + .render_group = visual_render_group::item, + .moves_independently = true, + .matches_any_previous = false, + .anchor_offset = {0, 0}}, + visual_layer_descriptor{.layer = viewport_visual_layer::designation, + .render_group = visual_render_group::designation, + .moves_independently = false, + .matches_any_previous = true, + .anchor_offset = {0, 0}}}; + +constexpr std::array visual_layer_draw_order = { + viewport_visual_layer::center, viewport_visual_layer::item, + viewport_visual_layer::vehicle, viewport_visual_layer::right, + viewport_visual_layer::left, viewport_visual_layer::upright, + viewport_visual_layer::up, viewport_visual_layer::upleft, + viewport_visual_layer::designation}; + +constexpr bool valid_visual_layer_draw_order() { + uint16_t layers = 0; + for (const auto layer : visual_layer_draw_order) { + const uint16_t bit = uint16_t(1U << static_cast(layer)); + if (layers & bit) + return false; + layers |= bit; + } + return layers == uint16_t((1U << static_cast(viewport_visual_layer::count)) - 1); +} + +static_assert(valid_visual_layer_draw_order()); + +inline const visual_layer_descriptor &visual_layer_descriptor(viewport_visual_layer layer) { + return visual_layer_descriptors[static_cast(layer)]; +} + +inline visual_render_group get_visual_render_group(viewport_visual_layer layer) { + return visual_layer_descriptor(layer).render_group; +} + +inline bool visual_layer_moves_independently(viewport_visual_layer layer) { + return visual_layer_descriptor(layer).moves_independently; +} + +inline bool visual_layer_tracks_own_movement(viewport_visual_layer layer) { + const auto &descriptor = visual_layer_descriptor(layer); + return descriptor.moves_independently || + descriptor.render_group == visual_render_group::designation; +} + +inline bool visual_layer_matches(viewport_visual_layer layer, int32_t current, int32_t previous) { + return visual_layer_descriptor(layer).matches_any_previous ? previous != 0 + : previous == current; +} + +struct viewport_visual_animation_input { + const df::graphic_viewportst *viewport = nullptr; + df::coord2d dimensions = df::coord2d(0, 0); + uint64_t context_revision = 0; + std::array(viewport_visual_layer::count)> current{}; + std::array(viewport_visual_layer::count)> previous{}; + // Current map-scroll offset (window_x/window_y). A pure pan does not bump + // context_revision. Only a hint: it changes at input time, the buffers shift + // on a later render frame. + df::coord2d pan = df::coord2d(0, 0); + + bool valid() const { + if (viewport == nullptr || dimensions.x <= 0 || dimensions.y <= 0) + return false; + for (size_t layer = 0; layer < current.size(); ++layer) { + if (current[layer] == nullptr || previous[layer] == nullptr) + return false; + } + return true; + } +}; + +using visual_movement_id = uint64_t; +constexpr visual_movement_id no_visual_movement = 0; + +struct visual_movement_render { + bool active = false; + DFHack::Coord2d source{0.0f, 0.0f}; + float progress = 1.0f; + bool inherited = false; + visual_movement_id movement_id = no_visual_movement; +}; + +inline float animation_progress(uint32_t now_ms, uint32_t start_time_ms, uint32_t duration_ms) { + const float linear = std::min(1.0f, float(now_ms - start_time_ms) / duration_ms); + return linear * linear * (3.0f - 2.0f * linear); +} + +inline bool visual_moved_between_tiles(viewport_visual_layer layer, const int32_t *current, + const int32_t *previous, int32_t source, int32_t target) { + return previous[target] == 0 && current[source] == 0 && + (layer == viewport_visual_layer::designation || previous[source] != 0); +} + +inline int32_t inherited_visual_source_tile(int32_t overlay_target, float center_source, + float center_target) { + return overlay_target + int32_t(std::lround(center_source - center_target)); +} + +enum class visual_facing : int8_t { east = 0, west = 1 }; + +// DF creature art faces west, so only east needs flipping. Also the default and +// cleared value. +constexpr visual_facing native_sprite_facing = visual_facing::west; + +// Sticky facing: only a horizontal component changes it. +constexpr visual_facing facing_after_move(int32_t dx, visual_facing previous) { + if (dx > 0) + return visual_facing::east; + if (dx < 0) + return visual_facing::west; + return previous; +} + +// anchor_offset.x is only -1, 0 or +1, so a creature is at most three columns wide +// here. +constexpr int32_t mirrored_tile_x(int32_t piece_x, int32_t anchor_x) { + return anchor_x - (piece_x - anchor_x); +} + +class visual_animation_manager { + struct movement { + viewport_visual_layer layer; + int32_t texpos; + DFHack::Coord2d source; + df::coord2d target; + uint32_t start_time_ms; + visual_movement_id id; + }; + + struct viewport_animation { + const df::graphic_viewportst *viewport = nullptr; + df::coord2d dimensions = df::coord2d(0, 0); + uint64_t context_revision = 0; + bool has_context = false; + bool seen = false; + std::vector movements; + // One facing per tile, not per unit: the viewport exposes one creature + // texpos per tile. + std::vector facing; + // Stationary mirrored creatures are repainted every frame; this is the + // cheap pre-check. + bool has_mirrored = false; + df::coord2d pan = df::coord2d(0, 0); + bool has_pan = false; + // Window scrolls not yet observed in the buffers, oldest first. + // A signed total would cancel on a reversing drag while both shifts are + // still owed. + std::vector pending; + // Redraws no prefix has matched. + int32_t pending_frames = 0; + // Redraws spent waiting for the buffers to move at all. + int32_t pending_age = 0; + // Redraws left in which new-movement detection stays suppressed after + // scroll activity. + int32_t suppress_frames = 0; + // Buffer contents last seen, to recognize a repeat of them. + std::optional buffer_signature; + // Set while the previous buffer still belongs to a view that has been left + // behind. + bool previous_view_stale = false; + }; + + uint32_t frame_time_ms = 0; + uint32_t frame_delta_ms = 0; + bool has_frame = false; + bool force_full_redraw = false; + std::vector viewports; + visual_movement_id next_movement_id = 1; + + visual_movement_id allocate_movement_id() { + const auto id = next_movement_id++; + if (next_movement_id == no_visual_movement) + next_movement_id = 1; + return id; + } + + // Tuned visual transition length; this is not a DF timing contract. + static constexpr uint32_t movement_duration_ms = 100; + // Scrolling faster than detection keeps up: give up rather than test ever + // more prefixes. + static constexpr size_t max_pending_shifts = 8; + // Bounds the wait on a scroll that never lands, so suppression cannot stick + // forever. These are observed recovery limits, not viewport limits. + static constexpr int32_t max_pending_age_frames = 120; + // A majority of visible, position-bearing sprites must confirm a scroll. + static constexpr double min_scroll_match_ratio = 0.5; + // Wait for buffer settling before treating changes as creature movement. + static constexpr int32_t scroll_settle_redraws = 2; + // Give up when a queued scroll does not match this many redraws. + static constexpr int32_t max_unmatched_scroll_redraws = 4; + + static void clear_pending(viewport_animation &state) { + state.pending.clear(); + state.pending_frames = 0; + state.pending_age = 0; + } + + static void abandon_pending(viewport_animation &state) { + state.movements.clear(); + clear_pending(state); + } + + static void reset_facing(viewport_animation &state) { + std::fill(state.facing.begin(), state.facing.end(), int8_t(native_sprite_facing)); + state.has_mirrored = false; + } + + static void reset_tracking(viewport_animation &state) { + abandon_pending(state); + state.suppress_frames = 0; + } + + // Identifies the buffer contents this frame, to tell a redrawn viewport from + // a repeated one. + static uint64_t compute_buffer_signature(const viewport_visual_animation_input &input) { + // FNV-1a. Only ever compared against the previous frame's value, never + // stored. + constexpr uint64_t fnv_offset_basis = 0xcbf29ce484222325ULL; + constexpr uint64_t fnv_prime = 0x100000001b3ULL; + uint64_t hash = fnv_offset_basis; + const int32_t tile_count = input.dimensions.x * input.dimensions.y; + for (size_t layer = 0; layer < input.current.size(); ++layer) { + if (!visual_layer_tracks_own_movement(static_cast(layer))) + continue; + for (int32_t i = 0; i < tile_count; ++i) { + hash = (hash ^ uint64_t(uint32_t(input.current[layer][i]))) * fnv_prime; + hash = (hash ^ uint64_t(uint32_t(input.previous[layer][i]))) * fnv_prime; + } + } + return hash; + } + + // Fraction of tracked sprites consistent with a buffer shift: + // current[x]==previous[x+dwx]. Negative when there is nothing to compare. + static double shift_match_ratio(const viewport_visual_animation_input &input, int32_t dwx, + int32_t dwy) { + int32_t considered = 0; + int32_t matches = 0; + for (size_t layer = 0; layer < input.current.size(); ++layer) { + const auto id = static_cast(layer); + if (!visual_layer_tracks_own_movement(id)) + continue; + // A layer matching any non-zero previous carries no position, so it would + // vote for every hypothesis and carry an unapplied scroll over the bar. + if (visual_layer_descriptor(id).matches_any_previous) + continue; + const int32_t *current = input.current[layer]; + const int32_t *previous = input.previous[layer]; + for (int32_t x = 0; x < input.dimensions.x; ++x) { + const int32_t sx = x + dwx; + if (sx < 0 || sx >= input.dimensions.x) + continue; + for (int32_t y = 0; y < input.dimensions.y; ++y) { + const int32_t texpos = current[x * input.dimensions.y + y]; + if (texpos == 0) + continue; + const int32_t sy = y + dwy; + if (sy < 0 || sy >= input.dimensions.y) + continue; + ++considered; + if (visual_layer_matches(id, texpos, previous[sx * input.dimensions.y + sy])) + ++matches; + } + } + } + if (considered == 0) + return -1.0; + return double(matches) / double(considered); + } + + static df::coord2d shared_movement_delta(std::span current, + std::span previous, + int32_t dim_x, int32_t dim_y) { + df::coord2d best(0, 0); + int32_t best_count = 1; + bool ambiguous = false; + for (int32_t dx = -1; dx <= 1; ++dx) { + for (int32_t dy = -1; dy <= 1; ++dy) { + if (dx == 0 && dy == 0) + continue; + int32_t count = 0; + for (int32_t x = 0; x < dim_x; ++x) { + const int32_t source_x = x + dx; + if (source_x < 0 || source_x >= dim_x) + continue; + for (int32_t y = 0; y < dim_y; ++y) { + const int32_t source_y = y + dy; + if (source_y < 0 || source_y >= dim_y) + continue; + const int32_t target = x * dim_y + y; + const int32_t texpos = current[target]; + if (texpos != 0 && previous[target] != texpos && + previous[source_x * dim_y + source_y] == texpos) + ++count; + } + } + if (count > best_count) { + best_count = count; + best = df::coord2d(dx, dy); + ambiguous = false; + } else if (count == best_count && count > 1) + ambiguous = true; + } + } + return ambiguous ? df::coord2d(0, 0) : best; + } + + viewport_animation &get_viewport(const viewport_visual_animation_input &input) { + for (viewport_animation &state : viewports) { + if (state.viewport == input.viewport) + return state; + } + viewports.emplace_back(); + viewports.back().viewport = input.viewport; + return viewports.back(); + } + + float movement_progress(uint32_t start_time_ms) const { + return animation_progress(frame_time_ms, start_time_ms, movement_duration_ms); + } + + public: + visual_animation_manager() = default; + + void begin_frame(uint32_t now_ms) { + frame_delta_ms = has_frame ? now_ms - frame_time_ms : 0; + frame_time_ms = now_ms; + has_frame = true; + force_full_redraw = false; + // Keep one final full redraw when the last movement expires. + for (viewport_animation &state : viewports) { + state.seen = false; + if (!state.movements.empty()) + force_full_redraw = true; + } + } + + void synchronize_viewport(const viewport_visual_animation_input &input) { + if (input.viewport == nullptr) + return; + viewport_animation &state = get_viewport(input); + state.seen = true; + + if (!input.valid()) { + reset_tracking(state); + state.has_context = false; + state.has_mirrored = false; + return; + } + + // Only a replaced view leaves a previous buffer belonging somewhere else; a + // first sighting does not. + const bool view_switched = + state.has_context && + (state.context_revision != input.context_revision || state.dimensions != input.dimensions); + const bool context_changed = !state.has_context || view_switched; + // The scroll delta is queued here as a hint; the buffers are + // hypothesis-tested each frame to find where it lands. Detection stays + // suppressed until then: a shifted buffer makes every panned creature look + // like a real move. + if (state.has_pan && state.pan != input.pan) { + if (state.pending.size() >= max_pending_shifts) { + // Same give-up as the other two sites: the owed shifts are unknowable + // now. + abandon_pending(state); + reset_facing(state); + } + state.pending.emplace_back(input.pan - state.pan); + state.pending_frames = 0; + state.suppress_frames = scroll_settle_redraws; + } + state.context_revision = input.context_revision; + state.dimensions = input.dimensions; + state.has_context = true; + state.pan = input.pan; + state.has_pan = true; + if (context_changed) { + state.facing.assign(size_t(input.dimensions.x) * size_t(input.dimensions.y), + int8_t(native_sprite_facing)); + state.has_mirrored = false; + } + // This hook runs per frame; the viewport is recomputed only when it + // changes, and while paused hardly at all. Re-reading a landed scroll steps + // every sprite by a tile. + const uint64_t signature = compute_buffer_signature(input); + const bool buffers_advanced = + !state.buffer_signature || *state.buffer_signature != signature; + state.buffer_signature = signature; + + if (context_changed) { + // Skips the recompute sweep, so clear has_mirrored here or a stale true + // survives. + state.has_mirrored = false; + reset_tracking(state); + // window_z, zoom and resize change at input time; the buffers cross + // later. This reset covers only the input frame, not the crossing itself. + if (view_switched) + state.previous_view_stale = true; + return; + } + + // On the crossing frame `current` is the new view and `previous` the old + // one, so a sprite on each side, a tile apart, reads as one that moved + // between them. + const bool crossed_views = buffers_advanced && state.previous_view_stale; + if (buffers_advanced) + state.previous_view_stale = false; + // The new view is drawn at the current window, so a queued scroll is + // already in it. Left queued it would never match, and suppress everything + // until it aged out. + if (crossed_views) + clear_pending(state); + + bool translated = false; + df::coord2d landed_shift(0, 0); + if (buffers_advanced && !crossed_views && !state.pending.empty()) { + // Queued scrolls land in order and may coalesce, so each hypothesis is a + // prefix. Shortest first: over-retiring leaves owed shifts to be read as + // movement. + df::coord2d landed(0, 0); + size_t landed_count = 0; + bool any_data = false; + df::coord2d shift(0, 0); + for (size_t count = 1; count <= state.pending.size(); ++count) { + shift = shift + state.pending[count - 1]; + // A prefix netting to zero is indistinguishable from "nothing landed + // yet". Accepting it would retire shifts the buffers have still to + // apply. + if (shift == df::coord2d{0, 0}) + continue; + const double ratio = shift_match_ratio(input, shift.x, shift.y); + // Emptiness is per-prefix: a long one can push every sprite out of + // range while a shorter one still has something to say. + if (ratio < 0.0) + continue; + any_data = true; + if (ratio >= min_scroll_match_ratio) { + landed = shift; + landed_count = count; + break; + } + } + if (!any_data) { + // Nothing visible to anchor the test on: nothing to animate either. + abandon_pending(state); + reset_facing(state); + } else if (landed_count > 0) { + // Re-anchor in-flight movements and drop anything scrolled off-screen. + const int32_t dwx = landed.x; + const int32_t dwy = landed.y; + state.movements.erase( + std::remove_if(state.movements.begin(), state.movements.end(), + [&](movement &movement) { + movement.source.x -= dwx; + movement.source.y -= dwy; + movement.target.x -= dwx; + movement.target.y -= dwy; + return movement.target.x < 0 || + movement.target.x >= input.dimensions.x || + movement.target.y < 0 || + movement.target.y >= input.dimensions.y; + }), + state.movements.end()); + // Facing describes creatures still on screen, so translate it rather + // than drop it. + if (state.facing.size() == + size_t(input.dimensions.x) * size_t(input.dimensions.y)) { + std::vector shifted(state.facing.size(), int8_t(native_sprite_facing)); + for (int32_t x = 0; x < input.dimensions.x; ++x) { + const int32_t sx = x + dwx; + if (sx < 0 || sx >= input.dimensions.x) + continue; + for (int32_t y = 0; y < input.dimensions.y; ++y) { + const int32_t sy = y + dwy; + if (sy < 0 || sy >= input.dimensions.y) + continue; + shifted[x * input.dimensions.y + y] = + state.facing[sx * input.dimensions.y + sy]; + } + } + state.facing.swap(shifted); + } + state.pending.erase(state.pending.begin(), + state.pending.begin() + std::ptrdiff_t(landed_count)); + state.pending_frames = 0; + state.pending_age = 0; + // The scroll is accounted for; the settle window must not block the + // rebased pass. + state.suppress_frames = 0; + landed_shift = landed; + translated = true; + } else if (shift_match_ratio(input, 0, 0) >= min_scroll_match_ratio && + ++state.pending_age <= max_pending_age_frames) { + // The buffers have not moved yet, so the scroll is still in flight. + state.pending_frames = 0; + } else if (++state.pending_frames > max_unmatched_scroll_redraws) { + // The shift never showed up recognizably: fall back to the safe reset. + abandon_pending(state); + // The delta was never identified, so the grid cannot be translated. + reset_facing(state); + // It may yet land, so do not resume detection on the very next redraw. + state.suppress_frames = scroll_settle_redraws; + } + } + + const bool suppress = !buffers_advanced || crossed_views || !state.pending.empty() || + state.suppress_frames > 0; + // The countdown measures redraws, not frames, so a repeated viewport must + // not spend it. + if (buffers_advanced && state.suppress_frames > 0) + --state.suppress_frames; + + // On the landing frame `previous` is still framed on the pre-scroll view. + // Rebasing it by the landed delta keeps a creature that walked during the + // scroll. + auto previous_layers = input.previous; + std::vector> rebased_previous; + if (translated && !suppress) { + rebased_previous.resize(input.previous.size()); + for (size_t layer = 0; layer < input.previous.size(); ++layer) { + if (!visual_layer_tracks_own_movement(static_cast(layer))) + continue; + rebased_previous[layer].assign( + size_t(input.dimensions.x) * size_t(input.dimensions.y), 0); + for (int32_t x = 0; x < input.dimensions.x; ++x) { + const int32_t sx = x + landed_shift.x; + if (sx < 0 || sx >= input.dimensions.x) + continue; + for (int32_t y = 0; y < input.dimensions.y; ++y) { + const int32_t sy = y + landed_shift.y; + if (sy < 0 || sy >= input.dimensions.y) + continue; + rebased_previous[layer][x * input.dimensions.y + y] = + input.previous[layer][sx * input.dimensions.y + sy]; + } + } + previous_layers[layer] = rebased_previous[layer].data(); + } + } + if (!suppress) { + const int32_t tile_count = input.dimensions.x * input.dimensions.y; + std::vector claimed_sources(tile_count); + const size_t existing_movement_count = state.movements.size(); + // A chained movement's source may already have been rewritten this frame. + const std::vector facing_at_frame_start = state.facing; + // Source clears are deferred until every movement this frame is + // registered. A source can be another movement's target in the same frame + // -- a chain. + std::vector facing_target_written; + std::vector pending_facing_source_clears; + if (state.facing.size() == size_t(input.dimensions.x) * size_t(input.dimensions.y)) + facing_target_written.assign(state.facing.size(), 0); + for (size_t layer = 0; layer < input.current.size(); ++layer) { + if (!visual_layer_tracks_own_movement(static_cast(layer))) + continue; + std::fill(claimed_sources.begin(), claimed_sources.end(), 0); + const int32_t *current = input.current[layer]; + const int32_t *previous = previous_layers[layer]; + const auto shared_delta = + static_cast(layer) == viewport_visual_layer::center + ? shared_movement_delta( + std::span(current, size_t(tile_count)), + std::span(previous, size_t(tile_count)), input.dimensions.x, + input.dimensions.y) + : df::coord2d(0, 0); + for (int32_t x = 0; x < input.dimensions.x; ++x) { + for (int32_t y = 0; y < input.dimensions.y; ++y) { + const int32_t target = x * input.dimensions.y + y; + const int32_t texpos = current[target]; + if (texpos == 0) + continue; + if (static_cast(layer) == + viewport_visual_layer::item && + previous_layers[static_cast(viewport_visual_layer::center)] + [target] != 0) + continue; + + int32_t source = -1; + int32_t candidate_count = 0; + if (shared_delta.x != 0 || shared_delta.y != 0) { + const int32_t source_x = x + shared_delta.x; + const int32_t source_y = y + shared_delta.y; + if (source_x >= 0 && source_x < input.dimensions.x && source_y >= 0 && + source_y < input.dimensions.y) { + const int32_t candidate = source_x * input.dimensions.y + source_y; + if (!claimed_sources[candidate] && previous[target] != texpos && + previous[candidate] == texpos) { + source = candidate; + candidate_count = 1; + } + } + } + // Otherwise require a unique same-sprite move between empty cells. + if (candidate_count == 0 && previous[target] == 0) { + for (int32_t dx = -1; dx <= 1; ++dx) { + for (int32_t dy = -1; dy <= 1; ++dy) { + if (dx == 0 && dy == 0) + continue; + const int32_t source_x = x + dx; + const int32_t source_y = y + dy; + if (source_x < 0 || source_x >= input.dimensions.x || + source_y < 0 || source_y >= input.dimensions.y) + continue; + const int32_t candidate = + source_x * input.dimensions.y + source_y; + if (!claimed_sources[candidate] && + visual_layer_matches( + static_cast(layer), texpos, + previous[candidate]) && + current[candidate] == 0) { + source = candidate; + ++candidate_count; + } + } + } + } + if (candidate_count != 1) + continue; + + claimed_sources[source] = 1; + DFHack::Coord2d visual_source{ + float(source / input.dimensions.y), float(source % input.dimensions.y)}; + for (size_t i = 0; i < existing_movement_count; ++i) { + const movement &movement = state.movements[i]; + if (movement.layer != static_cast(layer) || + movement.target.x != visual_source.x || + movement.target.y != visual_source.y) + continue; + const float progress = movement_progress(movement.start_time_ms); + visual_source = movement.source.lerp( + {float(movement.target.x), float(movement.target.y)}, progress); + break; + } + state.movements.push_back({static_cast(layer), + texpos, + visual_source, + df::coord2d(x, y), + frame_time_ms, + allocate_movement_id()}); + if (static_cast(layer) == + viewport_visual_layer::center && + state.facing.size() == + size_t(input.dimensions.x) * size_t(input.dimensions.y) && + !facing_at_frame_start.empty() && + facing_at_frame_start.size() == state.facing.size()) { + const int32_t source_tile_x = source / input.dimensions.y; + const int32_t target_index = x * input.dimensions.y + y; + state.facing[target_index] = int8_t(facing_after_move( + x - source_tile_x, + static_cast(facing_at_frame_start[source]))); + facing_target_written[size_t(target_index)] = 1; + pending_facing_source_clears.push_back(source); + } + } + } + } + // A source vacates its tile only if no movement this frame claimed it as + // a target. + if (!facing_target_written.empty()) { + for (int32_t pending_source : pending_facing_source_clears) { + if (!facing_target_written[size_t(pending_source)]) + state.facing[size_t(pending_source)] = int8_t(native_sprite_facing); + } + } + } + state.movements.erase( + std::remove_if( + state.movements.begin(), state.movements.end(), + [&](const movement &movement) { + const size_t layer = static_cast(movement.layer); + const int32_t target = + movement.target.x * input.dimensions.y + movement.target.y; + const int32_t current = input.current[layer][target]; + return frame_time_ms - movement.start_time_ms >= movement_duration_ms || + current == 0 || + !visual_layer_matches(movement.layer, current, movement.texpos); + }), + state.movements.end()); + // has_mirrored is recomputed here rather than maintained at every write + // site. + if (state.facing.size() == size_t(input.dimensions.x) * size_t(input.dimensions.y)) { + const int32_t *center_current = + input.current[static_cast(viewport_visual_layer::center)]; + bool any_mirrored = false; + for (size_t i = 0; i < state.facing.size(); ++i) { + if (center_current[i] == 0) + state.facing[i] = int8_t(native_sprite_facing); + else if (state.facing[i] != int8_t(native_sprite_facing)) + any_mirrored = true; + } + state.has_mirrored = any_mirrored; + } + if (!state.movements.empty()) + force_full_redraw = true; + } + + void end_frame() { + viewports.erase( + std::remove_if(viewports.begin(), viewports.end(), + [](const viewport_animation &state) { return !state.seen; }), + viewports.end()); + for (const viewport_animation &state : viewports) { + if (!state.movements.empty()) + force_full_redraw = true; + } + } + + uint32_t get_frame_time_ms() const { return frame_time_ms; } + + uint32_t get_frame_delta_ms() const { return frame_delta_ms; } + + visual_facing get_facing(const void *viewport, df::coord2d pos) const { + for (const viewport_animation &state : viewports) { + if (state.viewport != viewport) + continue; + if (pos.x < 0 || pos.x >= state.dimensions.x || pos.y < 0 || + pos.y >= state.dimensions.y) + break; + const size_t index = size_t(pos.x) * size_t(state.dimensions.y) + size_t(pos.y); + if (index >= state.facing.size()) + break; + return static_cast(state.facing[index]); + } + return native_sprite_facing; + } + + visual_facing get_facing(const void *viewport, int32_t x, int32_t y) const { + return get_facing(viewport, df::coord2d(x, y)); + } + + bool has_mirrored_facing(const void *viewport) const { + for (const viewport_animation &state : viewports) { + if (state.viewport != viewport) + continue; + return state.has_mirrored; + } + return false; + } + + bool requires_full_redraw() const { return force_full_redraw; } + + bool has_active_movement(const void *viewport) const { + for (const viewport_animation &state : viewports) { + if (state.viewport == viewport) + return !state.movements.empty(); + } + return false; + } + + visual_movement_render get_movement(const void *viewport, viewport_visual_layer layer, + int32_t target_x, int32_t target_y) const { + for (const viewport_animation &state : viewports) { + if (state.viewport != viewport) + continue; + const movement *companion = nullptr; + bool ambiguous = false; + const auto &descriptor = visual_layer_descriptor(layer); + const DFHack::Coord2d target(target_x, target_y); + const bool creature_fragment = descriptor.render_group == visual_render_group::main || + descriptor.render_group == visual_render_group::upper; + for (const movement &movement : state.movements) { + if (movement.layer == layer && movement.target.x == target_x && + movement.target.y == target_y) { + return {true, movement.source, movement_progress(movement.start_time_ms), false, + movement.id}; + } + if (layer == viewport_visual_layer::vehicle || + layer == viewport_visual_layer::center || + movement.layer != viewport_visual_layer::center) + continue; + if (creature_fragment) { + const DFHack::Coord2d movement_target(movement.target.x, + movement.target.y); + if (movement_target == + target + DFHack::Coord2d(descriptor.anchor_offset)) { + companion = &movement; + break; + } + continue; + } + if (std::abs(movement.target.x - target_x) > 1 || + std::abs(movement.target.y - target_y) > 1) + continue; + if (companion != nullptr && (companion->source.x - companion->target.x != + movement.source.x - movement.target.x || + companion->source.y - companion->target.y != + movement.source.y - movement.target.y || + companion->start_time_ms != movement.start_time_ms)) + ambiguous = true; + else if (companion == nullptr) + companion = &movement; + } + if (ambiguous) + return {}; + if (companion != nullptr) + return {true, + {target_x + companion->source.x - companion->target.x, + target_y + companion->source.y - companion->target.y}, + movement_progress(companion->start_time_ms), + true, + companion->id}; + break; + } + return {}; + } +}; + +#endif // SMOOTH_MOVEMENT_VISUAL_ANIMATION_H diff --git a/plugins/sort.cpp b/plugins/sort.cpp index add54688c4..f1b700c5af 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,3 +1,6 @@ +#include +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -11,6 +14,7 @@ #include "df/widget_unit_list.h" #include "df/world.h" + using std::vector; using std::string; @@ -25,8 +29,9 @@ namespace DFHack { DBG_DECLARE(sort, log, DebugCategory::LINFO); } -using item_or_unit = std::pair; -using filter_vec_type = std::vector>; +using item_or_unit = std::variant; +using filter_func = bool(item_or_unit); +using filter_vec_type = std::vector>; // recreated here since our autogenerated df::sort_entry lacks template params struct sort_entry { @@ -40,18 +45,9 @@ static const string DFHACK_SORT_IDENT = "dfhack_sort"; // filter logic // -static bool probing = false; -static bool probe_result = false; - static bool do_filter(const char *module_name, const char *fn_name, const item_or_unit &elem) { - if (elem.second) return true; - auto unit = (df::unit *)elem.first; - - if (probing) { - TRACE(log).print("probe successful\n"); - probe_result = true; - return false; - } + if (std::holds_alternative(elem)) return true; + auto unit = std::get(elem); bool ret = true; color_ostream &out = Core::getInstance().getConsole(); @@ -60,7 +56,7 @@ static bool do_filter(const char *module_name, const char *fn_name, const item_o ret = lua_toboolean(L, 1); } ); - TRACE(log).print("filter result for %s: %d\n", Units::getReadableName(unit).c_str(), ret); + TRACE(log).print("filter result for {}: {}\n", Units::getReadableName(unit), ret); return !ret; } @@ -76,35 +72,28 @@ static bool do_work_animal_assignment_filter(item_or_unit elem) { return do_filter("plugins.sort.info", "do_work_animal_assignment_filter", elem); } -static int32_t our_filter_idx(df::widget_unit_list *unitlist) { - if (world->units.active.empty()) - return true; - - df::unit *u = world->units.active[0]; // any unit will do; we just need a sentinel - if (!u) - return true; - - probing = true; - probe_result = false; +static int32_t our_filter_idx(filter_func* filter, df::widget_unit_list* unitlist) +{ int32_t idx = 0; - filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); + filter_vec_type* filter_vec = reinterpret_cast(&unitlist->filter_func); TRACE(log).print("probing for our filter function\n"); - for (auto fn : *filter_vec) { - fn(std::make_pair(u, false)); - if (probe_result) { - TRACE(log).print("found our filter function at idx %d\n", idx); - break; + + for (auto& fn : *filter_vec) + { + auto t = fn.target(); + if (t && *t == filter) + { + TRACE(log).print("found our filter function at idx {}\n", idx); + return idx; } ++idx; } - - probing = false; - return probe_result ? idx : -1; + return -1; } -static df::widget_unit_list * get_squad_unit_list() { +static df::widget_unit_list* get_squad_unit_list() { return virtual_cast( Gui::getWidget(&game->main_interface.unit_selector, "Unit selector")); } @@ -144,13 +133,13 @@ static df::widget_unit_list * get_work_animal_assignment_unit_list() { // static bool sort_proxy(const item_or_unit &a, const item_or_unit &b) { - if (a.second || b.second) + if (std::holds_alternative(a) || std::holds_alternative(b)) return true; bool ret = true; color_ostream &out = Core::getInstance().getConsole(); Lua::CallLuaModuleFunction(out, "plugins.sort", "do_sort", - std::make_tuple((df::unit *)a.first, (df::unit *)b.first), + std::make_tuple(std::get(a), std::get(b)), 1, [&](lua_State *L){ ret = lua_toboolean(L, 1); } @@ -180,10 +169,10 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector= 0) { - DEBUG(log,out).print("removing %s filter function\n", which); + DEBUG(log,out).print("removing {} filter function\n", which); filter_vec_type *filter_vec = reinterpret_cast(&unitlist->filter_func); vector_erase_at(*filter_vec, idx); } @@ -193,29 +182,29 @@ static void remove_sort_function(color_ostream &out, const char *which, df::widg std::vector *sorting_by = reinterpret_cast *>(&unitlist->sorting_by); int32_t idx = our_sort_idx(*sorting_by); if (idx >= 0) { - DEBUG(log).print("removing %s sort function\n", which); + DEBUG(log).print("removing {} sort function\n", which); vector_erase_at(*sorting_by, idx); } } DFhackCExport command_result plugin_shutdown(color_ostream &out) { if (auto unitlist = get_squad_unit_list()) { - remove_filter_function(out, "squad", unitlist); + remove_filter_function(out, do_squad_filter, "squad", unitlist); remove_sort_function(out, "squad", unitlist); } if (auto unitlist = get_interrogate_unit_list("Open cases")) - remove_filter_function(out, "open cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "open cases interrogate", unitlist); if (auto unitlist = get_interrogate_unit_list("Cold cases")) - remove_filter_function(out, "cold cases interrogate", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases interrogate", unitlist); if (auto unitlist = get_convict_unit_list("Open cases")) - remove_filter_function(out, "open cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "open cases convict", unitlist); if (auto unitlist = get_convict_unit_list("Cold cases")) - remove_filter_function(out, "cold cases convict", unitlist); + remove_filter_function(out, do_justice_filter, "cold cases convict", unitlist); if (auto unitlist = get_work_animal_assignment_unit_list()) - remove_filter_function(out, "work animal assignment", unitlist); + remove_filter_function(out, do_work_animal_assignment_filter, "work animal assignment", unitlist); return CR_OK; } @@ -226,7 +215,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { static void sort_set_squad_filter_fn(color_ostream &out) { auto unitlist = get_squad_unit_list(); - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_squad_filter, unitlist) == -1) { DEBUG(log).print("adding squad filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_squad_filter); @@ -238,7 +227,7 @@ static void sort_set_squad_filter_fn(color_ostream &out) { } static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_justice_filter, unitlist) == -1) { DEBUG(log).print("adding justice filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_justice_filter); @@ -247,7 +236,7 @@ static void sort_set_justice_filter_fn(color_ostream &out, df::widget_unit_list } static void sort_set_work_animal_assignment_filter_fn(color_ostream &out, df::widget_unit_list *unitlist) { - if (unitlist && our_filter_idx(unitlist) == -1) { + if (unitlist && our_filter_idx(do_work_animal_assignment_filter, unitlist) == -1) { DEBUG(log).print("adding work animal assignment filter function\n"); auto filter_vec = reinterpret_cast(&unitlist->filter_func); filter_vec->emplace_back(do_work_animal_assignment_filter); diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp new file mode 100644 index 0000000000..072ad813c7 --- /dev/null +++ b/plugins/spectate.cpp @@ -0,0 +1,627 @@ +#include "Debug.h" +#include "LuaTools.h" +#include "PluginLua.h" +#include "PluginManager.h" +#include "VTableInterpose.h" + +#include "modules/EventManager.h" +#include "modules/Gui.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include "df/announcements.h" +#include "df/d_init.h" +#include "df/plotinfost.h" +#include "df/unit.h" +#include "df/activity_entry.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/world.h" + +#include + +using namespace DFHack; + +using std::string; +using std::vector; + +DFHACK_PLUGIN("spectate"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(d_init); +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(spectate, control, DebugCategory::LINFO); + DBG_DECLARE(spectate, cycle, DebugCategory::LINFO); + DBG_DECLARE(spectate, event, DebugCategory::LINFO); +} + +static std::default_random_engine rng; +static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle + +static const size_t MAX_HISTORY = 200; + +static const float CITIZEN_COMBAT_PREFERRED_WEIGHT = 25.0f; +static const float NICKNAMED_CITIZEN_PREFERRED_WEIGHT = 15.0f; +static const float OTHER_COMBAT_PREFERRED_WEIGHT = 10.0f; +static const float JOB_WEIGHT = 5.0f; +static const float OTHER_WEIGHT = 1.0f; + +static const int32_t RECENT_UNITS_SCAN_CYCLE = 51; +static const float RECENT_UNIT_MULTIPLIER = 2.0f; // weight multiplier for recent units +static const int32_t RECENT_UNIT_MS = 15 * 60 * 1000; // 15 minutes + +// jobs that get "other" weight instad of "job" weight +static const std::unordered_set boring_jobs = { + df::job_type::Eat, + df::job_type::Drink, + df::job_type::Sleep, +}; + + +///////////////////////////////////////////////////// +// Configuration + +static struct Configuration { + bool auto_unpause; + bool cinematic_action; + bool include_animals; + bool include_hostiles; + bool include_visitors; + bool include_wildlife; + bool prefer_conflict; + bool prefer_new_arrivals; + bool prefer_nicknamed; + int32_t follow_ms; + + void reset() { + auto_unpause = false; + cinematic_action = true; + include_animals = false; + include_hostiles = false; + include_visitors = false; + include_wildlife = false; + prefer_conflict = true; + prefer_new_arrivals = true; + prefer_nicknamed = true; + follow_ms = 10000; + } +} config; + +///////////////////////////////////////////////////// +// AnnouncementSettings + +static class AnnouncementSettings { + bool was_in_settings = false; // whether we were in the vanilla settings screen last update + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + std::unique_ptr saved; + + void save_settings(color_ostream &out) { + if (!saved) + saved = std::make_unique(new uint32_t[announcement_flag_arr_size]); + DEBUG(control,out).print("saving announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + (*saved)[i] = d_init->announcements.flags[i].whole; + } + +public: + void reset(color_ostream &out, bool skip_restore) { + was_in_settings = false; + + if (saved) { + if (!skip_restore) + restore_settings(out); + delete[] *saved; + saved.reset(); + } + } + + void on_update(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + if (!was_in_settings) { + DEBUG(cycle,out).print("settings screen active; restoring announcement settings\n"); + restore_settings(out); + was_in_settings = true; + } + } else if (was_in_settings) { + was_in_settings = false; + if (config.auto_unpause) { + DEBUG(cycle,out).print("settings screen now inactive; disabling announcement pausing\n"); + save_and_scrub_settings(out); + } + } + } + + void restore_settings(color_ostream &out) { + if (!saved || was_in_settings) + return; + DEBUG(control,out).print("restoring saved announcement settings\n"); + for (size_t i = 0; i < announcement_flag_arr_size; ++i) + d_init->announcements.flags[i].whole = (*saved)[i]; + } + + // remove pausing, popups, and recentering from all announcements + // saves first so the original settings can be restored + void save_and_scrub_settings(color_ostream &out) { + if (Gui::matchFocusString("dwarfmode/Settings")) { + DEBUG(control,out).print("not modifying announcement settings; vanilla settings screen is active\n"); + return; + } + + save_settings(out); + + DEBUG(control,out).print("scrubbing announcement settings\n"); + for (auto& flag : d_init->announcements.flags) { + flag.bits.DO_MEGA = false; + flag.bits.PAUSE = false; + flag.bits.RECENTER = false; + } + } +} announcement_settings; + +///////////////////////////////////////////////////// +// UnitHistory + +static void follow_a_dwarf(color_ostream &out); + +static class UnitHistory { + std::deque history; + size_t offset = 0; + +public: + void reset() { + history.clear(); + offset = 0; + } + + void add_to_history(color_ostream &out, int32_t unit_id) { + if (offset > 0) { + DEBUG(cycle,out).print("trimming history forward of offset {}\n", offset); + history.resize(history.size() - offset); + offset = 0; + } + if (history.size() && history.back() == unit_id) { + DEBUG(cycle,out).print("unit {} is already current unit; not adding to history\n", unit_id); + } else { + history.push_back(unit_id); + if (history.size() > MAX_HISTORY) { + DEBUG(cycle,out).print("history full, truncating\n"); + history.pop_front(); + } + } + DEBUG(cycle,out).print("history now has {} entries\n", history.size()); + } + + void add_and_follow(color_ostream &out, df::unit *unit) { + // if we're currently following a unit, add it to the history if it's not already there + if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { + DEBUG(cycle,out).print("currently following unit {} that is not in history; adding\n", plotinfo->follow_unit); + add_to_history(out, plotinfo->follow_unit); + } + + int32_t id = unit->id; + add_to_history(out, id); + DEBUG(cycle,out).print("now following unit {}: {}\n", id, DF2CONSOLE(Units::getReadableName(unit))); + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; + plotinfo->follow_unit = id; + } + + void scan_back(color_ostream &out) { + if (history.empty() || offset >= history.size()-1) { + DEBUG(cycle,out).print("already at beginning of history\n"); + return; + } + ++offset; + int unit_id = get_cur_unit_id(); + DEBUG(cycle,out).print("scanning back to unit {} at offset {}\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; + plotinfo->follow_unit = unit_id; + } + + void scan_forward(color_ostream &out) { + if (history.empty() || offset == 0) { + DEBUG(cycle,out).print("already at most recent unit; following new unit\n"); + follow_a_dwarf(out); + return; + } + + --offset; + int unit_id = get_cur_unit_id(); + DEBUG(cycle,out).print("scanning forward to unit {} at offset {}\n", unit_id, offset); + if (auto unit = df::unit::find(unit_id)) + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; + plotinfo->follow_unit = unit_id; + } + + int32_t get_cur_unit_id() { + if (offset >= history.size()) + return -1; + return history[history.size() - (1 + offset)]; + } +} unit_history; + +///////////////////////////////////////////////////// +// RecentUnits + +static class RecentUnits { + std::unordered_map units; // unit id -> time seen + +public: + void reset() { + units.clear(); + } + + void add(int32_t unit_id) { + units[unit_id] = Core::getInstance().getUnpausedMs(); + } + + bool contains(int32_t unit_id) { + return units.contains(unit_id); + } + + void trim() { + uint32_t unpaused_ms = Core::getInstance().getUnpausedMs(); + if (unpaused_ms < RECENT_UNIT_MS) + return; + uint32_t cutoff = unpaused_ms - RECENT_UNIT_MS; + for (auto it = units.begin(); it != units.end();) { + if (it->second < cutoff) + it = units.erase(it); + else + ++it; + } + } +} recent_units; + +static void on_new_active_unit(color_ostream& out, void* data) { + int32_t unit_id = reinterpret_cast(data); + DEBUG(event,out).print("unit {} has arrived on map\n", unit_id); + recent_units.add(unit_id); +} + +static EventManager::EventHandler new_unit_handler(plugin_self, on_new_active_unit, RECENT_UNITS_SCAN_CYCLE); + +///////////////////////////////////////////////////// +// plugin API + +static command_result do_command(color_ostream &out, vector ¶meters); +static void follow_a_dwarf(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(control,out).print("initializing {}\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Automated spectator mode.", + do_command)); + + return CR_OK; +} + +static void on_disable(color_ostream &out, bool skip_restore_settings = false) { + EventManager::unregisterAll(plugin_self); + announcement_settings.reset(out, skip_restore_settings); +} + +static bool is_squads_open() { + return Gui::matchFocusString("dwarfmode/Squads", Gui::getDFViewscreen()); +} + +static void set_next_cycle_unpaused_ms(color_ostream &out, bool has_active_combat = false) { + int32_t delay_ms = config.follow_ms; + if (config.cinematic_action && has_active_combat) { + std::normal_distribution distribution(config.follow_ms / 2, config.follow_ms / 6); + delay_ms = distribution(rng); + delay_ms = std::min(config.follow_ms, std::max(1, delay_ms)); + } + DEBUG(cycle,out).print("next cycle in {} ms\n", delay_ms); + next_cycle_unpaused_ms = Core::getInstance().getUnpausedMs() + delay_ms; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control,out).print("{} from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + if (enable) { + config.reset(); + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "refresh_cpp_config")) { + WARN(control,out).print("Failed to refresh config\n"); + } + if (is_squads_open()) { + out.printerr("Cannot enable {} while the squads screen is open.\n", plugin_name); + Lua::CallLuaModuleFunction(out, "plugins.spectate", "show_squads_warning"); + is_enabled = false; + return CR_FAILURE; + } + INFO(control,out).print("Spectate mode enabled!\n"); + EventManager::registerListener(EventManager::EventType::UNIT_NEW_ACTIVE, new_unit_handler); + if (plotinfo->follow_unit > -1) + set_next_cycle_unpaused_ms(out); + else + follow_a_dwarf(out); + } else { + INFO(control,out).print("Spectate mode disabled!\n"); + plotinfo->follow_unit = -1; + on_disable(out); + // don't reset the unit history since we may want to re-enable + } + } else { + DEBUG(control,out).print("{} from the API, but already {}; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(control,out).print("shutting down {}\n", plugin_name); + on_disable(out); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case SC_WORLD_LOADED: + next_cycle_unpaused_ms = 0; + break; + case SC_WORLD_UNLOADED: + if (is_enabled) { + DEBUG(control,out).print("world unloaded; disabling {}\n", + plugin_name); + is_enabled = false; + on_disable(out, true); + unit_history.reset(); + recent_units.reset(); + } + break; + default: + break; + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + announcement_settings.on_update(out); + + if (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open()) { + DEBUG(cycle,out).print("auto-disengage triggered\n"); + is_enabled = false; + plotinfo->follow_unit = -1; + on_disable(out); + return CR_OK; + } + + if (Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { + recent_units.trim(); + follow_a_dwarf(out); + } + return CR_OK; +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.spectate", "parse_commandline", std::make_tuple(parameters), + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic + +static bool is_in_combat(df::unit *unit) { + if (Units::isCrazed(unit) || unit->mood == df::mood_type::Berserk) + return true; + for (auto activity_id : unit->activities) { + auto activity = df::activity_entry::find(activity_id); + if (activity && activity->type == df::activity_entry_type::Conflict) + return true; + } + return false; +} + +static void get_dwarf_buckets(color_ostream &out, + vector &citizen_combat_units, + vector &other_combat_units, + vector &nicknamed_units, + vector &job_units, + vector &other_units) +{ + for (auto unit : world->units.active) { + if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) + continue; + if (!config.include_animals && Units::isAnimal(unit)) + continue; + if (!config.include_hostiles && Units::isDanger(unit)) + continue; + if (!config.include_visitors && Units::isVisitor(unit)) + continue; + if (!config.include_wildlife && Units::isWildlife(unit)) + continue; + + if (is_in_combat(unit)) { + TRACE(cycle, out).print("unit {} is in combat: {}\n", unit->id, DF2CONSOLE(Units::getReadableName(unit))); + if (Units::isCitizen(unit, true) || Units::isResident(unit, true)) + citizen_combat_units.push_back(unit); + else + other_combat_units.push_back(unit); + } else if (config.prefer_nicknamed && !unit->name.nickname.empty()) { + nicknamed_units.push_back(unit); + } else if (unit->job.current_job && !boring_jobs.contains(unit->job.current_job->job_type)) { + job_units.push_back(unit); + } else { + other_units.push_back(unit); + } + } +} + +static void add_bucket_to_vectors(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + intervals.push_back(units.size() + bucket.size()); + weights.push_back(weight); + units.insert(units.end(), bucket.begin(), bucket.end()); +} + +static void add_bucket(const vector &bucket, vector &units, vector &intervals, vector &weights, float weight) { + if (bucket.empty()) + return; + if (config.prefer_new_arrivals) { + vector new_bucket, old_bucket; + for (auto unit : bucket) { + if (recent_units.contains(unit->id)) + new_bucket.push_back(unit); + else + old_bucket.push_back(unit); + } + add_bucket_to_vectors(new_bucket, units, intervals, weights, weight * RECENT_UNIT_MULTIPLIER); + add_bucket_to_vectors(old_bucket, units, intervals, weights, weight); + } else { + add_bucket_to_vectors(bucket, units, intervals, weights, weight); + } +} + +#define DUMP_BUCKET(name) \ + DEBUG(cycle,out).print("bucket: " #name ", size: {}\n", name.size()); \ + if (debug_cycle.isEnabled(DebugCategory::LTRACE)) { \ + for (auto u : name) { \ + DEBUG(cycle,out).print(" unit {}: {}\n", u->id, DF2CONSOLE(Units::getReadableName(u))); \ + } \ + } + +#define DUMP_FLOAT_VECTOR(name) \ + DEBUG(cycle,out).print(#name ":\n"); \ + for (float f : name) { \ + DEBUG(cycle,out).print(" {}\n", (int)f); \ + } + +static void follow_a_dwarf(color_ostream &out) { + DEBUG(cycle,out).print("choosing a unit to follow\n"); + + vector citizen_combat_units; + vector other_combat_units; + vector nicknamed_units; + vector job_units; + vector other_units; + get_dwarf_buckets(out, citizen_combat_units, other_combat_units, nicknamed_units, job_units, other_units); + + set_next_cycle_unpaused_ms(out, !citizen_combat_units.empty()); + + // coalesce the buckets and add weights + vector units; + vector intervals; + vector weights; + intervals.push_back(0); + add_bucket(citizen_combat_units, units, intervals, weights, config.prefer_conflict ? CITIZEN_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(other_combat_units, units, intervals, weights, config.prefer_conflict ? OTHER_COMBAT_PREFERRED_WEIGHT : JOB_WEIGHT); + add_bucket(nicknamed_units, units, intervals, weights, NICKNAMED_CITIZEN_PREFERRED_WEIGHT); + add_bucket(job_units, units, intervals, weights, JOB_WEIGHT); + add_bucket(other_units, units, intervals, weights, OTHER_WEIGHT); + + if (units.empty()) { + DEBUG(cycle,out).print("no units to follow\n"); + return; + } + + std::piecewise_constant_distribution distribution(intervals.begin(), intervals.end(), weights.begin()); + int unit_idx = distribution(rng); + df::unit *unit = units[unit_idx]; + + if (debug_cycle.isEnabled(DebugCategory::LDEBUG)) { + DUMP_BUCKET(citizen_combat_units); + DUMP_BUCKET(other_combat_units); + DUMP_BUCKET(nicknamed_units); + DUMP_BUCKET(job_units); + DUMP_BUCKET(other_units); + DUMP_FLOAT_VECTOR(intervals); + DUMP_FLOAT_VECTOR(weights); + DEBUG(cycle,out).print("selected unit idx {}\n", unit_idx); + } + + unit_history.add_and_follow(out, unit); +} + +///////////////////////////////////////////////////// +// Lua API + +static void spectate_setSetting(color_ostream &out, string name, int val) { + DEBUG(control,out).print("entering spectate_setSetting {} = {}\n", name, val); + + if (name == "auto-unpause") { + if (val && !config.auto_unpause) { + announcement_settings.save_and_scrub_settings(out); + } else if (!val && config.auto_unpause) { + announcement_settings.restore_settings(out); + } + config.auto_unpause = val; + } else if (name == "cinematic-action") { + config.cinematic_action = val; + } else if (name == "include-animals") { + config.include_animals = val; + } else if (name == "include-hostiles") { + config.include_hostiles = val; + } else if (name == "include-visitors") { + config.include_visitors = val; + } else if (name == "include-wildlife") { + config.include_wildlife = val; + } else if (name == "prefer-conflict") { + config.prefer_conflict = val; + } else if (name == "prefer-new-arrivals") { + config.prefer_new_arrivals = val; + } else if (name == "prefer-nicknamed") { + config.prefer_nicknamed = val; + } else if (name == "follow-seconds") { + if (val <= 0) { + WARN(control,out).print("follow-seconds must be a positive integer\n"); + return; + } + config.follow_ms = val * 1000; + } else { + WARN(control,out).print("Unknown setting: {}\n", name); + } +} + +static void spectate_followPrev(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followPrev\n"); + unit_history.scan_back(out); + set_next_cycle_unpaused_ms(out); +}; + +static void spectate_followNext(color_ostream &out) { + DEBUG(control,out).print("entering spectate_followNext\n"); + unit_history.scan_forward(out); + set_next_cycle_unpaused_ms(out); +}; + +static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { + DEBUG(control,out).print("entering spectate_addToHistory; unit_id={}\n", unit_id); + if (!df::unit::find(unit_id)) { + WARN(control,out).print("unit with id {} not found; not adding to history\n", unit_id); + return; + } + unit_history.add_to_history(out, unit_id); +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(spectate_setSetting), + DFHACK_LUA_FUNCTION(spectate_followPrev), + DFHACK_LUA_FUNCTION(spectate_followNext), + DFHACK_LUA_FUNCTION(spectate_addToHistory), + DFHACK_LUA_END +}; diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt deleted file mode 100644 index d2de072d8b..0000000000 --- a/plugins/spectate/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ - -project(spectate) - -SET(SOURCES - spectate.cpp - pause.cpp) - -dfhack_plugin(${PROJECT_NAME} ${SOURCES}) diff --git a/plugins/spectate/pause.cpp b/plugins/spectate/pause.cpp deleted file mode 100644 index f94b1e73a9..0000000000 --- a/plugins/spectate/pause.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "pause.h" -#include -#include -#include -#include -#include -#include - -#include - -using namespace DFHack; -using namespace Pausing; -using namespace df::enums; - -// marked by REQUIRE_GLOBAL in spectate.cpp -using df::global::plotinfo; -using df::global::d_init; - -std::unordered_set PlayerLock::locks; -std::unordered_set AnnouncementLock::locks; - -namespace pausing { - AnnouncementLock announcementLock("monitor"); - PlayerLock playerLock("monitor"); - - const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); - bool state_saved = false; // indicates whether a restore state is ok - bool saved_states[announcement_flag_arr_size]; // state to restore - bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) - bool allow_player_pause = true; // toggles player pause ability - - using namespace df::enums; - struct player_pause_hook : df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { - if ((plotinfo->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { - input->erase(interface_key::D_PAUSE); - } - INTERPOSE_NEXT(feed)(input); - } - }; - - IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); -} -using namespace pausing; - -template -inline bool any_lock(Locks locks) { - return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); -} - -template -inline bool only_lock(Locks locks, LockT* this_lock) { - return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { - if (lock == this_lock) { - return lock->isLocked(); - } - return !lock->isLocked(); - }); -} - -template -inline bool only_or_none_locked(Locks locks, LockT* this_lock) { - for (auto &L: locks) { - if (L == this_lock) { - continue; - } - if (L->isLocked()) { - return false; - } - } - return true; -} - -template -inline bool reportLockedLocks(color_ostream &out, Locks locks) { - out.color(DFHack::COLOR_YELLOW); - for (auto &L: locks) { - if (L->isLocked()) { - out.print("Lock: '%s'\n", L->name.c_str()); - } - } - out.reset_color(); - return true; -} - -bool AnnouncementLock::captureState() { - if (only_or_none_locked(locks, this)) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - locked_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - return true; - } - return false; -} - -void AnnouncementLock::lock() { - Lock::lock(); - captureState(); -} - -bool AnnouncementLock::isAnyLocked() const { - return any_lock(locks); -} - -bool AnnouncementLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void AnnouncementLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool PlayerLock::isAnyLocked() const { - return any_lock(locks); -} - -bool PlayerLock::isOnlyLocked() const { - return only_lock(locks, this); -} - -void PlayerLock::reportLocks(color_ostream &out) { - reportLockedLocks(out, locks); -} - -bool World::DisableAnnouncementPausing() { - if (!announcementLock.isAnyLocked()) { - for (auto& flag : d_init->announcements.flags) { - flag.bits.PAUSE = false; - //out.print("pause: %d\n", flag.bits.PAUSE); - } - return true; - } - return false; -} - -bool World::SaveAnnouncementSettings() { - if (!announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - saved_states[i] = d_init->announcements.flags[i].bits.PAUSE; - } - state_saved = true; - return true; - } - return false; -} - -bool World::RestoreAnnouncementSettings() { - if (!announcementLock.isAnyLocked() && state_saved) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; - } - return true; - } - return false; -} - -bool World::EnablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = true; - } - return allow_player_pause; -} - -bool World::DisablePlayerPausing() { - if (!playerLock.isAnyLocked()) { - allow_player_pause = false; - } - return !allow_player_pause; -} - -bool World::IsPlayerPausingEnabled() { - return allow_player_pause; -} - -void World::Update() { - static bool did_once = false; - if (!did_once) { - did_once = true; - INTERPOSE_HOOK(player_pause_hook, feed).apply(); - } - if (announcementLock.isAnyLocked()) { - for (size_t i = 0; i < announcement_flag_arr_size; ++i) { - d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; - } - } -} diff --git a/plugins/spectate/pause.h b/plugins/spectate/pause.h deleted file mode 100644 index ab736ed531..0000000000 --- a/plugins/spectate/pause.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include -#include -#include - -namespace DFHack { - //////////// - // Locking mechanisms for control over pausing - namespace Pausing - { - class Lock - { - bool locked = false; - public: - const std::string name; - explicit Lock(const char* name) : name(name){} - virtual ~Lock()= default; - virtual bool isAnyLocked() const = 0; - virtual bool isOnlyLocked() const = 0; - bool isLocked() const { return locked; } - virtual void lock() { locked = true; } //simply locks the lock - void unlock() { locked = false; } - virtual void reportLocks(color_ostream &out) = 0; - }; - - // non-blocking lock resource used in conjunction with the announcement functions in World - class AnnouncementLock : public Lock - { - static std::unordered_set locks; - public: - explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } - ~AnnouncementLock() override { locks.erase(this); } - bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) - void lock() override; // locks and attempts to capture state - bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the Player pause functions in World - class PlayerLock : public Lock - { - static std::unordered_set locks; - public: - explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } - ~PlayerLock() override { locks.erase(this); } - bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked - bool isOnlyLocked() const override; // returns true if locked and no other instance is locked - void reportLocks(color_ostream &out) override; - }; - - // non-blocking lock resource used in conjunction with the pause set state function in World -// todo: integrate with World::SetPauseState -// class PauseStateLock : public Lock -// { -// static std::unordered_set locks; -// public: -// explicit PauseStateLock(const char* name): Lock(name) { locks.emplace(this); } -// ~PauseStateLock() override { locks.erase(this); } -// bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked -// bool isOnlyLocked() const override; // returns true if locked and no other instance is locked -// void reportLocks(color_ostream &out) override; -// }; - } - namespace World { - bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open - bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open - bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open and there is state information to restore (returns true if a restore took place) - - bool EnablePlayerPausing(); // enable player pausing if all locks are open - bool DisablePlayerPausing(); // disable player pausing if all locks are open - bool IsPlayerPausingEnabled(); // returns whether the player can pause or not - - void Update(); - } -} diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp deleted file mode 100644 index 36bd2d94b2..0000000000 --- a/plugins/spectate/spectate.cpp +++ /dev/null @@ -1,478 +0,0 @@ -#include "pause.h" - -#include "Debug.h" -#include "Export.h" -#include "PluginManager.h" - -#include "modules/EventManager.h" -#include "modules/World.h" -#include "modules/Maps.h" -#include "modules/Gui.h" -#include "modules/Job.h" -#include "modules/Units.h" - -#include "df/job.h" -#include "df/unit.h" -#include "df/historical_figure.h" -#include "df/global_objects.h" -#include "df/plotinfost.h" -#include "df/world.h" -#include "df/viewscreen.h" -#include "df/creature_raw.h" - -#include -#include -#include - -// Debugging -namespace DFHack { - DBG_DECLARE(log, plugin, DebugCategory::LINFO); -} - -DFHACK_PLUGIN("spectate"); -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(d_init); // used in pause.cpp - -using namespace DFHack; -using namespace Pausing; -using namespace df::enums; - -struct Configuration { - bool unpause = false; - bool disengage = false; - bool animals = false; - bool hostiles = true; - bool visitors = false; - int32_t tick_threshold = 1000; -} config; - -Pausing::AnnouncementLock* pause_lock = nullptr; -bool lock_collision = false; -bool announcements_disabled = false; - -#define base 0.99 - -static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; -enum ConfigData { - UNPAUSE, - DISENGAGE, - TICK_THRESHOLD, - ANIMALS, - HOSTILES, - VISITORS -}; - -static PersistentDataItem pconfig; - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); -command_result spectate (color_ostream &out, std::vector & parameters); -#define COORDARGS(id) id.x, id.y, id.z - -namespace SP { - bool following_dwarf = false; - df::unit* our_dorf = nullptr; - int32_t timestamp = -1; - std::default_random_engine RNG; - - void DebugUnitVector(std::vector units) { - if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { - for (auto unit: units) { - DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); - } - } - } - - void PrintStatus(color_ostream &out) { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); - out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); - out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); - out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); - out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); - out.print(" SETTINGS:\n"); - out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); - if (following_dwarf) - out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", plotinfo->follow_unit); - } - - void SetUnpauseState(bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] - // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && config.unpause != state) { - WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); - // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, - // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) - lock_collision = false; - config.unpause = state; - if (config.unpause) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); - WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " The action you were attempting will complete when the following lock or locks lift.\n"); - pause_lock->reportLocks(Core::getInstance().getConsole()); - } - } - config.unpause = state; - } - - void SaveSettings() { - if (pconfig.isValid()) { - pconfig.ival(UNPAUSE) = config.unpause; - pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; - pconfig.ival(ANIMALS) = config.animals; - pconfig.ival(HOSTILES) = config.hostiles; - pconfig.ival(VISITORS) = config.visitors; - } - } - - void LoadSettings() { - pconfig = World::GetPersistentSiteData(CONFIG_KEY); - - if (!pconfig.isValid()) { - pconfig = World::AddPersistentSiteData(CONFIG_KEY); - SaveSettings(); - } else { - config.unpause = pconfig.ival(UNPAUSE); - config.disengage = pconfig.ival(DISENGAGE); - config.tick_threshold = pconfig.ival(TICK_THRESHOLD); - config.animals = pconfig.ival(ANIMALS); - config.hostiles = pconfig.ival(HOSTILES); - config.visitors = pconfig.ival(VISITORS); - pause_lock->unlock(); - SetUnpauseState(config.unpause); - } - } - - bool FollowADwarf() { - if (enabled && !World::ReadPauseState()) { - df::coord viewMin = Gui::getViewportPos(); - df::coord viewMax{viewMin}; - const auto &dims = Gui::getDwarfmodeViewDims().map().second; - viewMax.x += dims.x - 1; - viewMax.y += dims.y - 1; - viewMax.z = viewMin.z; - std::vector units; - static auto add_if = [&](std::function check) { - for (auto unit : world->units.active) { - if (check(unit)) { - units.push_back(unit); - } - } - }; - static auto valid = [](df::unit* unit) { - if (Units::isAnimal(unit)) { - return config.animals; - } - if (Units::isVisiting(unit)) { - return config.visitors; - } - if (Units::isDanger(unit)) { - return config.hostiles; - } - return true; - }; - static auto calc_extra_weight = [](size_t idx, double r1, double r2) { - switch(idx) { - case 0: - return r2; - case 1: - return (r2-r1)/1.3; - case 2: - return (r2-r1)/2; - default: - return 0.0; - } - }; - /// Collecting our choice pool - /////////////////////////////// - std::array ranges{}; - std::array range_exists{}; - static auto build_range = [&](size_t idx){ - size_t first = idx * 2; - size_t second = idx * 2 + 1; - size_t previous = first - 1; - // first we get the end of the range - ranges[second] = units.size() - 1; - // then we calculate whether the range exists, and set the first index appropriately - if (idx == 0) { - range_exists[idx] = ranges[second] >= 0; - ranges[first] = 0; - } else { - range_exists[idx] = ranges[second] > ranges[previous]; - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); - } - }; - - /// RANGE 0 (in view + working) - // grab valid working units - add_if([&](df::unit* unit) { - return valid(unit) && - Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && - Units::isCitizen(unit, true) && - unit->job.current_job; - }); - build_range(0); - - /// RANGE 1 (in view) - add_if([&](df::unit* unit) { - return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); - }); - build_range(1); - - /// RANGE 2 (working citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; - }); - build_range(2); - - /// RANGE 3 (citizens) - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true); - }); - build_range(3); - - /// RANGE 4 (any valid) - add_if(valid); - build_range(4); - - // selecting from our choice pool - if (!units.empty()) { - std::array bw{23,17,13,7,1}; // probability weights for each range - std::vector i; - std::vector w; - bool at_least_one = false; - // in one word, elegance - for(size_t idx = 0; idx < range_exists.size(); ++idx) { - if (range_exists[idx]) { - at_least_one = true; - const auto &r1 = ranges[idx*2]; - const auto &r2 = ranges[idx*2+1]; - double extra = calc_extra_weight(idx, r1, r2); - i.push_back(r1); - w.push_back(bw[idx] + extra); - if (r1 != r2) { - i.push_back(r2); - w.push_back(bw[idx] + extra); - } - } - } - if (!at_least_one) { - return false; - } - DebugUnitVector(units); - std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); - // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) - size_t idx = follow_any(RNG); - our_dorf = units[idx]; - plotinfo->follow_unit = our_dorf->id; - timestamp = world->frame_counter; - return true; - } else { - WARN(plugin).print("units vector is empty!\n"); - } - } - return false; - } - - void onUpdate(color_ostream &out) { - // keeps announcement pause settings locked - World::Update(); // from pause.h - - // Plugin Management - if (lock_collision) { - if (config.unpause) { - // player asked for auto-unpause enabled - World::SaveAnnouncementSettings(); - if (World::DisableAnnouncementPausing()) { - // now that we've got what we want, we can lock it down - lock_collision = false; - } - } else { - if (World::RestoreAnnouncementSettings()) { - lock_collision = false; - } - } - } - int failsafe = 0; - while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { - // dismiss announcement popup(s) - Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); - if (World::ReadPauseState()) { - // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is - World::SetPauseState(false); - } - } - if (failsafe >= 10) { - out.printerr("spectate encountered a problem dismissing a popup!\n"); - } - - // plugin logic - static int32_t last_tick = -1; - int32_t tick = world->frame_counter; - if (!World::ReadPauseState() && tick - last_tick >= 1) { - last_tick = tick; - // validate follow state - if (!following_dwarf || !our_dorf || plotinfo->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { - // we're not following anyone - following_dwarf = false; - if (!config.disengage) { - // try to - following_dwarf = FollowADwarf(); - } else if (!World::ReadPauseState()) { - plugin_enable(out, false); - } - } - } - } -}; - -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand("spectate", - "Automated spectator mode.", - spectate, - false)); - pause_lock = new AnnouncementLock("spectate"); - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - delete pause_lock; - return CR_OK; -} - -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - SP::LoadSettings(); - if (enabled) { - SP::following_dwarf = SP::FollowADwarf(); - SP::PrintStatus(out); - } - return DFHack::CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (enable && !enabled) { - out.print("Spectate mode enabled!\n"); - enabled = true; // enable_auto_unpause won't do anything without this set now - SP::SetUnpauseState(config.unpause); - } else if (!enable && enabled) { - // warp 8, engage! - out.print("Spectate mode disabled!\n"); - // we need to retain whether auto-unpause is enabled, but we also need to disable its effect - bool temp = config.unpause; - SP::SetUnpauseState(false); - config.unpause = temp; - } - enabled = enable; - return DFHack::CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (enabled) { - switch (event) { - case SC_WORLD_UNLOADED: - SP::our_dorf = nullptr; - SP::following_dwarf = false; - enabled = false; - default: - break; - } - } - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - SP::onUpdate(out); - return DFHack::CR_OK; -} - -command_result spectate (color_ostream &out, std::vector & parameters) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (!parameters.empty()) { - if (parameters.size() >= 2 && parameters.size() <= 3) { - bool state =false; - bool set = false; - if (parameters[0] == "enable") { - state = true; - } else if (parameters[0] == "disable") { - state = false; - } else if (parameters[0] == "set") { - set = true; - } else { - return DFHack::CR_WRONG_USAGE; - } - if(parameters[1] == "auto-unpause"){ - SP::SetUnpauseState(state); - } else if (parameters[1] == "auto-disengage") { - config.disengage = state; - } else if (parameters[1] == "animals") { - config.animals = state; - } else if (parameters[1] == "hostiles") { - config.hostiles = state; - } else if (parameters[1] == "visiting") { - config.visitors = state; - } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { - try { - config.tick_threshold = std::abs(std::stol(parameters[2])); - } catch (const std::exception &e) { - out.printerr("%s\n", e.what()); - } - } else { - return DFHack::CR_WRONG_USAGE; - } - } - } else { - SP::PrintStatus(out); - } - SP::SaveSettings(); - return DFHack::CR_OK; -} diff --git a/plugins/steam-engine.cpp b/plugins/steam-engine.cpp index befd53f10a..06b14aa6c0 100644 --- a/plugins/steam-engine.cpp +++ b/plugins/steam-engine.cpp @@ -870,7 +870,7 @@ static bool find_engines(color_ostream &out) if (!ws.gear_tiles.empty()) engines.push_back(ws); else - out.printerr("%s has no gear tiles - ignoring.\n", wslist[i]->code.c_str()); + out.printerr("{} has no gear tiles - ignoring.\n", wslist[i]->code); } return !engines.empty(); diff --git a/plugins/stockflow.cpp b/plugins/stockflow.cpp index 1671d08991..a14aafadcc 100644 --- a/plugins/stockflow.cpp +++ b/plugins/stockflow.cpp @@ -4,31 +4,22 @@ * or set AUTOENABLE to true. */ -#include "uicommon.h" #include "LuaTools.h" +#include "PluginManager.h" -#include "df/building_stockpilest.h" -#include "df/job.h" -#include "df/viewscreen_dwarfmodest.h" - -#include "modules/Gui.h" #include "modules/Maps.h" #include "modules/World.h" -using namespace DFHack; -using namespace std; +#include "df/global_objects.h" +#include "df/world.h" -using df::building_stockpilest; +using namespace DFHack; DFHACK_PLUGIN("stockflow"); #define AUTOENABLE false DFHACK_PLUGIN_IS_ENABLED(enabled); -REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); - -bool fast = false; /* * Lua interface. @@ -37,50 +28,18 @@ bool fast = false; class LuaHelper { public: void cycle(color_ostream &out) { - bool found = false; - - if (fast) { - // Ignore the bookkeeper; either gather or enqueue orders every cycle. - found = !bookkeeping; - } else { - // Gather orders when the bookkeeper starts updating stockpile records, - // and enqueue them when the job is done. - for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next) { - if (link->item == NULL) continue; - if (link->item->job_type == job_type::UpdateStockpileRecords) { - found = true; - break; - } - } - } - - if (found) { - // Entice the bookkeeper to spend less time update records. - plotinfo->nobles.bookkeeper_precision += plotinfo->nobles.bookkeeper_precision >> 3; - if (!bookkeeping) { - command_method("start_bookkeeping", out); - bookkeeping = true; - } - } else { - // Entice the bookkeeper to update records more often. - plotinfo->nobles.bookkeeper_precision -= plotinfo->nobles.bookkeeper_precision >> 5; - plotinfo->nobles.bookkeeper_cooldown -= plotinfo->nobles.bookkeeper_cooldown >> 2; - if (bookkeeping) { - command_method("finish_bookkeeping", out); - bookkeeping = false; - } - } + // Gather orders and enqueue them in a single pass. This used to be + // gated on the bookkeeper's UpdateStockpileRecords job, but that job + // does not run reliably due to a base game bug. + command_method("start_bookkeeping", out); + command_method("finish_bookkeeping", out); } void init() { - stockpile_id = -1; initialized = false; - bookkeeping = false; } bool reset(color_ostream &out, bool load) { - stockpile_id = -1; - bookkeeping = false; if (load) { return initialized = command_method("initialize_world", out); } else if (initialized) { @@ -93,122 +52,18 @@ class LuaHelper { bool command_method(const char *method, color_ostream &out) { // Calls a lua function with no parameters. - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 1)) - return false; - - if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method)) - return false; - - if (!Lua::SafeCall(out, L, 0, 0)) - return false; - - return true; - } - - bool stockpile_method(const char *method, building_stockpilest *sp) { - // Combines the select_order and toggle_trigger method calls, - // because they share the same signature. - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2)) - return false; - - if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method)) - return false; - - Lua::Push(L, sp); - - if (!Lua::SafeCall(out, L, 1, 0)) - return false; - - // Invalidate the string cache. - stockpile_id = -1; - - return true; - } - - bool collect_settings(building_stockpilest *sp) { - // Find strings representing the job to order, and the trigger condition. - // There might be a memory leak here; C++ is odd like that. - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - CoreSuspender suspend; - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2)) - return false; - - if (!Lua::PushModulePublic(out, L, "plugins.stockflow", "stockpile_settings")) - return false; - - Lua::Push(L, sp); - - if (!Lua::SafeCall(out, L, 1, 2)) - return false; - - if (!lua_isstring(L, -1)) - return false; - - current_trigger = lua_tostring(L, -1); - lua_pop(L, 1); - - if (!lua_isstring(L, -1)) - return false; - - current_job = lua_tostring(L, -1); - lua_pop(L, 1); - - stockpile_id = sp->id; - - return true; - } - - void draw(building_stockpilest *sp) { - if (sp->id != stockpile_id) { - if (!collect_settings(sp)) { - Core::printerr("Stockflow job collection failed!\n"); - return; - } - } - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = dims.y2 - 2; // below autodump, automelt, autotrade, stocks, stockpiles - - int links = 0; - links += sp->links.give_to_pile.size(); - links += sp->links.take_from_pile.size(); - links += sp->links.give_to_workshop.size(); - links += sp->links.take_from_workshop.size(); - if (links + 12 >= y) - y += 1; - - OutputHotkeyString(x, y, current_job, "j", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (*current_trigger) - OutputHotkeyString(x, y, current_trigger, " J", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); + return Lua::CallLuaModuleFunction(out, "plugins.stockflow", method); } - private: - long stockpile_id; bool initialized; - bool bookkeeping; - const char *current_job; - const char *current_trigger; }; static LuaHelper helper; -#define DELTA_TICKS 600 +// Once per in-game day, more or less. A prime number is used so that +// periodic tools don't all fire on the same tick. +constexpr auto DELTA_TICKS = 1187; DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!Maps::IsValid()) @@ -225,69 +80,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } - -/* - * Interface hooks - */ -struct stockflow_hook : public df::viewscreen_dwarfmodest { - typedef df::viewscreen_dwarfmodest interpose_base; - - bool handleInput(set *input) { - if (Gui::inRenameBuilding()) - return false; - - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return false; - - if (input->count(interface_key::CUSTOM_J)) { - // Select a new order for this stockpile. - if (!helper.stockpile_method("select_order", sp)) { - Core::printerr("Stockflow order selection failed!\n"); - } - - return true; - } else if (input->count(interface_key::CUSTOM_SHIFT_J)) { - // Toggle the order trigger for this stockpile. - if (!helper.stockpile_method("toggle_trigger", sp)) { - Core::printerr("Stockflow trigger toggle failed!\n"); - } - - return true; - } - - return false; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) { - INTERPOSE_NEXT(render)(); - - building_stockpilest *sp = get_selected_stockpile(); - if (sp) - helper.draw(sp); - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, render); - - -static bool apply_hooks(color_ostream &out, bool enabling) { - if (enabling && !gps) { - out.printerr("Stockflow needs graphics.\n"); - return false; - } - - if (!INTERPOSE_HOOK(stockflow_hook, feed).apply(enabling) || !INTERPOSE_HOOK(stockflow_hook, render).apply(enabling)) { - out.printerr("Could not %s stockflow hooks!\n", enabling? "insert": "remove"); - return false; - } - +static bool apply_state(color_ostream &out, bool enabling) { if (!helper.reset(out, enabling && Maps::IsValid())) { out.printerr("Could not reset stockflow world data!\n"); return false; @@ -296,18 +89,13 @@ static bool apply_hooks(color_ostream &out, bool enabling) { return true; } -static command_result stockflow_cmd(color_ostream &out, vector & parameters) { +static command_result stockflow_cmd(color_ostream &out, std::vector & parameters) { bool desired = enabled; if (parameters.size() == 1) { if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1") { desired = true; - fast = false; } else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0") { desired = false; - fast = false; - } else if (parameters[0] == "fast" || parameters[0] == "always" || parameters[0] == "2") { - desired = true; - fast = true; } else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") { return CR_WRONG_USAGE; } else if (parameters[0] == "list") { @@ -331,12 +119,12 @@ static command_result stockflow_cmd(color_ostream &out, vector & parame } if (desired != enabled) { - if (!apply_hooks(out, desired)) { + if (!apply_state(out, desired)) { return CR_FAILURE; } } - out.print("Stockflow is %s %s%s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); + out.print("Stockflow is {} {}.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled"); enabled = desired; return CR_OK; } @@ -362,7 +150,7 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { /* Accept the "enable stockflow"/"disable stockflow" syntax, where available. */ /* Same as "stockflow enable"/"stockflow disable" except without the status line. */ if (enable != enabled) { - if (!apply_hooks(out, enable)) { + if (!apply_state(out, enable)) { return CR_FAILURE; } @@ -375,7 +163,7 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { helper.init(); if (AUTOENABLE) { - if (!apply_hooks(out, true)) { + if (!apply_state(out, true)) { return CR_FAILURE; } diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1efdd4df60..b722883804 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -5,6 +5,7 @@ #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/global_objects.h" #include "df/world.h" using namespace DFHack; @@ -22,8 +23,8 @@ DBG_EXTERN(stockpiles, log); */ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat) { - DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx); - df::world_raws& raws = world->raws; + DEBUG(log, out).print("food_lookup: food_idx({})\n", food_idx); + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; int32_t main_idx = table.organic_indexes[mat_category][food_idx]; int16_t type = table.organic_types[mat_category][food_idx]; @@ -32,16 +33,14 @@ void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category: mat_category == organic_mat_category::Eggs) { food_mat.creature = raws.creatures.all[type]; food_mat.caste = food_mat.creature->caste[main_idx]; - DEBUG(log, out).print("special creature type(%d) caste(%d)\n", type, main_idx); + DEBUG(log, out).print("special creature type({}) caste({})\n", type, main_idx); } else { food_mat.material.decode(type, main_idx); - DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str()); + DEBUG(log, out).print("type({}) index({}) token({})\n", type, main_idx, food_mat.material.getToken()); } } -std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx) { - FoodMat food_mat; - food_mat_by_idx(out, mat_category, idx, food_mat); +std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, const FoodMat& food_mat) { if (food_mat.material.isValid()) { return food_mat.material.getToken(); } @@ -58,22 +57,22 @@ size_t OrganicMatLookup::food_max_size(organic_mat_category::organic_mat_categor void OrganicMatLookup::food_build_map() { if (index_built) return; - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; using df::enums::organic_mat_category::organic_mat_category; using traits = df::enum_traits; - for (int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category) { + for (int32_t mat_category = 0; mat_category <= traits::last_item_value; ++mat_category) { for (size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i) { int16_t type = table.organic_types[mat_category].at(i); int32_t index = table.organic_indexes[mat_category].at(i); - food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); // wtf.. only in c++ + food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); } } index_built = true; } int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_category::organic_mat_category mat_category, const std::string& token) { - df::world_raws& raws = world->raws; + auto& raws = world->raws; df::special_mat_table table = raws.mat_table; DEBUG(log, out).print("food_idx_by_token:\n"); if (mat_category == organic_mat_category::Fish || @@ -82,22 +81,22 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate std::vector tokens; split_string(&tokens, token, ":"); if (tokens.size() != 2) { - WARN(log, out).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str()); + WARN(log, out).print("creature invalid CREATURE:CASTE token: {}\n", token); return -1; } int16_t creature_idx = find_creature(tokens[0]); if (creature_idx < 0) { - WARN(log, out).print("creature invalid token %s\n", tokens[0].c_str()); + WARN(log, out).print("creature invalid token {}\n", tokens[0]); return -1; } int16_t food_idx = linear_index(table.organic_types[mat_category], creature_idx); if (tokens[1] == "MALE") food_idx += 1; if (table.organic_types[mat_category][food_idx] == creature_idx) { - DEBUG(log, out).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + DEBUG(log, out).print("creature {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return food_idx; } - WARN(log, out).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx); + WARN(log, out).print("creature caste not found: {} caste {} creature_idx({}) food_idx({})\n", token, tokens[1], creature_idx, food_idx); return -1; } @@ -108,12 +107,12 @@ int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_cate int32_t index = mat_info.index; auto it = food_index[mat_category].find(std::make_pair(type, index)); if (it != food_index[mat_category].end()) { - DEBUG(log, out).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second); + DEBUG(log, out).print("matinfo: {} type({}) idx({}) food_idx({})\n", token, mat_info.type, mat_info.index, it->second); return it->second; } - WARN(log, out).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index); + WARN(log, out).print("matinfo: {} type({}) idx({}) food_idx not found :(\n", token, mat_info.type, mat_info.index); return -1; } diff --git a/plugins/stockpiles/OrganicMatLookup.h b/plugins/stockpiles/OrganicMatLookup.h index f585ef2de4..15f9838df2 100644 --- a/plugins/stockpiles/OrganicMatLookup.h +++ b/plugins/stockpiles/OrganicMatLookup.h @@ -31,7 +31,7 @@ class OrganicMatLookup { }; static void food_mat_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type food_idx, FoodMat& food_mat); - static std::string food_token_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector::size_type idx); + static std::string food_token_by_idx(DFHack::color_ostream& out, const FoodMat& food_mat); static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category); static void food_build_map(); diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index f315e7d674..f1b1a3bd3e 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -12,6 +12,7 @@ #include "df/building_stockpilest.h" #include "df/creature_raw.h" #include "df/caste_raw.h" +#include "df/descriptor_color.h" #include "df/inorganic_raw.h" #include "df/item_quality.h" #include @@ -179,8 +180,8 @@ bool StockpileSettingsSerializer::serialize_to_ostream(color_ostream& out, std:: bool StockpileSettingsSerializer::serialize_to_file(color_ostream& out, const string& file, uint32_t includedElements) { std::fstream output(file, std::ios::out | std::ios::binary | std::ios::trunc); if (output.fail()) { - WARN(log, out).print("ERROR: failed to open file for writing: '%s'\n", - file.c_str()); + WARN(log, out).print("ERROR: failed to open file for writing: '{}'\n", + file); return false; } return serialize_to_ostream(out, &output, includedElements); @@ -201,8 +202,8 @@ bool StockpileSettingsSerializer::parse_from_istream(color_ostream &out, std::is bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, const string& file, DeserializeMode mode, const vector& filters) { std::fstream input(file, std::ios::in | std::ios::binary); if (input.fail()) { - WARN(log, out).print("failed to open file for reading: '%s'\n", - file.c_str()); + WARN(log, out).print("failed to open file for reading: '{}'\n", + file); return false; } return parse_from_istream(out, &input, mode, filters); @@ -210,29 +211,20 @@ bool StockpileSettingsSerializer::unserialize_from_file(color_ostream &out, cons /** * 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 + * @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 typename df::enum_traits::base_type token_to_enum_val(const string& token) { + E val; + if (!find_enum_item(&val, token)) + return -1; + return val; } static bool matches_filter(color_ostream& out, const vector& filters, const string& name) { for (auto & filter : filters) { - DEBUG(log, out).print("searching for '%s' in '%s'\n", filter.c_str(), name.c_str()); + DEBUG(log, out).print("searching for '{}' in '{}'\n", filter, name); 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()) @@ -243,7 +235,7 @@ static bool matches_filter(color_ostream& out, const vector& filters, co static void set_flag(color_ostream& out, const char* name, const vector& filters, bool all, char val, bool enabled, bool& elem) { if ((all || enabled) && matches_filter(out, filters, name)) { - DEBUG(log, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); elem = val; } } @@ -251,7 +243,7 @@ static void set_flag(color_ostream& out, const char* name, const vector& static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, char val, const string& name, const string& id, char& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%s) to %d\n", name.c_str(), id.c_str(), val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -260,7 +252,7 @@ template static void set_filter_elem(color_ostream& out, const char* subcat, const vector& filters, T_val val, const string& name, T_id id, T_val& elem) { if (matches_filter(out, filters, subcat + ((*subcat ? "/" : "") + name))) { - DEBUG(log, out).print("setting %s (%d) to %d\n", name.c_str(), (int32_t)id, val); + DEBUG(log, out).print("setting {} ({}) to {}\n", name, id, val); elem = val; } } @@ -295,7 +287,7 @@ static bool serialize_list_itemdef(color_ostream& out, FuncWriteExport add_value ItemTypeInfo ii; if (!ii.decode(type, i)) continue; - DEBUG(log, out).print("adding itemdef type %s\n", ii.getToken().c_str()); + DEBUG(log, out).print("adding itemdef type {}\n", ii.getToken()); add_value(ii.getToken()); } return all; @@ -320,7 +312,7 @@ static void unserialize_list_itemdef(color_ostream& out, const char* subcat, boo if (!ii.find(id)) continue; if (ii.subtype < 0 || size_t(ii.subtype) >= pile_list.size()) { - WARN(log, out).print("item type index invalid: %d\n", ii.subtype); + WARN(log, out).print("item type index invalid: {}\n", ii.subtype); continue; } set_filter_elem(out, subcat, filters, val, id, ii.subtype, pile_list.at(ii.subtype)); @@ -328,7 +320,7 @@ static void unserialize_list_itemdef(color_ostream& out, const char* subcat, boo } static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value, - const bool(&quality_list)[7]) { + const std::array &quality_list) { using df::enums::item_quality::item_quality; using quality_traits = df::enum_traits; @@ -340,17 +332,17 @@ static bool serialize_list_quality(color_ostream& out, FuncWriteExport add_value } const string f_type(quality_traits::key_table[i]); add_value(f_type); - DEBUG(log, out).print("adding quality %s\n", f_type.c_str()); + DEBUG(log, out).print("adding quality {}\n", f_type); } return all; } -static void quality_clear(bool(&pile_list)[7]) { - std::fill(pile_list, pile_list + 7, false); +static void quality_clear(std::array &pile_list) { + pile_list.fill(false); } static void unserialize_list_quality(color_ostream& out, const char* subcat, bool all, bool val, const vector& filters, - FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]) { + FuncReadImport read_value, int32_t list_size, std::array &pile_list) { if (all) { for (auto idx = 0; idx < 7; ++idx) { string id = ENUM_KEY_STR(item_quality, (df::item_quality)idx); @@ -360,12 +352,11 @@ static void unserialize_list_quality(color_ostream& out, const char* subcat, boo } 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); + df::enum_traits::base_type idx = token_to_enum_val(quality); if (idx < 0) { - WARN(log, out).print("invalid quality token: %s\n", quality.c_str()); + WARN(log, out).print("invalid quality token: {}\n", quality); continue; } set_filter_elem(out, subcat, filters, val, quality, idx, pile_list[idx]); @@ -401,11 +392,11 @@ static bool serialize_list_other_mats(color_ostream& out, } const string token = other_mats_index(other_mats, i); if (token.empty()) { - WARN(log, out).print("invalid other material with index %zd\n", i); + WARN(log, out).print("invalid other material with index {}\n", i); continue; } add_value(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } return all; } @@ -425,11 +416,11 @@ static void unserialize_list_other_mats(color_ostream& out, const char* subcat, const string token = read_value(i); size_t idx = other_mats_token(other_mats, token); if (idx < 0) { - WARN(log, out).print("invalid other mat with token %s\n", token.c_str()); + WARN(log, out).print("invalid other mat with token {}\n", token); continue; } if (idx >= num_elems) { - WARN(log, out).print("other_mats index too large! idx[%zd] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("other_mats index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -449,17 +440,37 @@ static bool serialize_list_organic_mat(color_ostream& out, FuncWriteExport add_v all = false; continue; } - string token = OrganicMatLookup::food_token_by_idx(out, cat, i); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, i, food_mat); + string token = OrganicMatLookup::food_token_by_idx(out, food_mat); if (token.empty()) { DEBUG(log, out).print("food mat invalid :(\n"); continue; } - DEBUG(log, out).print("organic_material %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("organic_material {} is {}\n", i, token); add_value(token); } return all; } +static string get_filter_string(color_ostream& out, const OrganicMatLookup::FoodMat& food_mat) { + auto str = OrganicMatLookup::food_token_by_idx(out, food_mat); + if (auto plant = food_mat.material.plant) { + if (plant->flags.is_set(df::plant_raw_flags::DRINK)) + str += "/brewable"; + if (plant->flags.is_set(df::plant_raw_flags::MILL)) + str += "/millable"; + if (auto mat = food_mat.material.material) { + if (mat->flags.is_set(df::material_flags::STRUCTURAL_PLANT_MAT) && + (plant->flags.is_set(df::plant_raw_flags::THREAD) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_VIAL) || + plant->flags.is_set(df::plant_raw_flags::EXTRACT_BARREL))) + str += "/processable"; + } + } + return str; +} + static void unserialize_list_organic_mat(color_ostream& out, 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) { @@ -467,8 +478,9 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, pile_list.resize(num_elems, '\0'); if (all) { for (size_t idx = 0; idx < num_elems; ++idx) { - string token = OrganicMatLookup::food_token_by_idx(out, cat, idx); - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); } return; } @@ -477,10 +489,63 @@ static void unserialize_list_organic_mat(color_ostream& out, const char* subcat, const string token = read_value(i); int16_t idx = OrganicMatLookup::food_idx_by_token(out, cat, token); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("organic mat index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("organic mat index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } - set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); + OrganicMatLookup::FoodMat food_mat; + OrganicMatLookup::food_mat_by_idx(out, cat, idx, food_mat); + set_filter_elem(out, subcat, filters, val, get_filter_string(out, food_mat), idx, pile_list.at(idx)); + } +} + +static bool serialize_list_color(color_ostream& out, FuncWriteExport add_value, const vector* colors) { + bool all = world->raws.descriptors.colors.size() == colors->size(); + if (!colors) { + DEBUG(log, out).print("serialize_list_color: list null\n"); + return all; + } + for (size_t i = 0; i < colors->size(); ++i) { + if (!colors->at(i)) { + all = false; + continue; + } + add_value(world->raws.descriptors.colors[i]->id); + } + return all; +} + +// Find the index of a named color, returning SIZE_MAX on unknown. +static size_t find_color_by_token(const string& token) { + auto& colors = world->raws.descriptors.colors; + size_t num_colors = colors.size(); + for (size_t idx = 0; idx < num_colors; ++ idx) { + if (colors[idx]->id == token) { + return idx; + } + } + return SIZE_MAX; +} + +static void unserialize_list_color(color_ostream& out, 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.descriptors.colors.size(); + pile_list.resize(num_elems, '\0'); + if (all) { + for (size_t idx = 0; idx < num_elems; ++idx) { + set_filter_elem(out, subcat, filters, val, world->raws.descriptors.colors[idx]->id, idx, pile_list[idx]); + } + return; + } + + for (int32_t idx = 0; idx < list_size; ++idx) { + const string& value = read_value(idx); + + size_t color = find_color_by_token(value); + if (color == SIZE_MAX) { + WARN(log, out).print("unknown color {}\n", value); + continue; + } + set_filter_elem(out, subcat, filters, val, value, color, pile_list[color]); } } @@ -491,7 +556,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo bool all = true; size_t num_item_types = list.size(); - DEBUG(log, out).print("item_type size = %zd size limit = %d typecasted: %zd\n", + DEBUG(log, out).print("item_type size = {} size limit = {} typecasted: {}\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) { @@ -504,7 +569,7 @@ static bool serialize_list_item_type(color_ostream& out, FuncItemAllowed is_allo if (!is_allowed(type)) continue; add_value(r_type); - DEBUG(log, out).print("item_type key_table[%zd] type[%d] is %s\n", i + 1, (int16_t)type, r_type.c_str()); + DEBUG(log, out).print("item_type key_table[{}] type[{}] is {}\n", i + 1, (int16_t)type, r_type); } return all; } @@ -528,15 +593,13 @@ static void unserialize_list_item_type(color_ostream& out, const char* subcat, b } using df::enums::item_type::item_type; - df::enum_traits type_traits; 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 df::enum_traits::base_type idx = token_to_enum_val(token); if (!is_allowed((item_type)idx)) continue; if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("error item_type index too large! idx[%d] max_size[%zd]\n", idx, num_elems); + WARN(log, out).print("error item_type index too large! idx[{}] max_size[{}]\n", idx, num_elems); continue; } set_filter_elem(out, subcat, filters, val, token, idx, pile_list.at(idx)); @@ -555,7 +618,7 @@ static bool serialize_list_material(color_ostream& out, FuncMaterialAllowed is_a mi.decode(0, i); if (!is_allowed(mi)) continue; - DEBUG(log, out).print("adding material %s\n", mi.getToken().c_str()); + DEBUG(log, out).print("adding material {}\n", mi.getToken()); add_value(mi.getToken()); } return all; @@ -566,7 +629,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo 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(); + size_t num_elems = world->raws.inorganics.all.size(); pile_list.resize(num_elems, 0); for (size_t i = 0; i < pile_list.size(); ++i) { MaterialInfo mi(0, i); @@ -589,7 +652,7 @@ static void unserialize_list_material(color_ostream& out, const char* subcat, bo if (!mi.find(id) || !is_allowed(mi)) continue; if (mi.index < 0 || size_t(mi.index) >= pile_list.size()) { - WARN(log, out).print("material type index invalid: %d\n", mi.index); + WARN(log, out).print("material type index invalid: {}\n", mi.index); continue; } set_filter_elem(out, subcat, filters, val, id, mi.index, pile_list.at(mi.index)); @@ -608,7 +671,7 @@ static bool serialize_list_creature(color_ostream& out, FuncWriteExport add_valu if (r->flags.is_set(creature_raw_flags::GENERATED) || r->creature_id == "EQUIPMENT_WAGON") continue; - DEBUG(log, out).print("adding creature %s\n", r->creature_id.c_str()); + DEBUG(log, out).print("adding creature {}\n", r->creature_id); add_value(r->creature_id); } return all; @@ -638,7 +701,7 @@ static void unserialize_list_creature(color_ostream& out, const char* subcat, bo string id = read_value(i); int idx = find_creature(id); if (idx < 0 || size_t(idx) >= num_elems) { - WARN(log, out).print("animal index invalid: %d\n", idx); + WARN(log, out).print("animal index invalid: {}\n", idx); continue; } auto r = find_creature(idx); @@ -658,7 +721,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, T_cat_set* cat_set = mutable_cat_fn(); if (!include_types) { - DEBUG(log, out).print("including all for %s since only category is being recorded\n", name); + DEBUG(log, out).print("including all for {} since only category is being recorded\n", name); cat_set->set_all(true); return; } @@ -666,7 +729,7 @@ static void write_cat(color_ostream& out, const char* name, bool include_types, if (write_cat_fn(out, 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, out).print("including all for %s since all fields were enabled\n", name); + DEBUG(log, out).print("including all for {} since all fields were enabled\n", name); cat_set->Clear(); cat_set->set_all(true); } @@ -679,7 +742,7 @@ void StockpileSettingsSerializer::write(color_ostream& out, uint32_t includedEle if (!(includedElements & INCLUDED_ELEMENTS_CATEGORIES)) return; - DEBUG(log, out).print("GROUP SET %s\n", + DEBUG(log, out).print("GROUP SET {}\n", bitfield_to_string(mSettings->flags).c_str()); bool include_types = 0 != (includedElements & INCLUDED_ELEMENTS_TYPES); @@ -814,9 +877,9 @@ void StockpileSerializer::read(color_ostream &out, DeserializeMode mode, const v void StockpileSerializer::write_containers(color_ostream& out) { DEBUG(log, out).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); + mBuffer.set_max_bins(mPile->storage.max_bins); + mBuffer.set_max_barrels(mPile->storage.max_barrels); + mBuffer.set_max_wheelbarrows(mPile->storage.max_wheelbarrows); } template @@ -830,7 +893,7 @@ static void read_elem(color_ostream& out, const char* name, DeserializeMode mode 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, out).print("setting %s to %d\n", name, val); + DEBUG(log, out).print("setting {} to {}\n", name, val); setting = val; } } @@ -844,7 +907,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode std::function clear_fn, std::function set_fn) { if (mode == DESERIALIZE_MODE_SET) { - DEBUG(log, out).print("clearing %s\n", name); + DEBUG(log, out).print("clearing {}\n", name); cat_flags &= ~cat_mask; clear_fn(); } @@ -860,7 +923,7 @@ static void read_category(color_ostream& out, const char* name, DeserializeMode bool all = cat_fn().all(); char val = (mode == DESERIALIZE_MODE_DISABLE) ? (char)0 : (char)1; - DEBUG(log, out).print("setting %s %s elements to %d\n", + DEBUG(log, out).print("setting {} {} elements to {}\n", all ? "all" : "marked", name, val); set_fn(all, val); } @@ -869,21 +932,21 @@ void StockpileSerializer::read_containers(color_ostream& out, DeserializeMode mo read_elem(out, "max_bins", mode, std::bind(&StockpileSettings::has_max_bins, mBuffer), std::bind(&StockpileSettings::max_bins, mBuffer), - mPile->max_bins); + mPile->storage.max_bins); read_elem(out, "max_barrels", mode, std::bind(&StockpileSettings::has_max_barrels, mBuffer), std::bind(&StockpileSettings::max_barrels, mBuffer), - mPile->max_barrels); + mPile->storage.max_barrels); read_elem(out, "max_wheelbarrows", mode, std::bind(&StockpileSettings::has_max_wheelbarrows, mBuffer), std::bind(&StockpileSettings::max_wheelbarrows, mBuffer), - mPile->max_wheelbarrows); + mPile->storage.max_wheelbarrows); } void StockpileSettingsSerializer::write_general(color_ostream& out) { DEBUG(log, out).print("writing general settings\n"); - mBuffer.set_allow_inorganic(mSettings->allow_inorganic); - mBuffer.set_allow_organic(mSettings->allow_organic); + mBuffer.set_allow_inorganic(mSettings->misc.allow_inorganic); + mBuffer.set_allow_organic(mSettings->misc.allow_organic); } void StockpileSerializer::write_general(color_ostream& out) { @@ -895,11 +958,11 @@ void StockpileSettingsSerializer::read_general(color_ostream& out, DeserializeMo read_elem(out, "allow_inorganic", mode, std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), std::bind(&StockpileSettings::allow_inorganic, mBuffer), - mSettings->allow_inorganic); + mSettings->misc.allow_inorganic); read_elem(out, "allow_organic", mode, std::bind(&StockpileSettings::has_allow_organic, mBuffer), std::bind(&StockpileSettings::allow_organic, mBuffer), - mSettings->allow_organic); + mSettings->misc.allow_organic); } void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) { @@ -907,7 +970,7 @@ void StockpileSerializer::read_general(color_ostream& out, DeserializeMode mode) if (!mBuffer.has_use_links_only()) return; bool use_links_only = mBuffer.use_links_only(); - DEBUG(log, out).print("setting use_links_only to %d\n", use_links_only); + DEBUG(log, out).print("setting use_links_only to {}\n", use_links_only); mPile->stockpile_flag.bits.use_links_only = use_links_only; } @@ -928,7 +991,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin mSettings->ammo.mats) && all; if (mSettings->ammo.other_mats.size() > 2) { - WARN(log, out).print("ammo other materials > 2: %zd\n", + WARN(log, out).print("ammo other materials > 2: {}\n", mSettings->ammo.other_mats.size()); } @@ -941,7 +1004,7 @@ bool StockpileSettingsSerializer::write_ammo(color_ostream& out, StockpileSettin } const string token = i == 0 ? "WOOD" : "BONE"; ammo->add_other_mats(token); - DEBUG(log, out).print("other mats %zd is %s\n", i, token.c_str()); + DEBUG(log, out).print("other mats {} is {}\n", i, token); } all = serialize_list_quality(out, @@ -1048,10 +1111,12 @@ static bool armor_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSettings::ArmorSet* armor) { auto & parmor = mSettings->armor; - bool all = parmor.unusable && parmor.usable; + bool all = parmor.unusable && parmor.usable && parmor.dyed && parmor.undyed; armor->set_unusable(parmor.unusable); armor->set_usable(parmor.usable); + armor->set_dyed(parmor.dyed); + armor->set_undyed(parmor.undyed); // armor type all = serialize_list_itemdef(out, @@ -1114,6 +1179,9 @@ bool StockpileSettingsSerializer::write_armor(color_ostream& out, StockpileSetti all = serialize_list_quality(out, [&](const string& token) { armor->add_quality_total(token); }, parmor.quality_total) && all; + all = serialize_list_color(out, [&](const string& token) { armor->add_color(token); }, + &parmor.color) && all; + return all; } @@ -1137,6 +1205,9 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode parmor.mats.clear(); quality_clear(parmor.quality_core); quality_clear(parmor.quality_total); + parmor.dyed = false; + parmor.undyed = false; + parmor.color.clear(); }, [&](bool all, char val) { auto & barmor = mBuffer.armor(); @@ -1184,6 +1255,13 @@ void StockpileSettingsSerializer::read_armor(color_ostream& out, DeserializeMode unserialize_list_quality(out, "total", all, val, filters, [&](const size_t& idx) -> const string& { return barmor.quality_total(idx); }, barmor.quality_total_size(), parmor.quality_total); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return barmor.color(idx); }, + barmor.color_size(), parmor.color); + + set_flag(out, "dyed", filters, all, val, barmor.dyed(), parmor.dyed); + set_flag(out, "undyed", filters, all, val, barmor.undyed(), parmor.undyed); }); } @@ -1255,7 +1333,9 @@ void StockpileSettingsSerializer::read_bars_blocks(color_ostream& out, Deseriali } bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSettings::ClothSet* cloth) { - bool all = true; + bool all = mSettings->cloth.dyed && mSettings->cloth.undyed; + cloth->set_dyed(mSettings->cloth.dyed); + cloth->set_undyed(mSettings->cloth.undyed); all = serialize_list_organic_mat(out, [&](const string& token) { cloth->add_thread_silk(token); }, @@ -1289,6 +1369,10 @@ bool StockpileSettingsSerializer::write_cloth(color_ostream& out, StockpileSetti [&](const string& token) { cloth->add_cloth_metal(token); }, &mSettings->cloth.cloth_metal, organic_mat_category::MetalThread) && all; + all = serialize_list_color(out, + [&](const string& token) { cloth->add_color(token); }, + &mSettings->cloth.color) && all; + return all; } @@ -1308,6 +1392,9 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode pcloth.cloth_plant.clear(); pcloth.cloth_yarn.clear(); pcloth.cloth_metal.clear(); + pcloth.dyed = false; + pcloth.undyed = false; + pcloth.color.clear(); }, [&](bool all, char val) { auto & bcloth = mBuffer.cloth(); @@ -1343,6 +1430,13 @@ void StockpileSettingsSerializer::read_cloth(color_ostream& out, DeserializeMode unserialize_list_organic_mat(out, "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); + + unserialize_list_color(out, "cloth/color", all, val, filters, + [&](size_t idx) -> string { return bcloth.color(idx); }, + bcloth.color_size(), pcloth.color); + + set_flag(out, "dyed", filters, all, val, bcloth.dyed(), pcloth.dyed); + set_flag(out, "undyed", filters, all, val, bcloth.undyed(), pcloth.undyed); }); } @@ -1414,10 +1508,14 @@ static bool finished_goods_mat_is_allowed(const MaterialInfo& mi) { } bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, StockpileSettings::FinishedGoodsSet* finished_goods) { - bool all = serialize_list_item_type(out, + bool all = mSettings->finished_goods.dyed && mSettings->finished_goods.undyed; + finished_goods->set_dyed(mSettings->finished_goods.dyed); + finished_goods->set_undyed(mSettings->finished_goods.undyed); + + all = serialize_list_item_type(out, finished_goods_type_is_allowed, [&](const string& token) { finished_goods->add_type(token); }, - mSettings->finished_goods.type); + mSettings->finished_goods.type) && all; all = serialize_list_material(out, finished_goods_mat_is_allowed, @@ -1436,6 +1534,10 @@ bool StockpileSettingsSerializer::write_finished_goods(color_ostream& out, Stock [&](const string& token) { finished_goods->add_quality_total(token); }, mSettings->finished_goods.quality_total) && all; + all = serialize_list_color(out, + [&](const string& token) { finished_goods->add_color(token); }, + &mSettings->finished_goods.color) && all; + return all; } @@ -1452,6 +1554,9 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri pfinished_goods.mats.clear(); quality_clear(pfinished_goods.quality_core); quality_clear(pfinished_goods.quality_total); + pfinished_goods.dyed = false; + pfinished_goods.undyed = false; + pfinished_goods.color.clear(); }, [&](bool all, char val) { auto & bfinished_goods = mBuffer.finished_goods(); @@ -1475,6 +1580,13 @@ void StockpileSettingsSerializer::read_finished_goods(color_ostream& out, Deseri unserialize_list_quality(out, "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); + + unserialize_list_color(out, "color", all, val, filters, + [&](const size_t& idx) -> const string& { return bfinished_goods.color(idx); }, + bfinished_goods.color_size(), pfinished_goods.color); + + set_flag(out, "dyed", filters, all, val, bfinished_goods.dyed(), pfinished_goods.dyed); + set_flag(out, "undyed", filters, all, val, bfinished_goods.undyed(), pfinished_goods.undyed); }); } @@ -1720,7 +1832,6 @@ static bool furniture_mat_is_allowed(const MaterialInfo& mi) { bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileSettings::FurnitureSet* furniture) { using df::enums::furniture_type::furniture_type; - using type_traits = df::enum_traits; auto & pfurniture = mSettings->furniture; bool all = true; @@ -1730,9 +1841,9 @@ bool StockpileSettingsSerializer::write_furniture(color_ostream& out, StockpileS all = false; continue; } - string f_type(type_traits::key_table[i]); + string f_type{ENUM_KEY_STR(furniture_type, furniture_type(i))}; furniture->add_type(f_type); - DEBUG(log, out).print("furniture_type %zd is %s\n", i, f_type.c_str()); + DEBUG(log, out).print("furniture_type {} is {}\n", i, f_type); } all = serialize_list_material(out, @@ -1783,9 +1894,9 @@ void StockpileSettingsSerializer::read_furniture(color_ostream& out, Deserialize } 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); + df::enum_traits::base_type idx = token_to_enum_val(token); if (idx < 0 || size_t(idx) >= pfurniture.type.size()) { - WARN(log, out).print("furniture type index invalid %s, idx=%d\n", token.c_str(), idx); + WARN(log, out).print("furniture type index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "type", filters, val, token, idx, pfurniture.type.at(idx)); @@ -1845,7 +1956,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(i, -1); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem rough_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem rough_other mat {} is {}\n", i, mi.getToken()); gems->add_rough_other_mats(mi.getToken()); } @@ -1859,7 +1970,7 @@ bool StockpileSettingsSerializer::write_gems(color_ostream& out, StockpileSettin mi.decode(0, i); if (!gem_other_mat_is_allowed(mi)) continue; - DEBUG(log, out).print("gem cut_other mat %zd is %s\n", i, mi.getToken().c_str()); + DEBUG(log, out).print("gem cut_other mat {} is {}\n", i, mi.getToken()); gems->add_cut_other_mats(mi.getToken()); } @@ -1873,24 +1984,24 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode std::bind(&StockpileSettings::gems, mBuffer), mSettings->flags.whole, mSettings->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(); + [&] (bool all, char val) { + auto& bgems = mBuffer.gems(); unserialize_list_material(out, "mats/rough", all, val, filters, gem_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, + [&] (const size_t& idx) -> const string& { return bgems.rough_mats(idx); }, bgems.rough_mats_size(), pgems.rough_mats); unserialize_list_material(out, "mats/cut", all, val, filters, gem_cut_mat_is_allowed, - [&](const size_t& idx) -> const string& { return bgems.cut_mats(idx); }, + [&] (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; + const size_t builtin_size = world->raws.mat_table.builtin.size(); pgems.rough_other_mats.resize(builtin_size, '\0'); pgems.cut_other_mats.resize(builtin_size, '\0'); if (all) { @@ -1924,9 +2035,17 @@ void StockpileSettingsSerializer::read_gems(color_ostream& out, DeserializeMode } bool StockpileSettingsSerializer::write_leather(color_ostream& out, StockpileSettings::LeatherSet* leather) { - return serialize_list_organic_mat(out, + bool all = mSettings->leather.dyed && mSettings->leather.undyed; + leather->set_dyed(mSettings->leather.dyed); + leather->set_undyed(mSettings->leather.undyed); + + all = serialize_list_organic_mat(out, [&](const string& id) { leather->add_mats(id); }, - &mSettings->leather.mats, organic_mat_category::Leather); + &mSettings->leather.mats, organic_mat_category::Leather) && all; + all = serialize_list_color(out, + [&](const string& id) { leather->add_color(id); }, + &mSettings->leather.color) && all; + return all; } void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMode mode, const vector& filters) { @@ -1938,6 +2057,9 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo mSettings->flags.mask_leather, [&]() { pleather.mats.clear(); + pleather.color.clear(); + pleather.dyed = false; + pleather.undyed = false; }, [&](bool all, char val) { auto & bleather = mBuffer.leather(); @@ -1945,6 +2067,12 @@ void StockpileSettingsSerializer::read_leather(color_ostream& out, DeserializeMo unserialize_list_organic_mat(out, "", all, val, filters, [&](size_t idx) -> string { return bleather.mats(idx); }, bleather.mats_size(), pleather.mats, organic_mat_category::Leather); + unserialize_list_color(out, "", all, val, filters, + [&](size_t idx) -> string { return bleather.color(idx); }, + bleather.color_size(), pleather.color); + + set_flag(out, "dyed", filters, all, val, bleather.dyed(), pleather.dyed); + set_flag(out, "undyed", filters, all, val, bleather.undyed(), pleather.undyed); }); } @@ -2256,7 +2384,7 @@ bool StockpileSettingsSerializer::write_wood(color_ostream& out, StockpileSettin if (!wood_mat_is_allowed(plant)) continue; wood->add_mats(plant->id); - DEBUG(log, out).print("plant %zd is %s\n", i, plant->id.c_str()); + DEBUG(log, out).print("plant {} is {}\n", i, plant->id); } return all; } @@ -2287,7 +2415,7 @@ void StockpileSettingsSerializer::read_wood(color_ostream& out, DeserializeMode const string token = bwood.mats(i); const size_t idx = find_plant(token); if (idx < 0 || (size_t)idx >= num_elems) { - WARN(log, out).print("wood mat index invalid %s idx=%zd\n", token.c_str(), idx); + WARN(log, out).print("wood mat index invalid {}, idx={}\n", token, idx); continue; } set_filter_elem(out, "", filters, val, token, idx, pwood.mats.at(idx)); diff --git a/plugins/stockpiles/StockpileUtils.h b/plugins/stockpiles/StockpileUtils.h index e78ffccd8c..4899001d5f 100644 --- a/plugins/stockpiles/StockpileUtils.h +++ b/plugins/stockpiles/StockpileUtils.h @@ -5,6 +5,7 @@ #include "df/world.h" #include "df/creature_raw.h" +#include "df/global_objects.h" #include "df/plant_raw.h" // Utility Functions {{{ diff --git a/plugins/stockpiles/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 681e7d9279..49a6d1c42e 100644 --- a/plugins/stockpiles/proto/stockpiles.proto +++ b/plugins/stockpiles/proto/stockpiles.proto @@ -100,10 +100,16 @@ message StockpileSettings { repeated string mats = 3; repeated string quality_core = 4; repeated string quality_total = 5; + optional bool dyed = 7; + optional bool undyed = 8; + repeated string color = 9; } message LeatherSet { optional bool all = 2; repeated string mats = 1; + optional bool dyed = 3; + optional bool undyed = 4; + repeated string color = 5; } message ClothSet { optional bool all = 9; @@ -115,6 +121,9 @@ message StockpileSettings { repeated string cloth_plant = 6; repeated string cloth_yarn = 7; repeated string cloth_metal = 8; + optional bool dyed = 10; + optional bool undyed = 11; + repeated string color = 12; } message WoodSet { optional bool all = 2; @@ -145,6 +154,9 @@ message StockpileSettings { repeated string quality_total = 10; optional bool usable = 11; optional bool unusable = 12; + optional bool dyed = 14; + optional bool undyed = 15; + repeated string color = 16; } message CorpsesSet { optional bool all = 1; diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 33ce855b42..f9df8d7367 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -9,6 +9,7 @@ #include "df/building.h" #include "df/building_stockpilest.h" +#include "df/global_objects.h" #include "df/hauling_route.h" #include "df/hauling_stop.h" @@ -29,7 +30,7 @@ DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); static command_result do_command(color_ostream& out, vector& parameters); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { - DEBUG(log, out).print("initializing %s\n", plugin_name); + DEBUG(log, out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -62,7 +63,7 @@ static df::building_stockpilest* get_stockpile(int 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); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -72,12 +73,12 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t try { StockpileSerializer cereal(sp); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -87,7 +88,7 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t 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("Specified building isn't a stockpile: %d.\n", id); + out.printerr("Specified building isn't a stockpile: {}\n", id); return false; } @@ -95,7 +96,7 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -111,12 +112,12 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m try { StockpileSerializer cereal(sp); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -126,13 +127,13 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m static df::stockpile_settings * get_stop_settings(color_ostream& out, int route_id, int stop_id) { auto route = df::hauling_route::find(route_id); if (!route) { - out.printerr("Specified hauling route not found: %d.\n", route_id); + out.printerr("Specified hauling route not found: {}\n", route_id); return NULL; } df::hauling_stop *stop = binsearch_in_vector(route->stops, &df::hauling_stop::id, stop_id); if (!stop) { - out.printerr("Specified hauling stop not found in route %d: %d.\n", route_id, stop_id); + out.printerr("Specified hauling stop not found in route {}: {}.\n", route_id, stop_id); return NULL; } @@ -150,12 +151,12 @@ static bool stockpiles_route_export(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.serialize_to_file(out, fname, includedElements)) { - out.printerr("could not save to '%s'\n", fname.c_str()); + out.printerr("could not save to '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("serialization failed: protobuf exception: %s\n", e.what()); + out.printerr("serialization failed: protobuf exception: {}\n", e.what()); return false; } @@ -171,7 +172,7 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ fname += ".dfstock"; if (!Filesystem::exists(fname)) { - out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str()); + out.printerr("ERROR: file doesn't exist: '{}'\n", fname); return false; } @@ -187,12 +188,12 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_ try { StockpileSettingsSerializer cereal(settings); if (!cereal.unserialize_from_file(out, fname, mode, filters)) { - out.printerr("deserialization failed: '%s'\n", fname.c_str()); + out.printerr("deserialization failed: '{}'\n", fname); return false; } } catch (std::exception& e) { - out.printerr("deserialization failed: protobuf exception: %s\n", e.what()); + out.printerr("deserialization failed: protobuf exception: {}\n", e.what()); return false; } diff --git a/plugins/stonesense b/plugins/stonesense index 9d851db080..b71216ddfc 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 +Subproject commit b71216ddfc54e20ad48fb26f9ba1d82c7656b6ec diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 4955d49e96..403dfaa6e9 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -1,8 +1,10 @@ // Triggers a strange mood using (mostly) the same logic used in-game +// Last updated 2026/09/17 against version 53.16 #include "Console.h" #include "DataDefs.h" #include "Export.h" +#include "MiscUtils.h" #include "PluginManager.h" #include "modules/Gui.h" @@ -21,6 +23,7 @@ #include "df/entity_raw.h" #include "df/general_ref_unit_workerst.h" #include "df/historical_entity.h" +#include "df/history_event_change_hf_moodst.h" #include "df/inorganic_raw.h" #include "df/item.h" #include "df/job.h" @@ -149,7 +152,7 @@ int getCreatedMetalBars (int32_t idx) command_result df_strangemood (color_ostream &out, vector & parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -164,6 +167,24 @@ command_result df_strangemood (color_ostream &out, vector & parameters) return CR_WRONG_USAGE; else if(parameters[i] == "--force") force = true; + else if(parameters[i] == "--id") + { + i++; + if (i == parameters.size()) + { + out.printerr("No unit id specified!\n"); + return CR_WRONG_USAGE; + } + int id = string_to_int(parameters[i], -1); + if (id < 0) + { + out.printerr("Invalid unit id: '{}'\n", parameters[i]); + return CR_WRONG_USAGE; + } + unit = df::unit::find(id); + if (!unit) + return CR_FAILURE; + } else if(parameters[i] == "--unit") { unit = DFHack::Gui::getSelectedUnit(out); @@ -190,7 +211,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) type = mood_type::Macabre; else { - out.printerr("Mood type '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood type '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -248,13 +269,13 @@ command_result df_strangemood (color_ostream &out, vector & parameters) skill = job_skill::MECHANICS; else { - out.printerr("Mood skill '%s' not recognized!\n", parameters[i].c_str()); + out.printerr("Mood skill '{}' not recognized!\n", parameters[i]); return CR_WRONG_USAGE; } } else { - out.printerr("Unrecognized parameter: %s\n", parameters[i].c_str()); + out.printerr("Unrecognized parameter: {}\n", parameters[i]); return CR_WRONG_USAGE; } } @@ -416,7 +437,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) if (unit->job.current_job) { // TODO: cancel job - out.printerr("Chosen unit '%s' has active job, cannot start mood!\n", + out.printerr("Chosen unit '{}' has active job, cannot start mood!\n", DF2CONSOLE(Units::getReadableName(unit)).c_str()); return CR_FAILURE; } @@ -488,6 +509,32 @@ command_result df_strangemood (color_ostream &out, vector & parameters) unit->mood = type; unit->mood_copy = unit->mood; Gui::showAutoAnnouncement(announcement_type::STRANGE_MOOD, unit->pos, msg, color, bright); + unit->create_nemesis(0, 1, false); + + if (df::global::hist_event_next_id && df::global::cur_year && df::global::cur_year_tick) + { + auto histevent = df::allocate(); + if (!world->worldgen.worldgen_parms.reveal_all_history) + histevent->flags.set(df::history_event_flags::hidden); + histevent->id = (*df::global::hist_event_next_id)++; + histevent->reason = df::history_event_reason::none; + histevent->histfig = unit->hist_figure_id; + histevent->mood = unit->mood; + histevent->site = plotinfo->site_id; + if (auto cur_site = df::world_site::find(plotinfo->site_id)) + { + histevent->region_pos.x = cur_site->pos.x; + histevent->region_pos.y = cur_site->pos.y; + } + histevent->region = -1; + histevent->layer = -1; + histevent->year = *df::global::cur_year; + histevent->seconds = *df::global::cur_year_tick; + insert_into_vector(world->history.events, &df::history_event::id, (df::history_event *)histevent); + // TODO: add to active history event collections if necessary + // TODO: add to active mission report if necessary + histevent->add_to_classes(); + } // TODO: make sure unit drops any wrestle items unit->job.mood_timeout = 50000; @@ -644,8 +691,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && pref->mattype == builtin_mats::INORGANIC) { item->mat_type = pref->mattype; @@ -685,7 +732,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) filter = NULL; continue; } - if (filter->getMaterial() != 0) + if (filter->getMaterial() != builtin_mats::INORGANIC) { filter = NULL; continue; @@ -696,7 +743,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) continue; } MaterialInfo mat(filter->getMaterial(), filter->getMaterialIndex()); - if (!mat.inorganic->flags.is_set(inorganic_flags::DEEP_SPECIAL)) + if (!mat.inorganic || !mat.inorganic->flags.is_set(inorganic_flags::DEEP_SPECIAL)) { filter = NULL; continue; @@ -707,8 +754,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) { job->job_items.elements.push_back(item = new df::job_item()); item->item_type = item_type::CLOTH; - item->mat_type = filter->getMaterial(); - item->mat_index = filter->getMaterialIndex(); + item->mat_type = filter->getActualMaterial(); + item->mat_index = filter->getActualMaterialIndex(); item->quantity = base_item_count * 10000; item->min_dimension = 10000; } @@ -722,8 +769,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::SILK)) @@ -772,13 +819,18 @@ command_result df_strangemood (color_ostream &out, vector & parameters) filter = NULL; continue; } - if (filter->getMaterial() != 0) + if (filter->getMaterial() != builtin_mats::INORGANIC) { filter = NULL; continue; } MaterialInfo mat(filter->getMaterial(), filter->getMaterialIndex()); - if (!mat.inorganic->flags.is_set(inorganic_flags::DEEP_SPECIAL)) + if (!mat.material || !mat.material->flags.is_set(material_flags::IS_METAL)) + { + filter = NULL; + continue; + } + if (!mat.inorganic || !mat.inorganic->flags.is_set(inorganic_flags::DEEP_SPECIAL)) { filter = NULL; continue; @@ -789,8 +841,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) { job->job_items.elements.push_back(item = new df::job_item()); item->item_type = item_type::BAR; - item->mat_type = filter->getMaterial(); - item->mat_index = filter->getMaterialIndex(); + item->mat_type = filter->getActualMaterial(); + item->mat_index = filter->getActualMaterialIndex(); item->quantity = base_item_count * 150; // BUGFIX - the game does not adjust here! item->min_dimension = 150; } @@ -798,17 +850,21 @@ command_result df_strangemood (color_ostream &out, vector & parameters) { job->job_items.elements.push_back(item = new df::job_item()); item->item_type = item_type::BAR; - item->mat_type = 0; + item->mat_type = builtin_mats::INORGANIC; vector mats; if (soul) { for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && - pref->mattype == 0 && getCreatedMetalBars(pref->matindex) > 0) - mats.push_back(pref->matindex); + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && + pref->mattype == builtin_mats::INORGANIC) + { + MaterialInfo mat(pref->mattype, pref->matindex); + if (mat.material && mat.material->flags.is_set(material_flags::IS_METAL) && getCreatedMetalBars(pref->matindex)) + mats.push_back(pref->matindex); + } } } if (mats.size()) @@ -822,7 +878,7 @@ command_result df_strangemood (color_ostream &out, vector & parameters) case job_skill::ENCRUSTGEM: job->job_items.elements.push_back(item = new df::job_item()); item->item_type = item_type::ROUGH; - item->mat_type = 0; + item->mat_type = builtin_mats::INORGANIC; item->quantity = base_item_count; break; @@ -835,8 +891,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial && + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial && ((pref->mattype == builtin_mats::GLASS_GREEN) || (pref->mattype == builtin_mats::GLASS_CLEAR && have_glass[1]) || (pref->mattype == builtin_mats::GLASS_CRYSTAL && have_glass[2]))) @@ -867,8 +923,8 @@ command_result df_strangemood (color_ostream &out, vector & parameters) for (size_t i = 0; i < soul->preferences.size(); i++) { df::unit_preference *pref = soul->preferences[i]; - if (pref->active == 1 && - pref->type == df::unit_preference::T_type::LikeMaterial) + if (pref->flags.bits.visible && + pref->type == df::unitpref_type::LikeMaterial) { MaterialInfo mat(pref->mattype, pref->matindex); if (mat.material->flags.is_set(material_flags::BONE)) @@ -1069,15 +1125,23 @@ command_result df_strangemood (color_ostream &out, vector & parameters) item->mat_type = mat_type; item->flags2.whole = flags2.whole; item->quantity = 1; - if (item_type == item_type::BAR) + switch(item_type) { - item->quantity *= 150; // BUGFIX - the game does not adjust here! + case item_type::BAR: item->min_dimension = 150; - } - if (item_type == item_type::CLOTH) - { - item->quantity *= 10000; // BUGFIX - the game does not adjust here! + item->quantity *= 150; // BUGFIX - the game does not adjust here! + break; + case item_type::THREAD: // yes, the game actually checks this item type + item->min_dimension = 15000; + item->quantity *= 15000; // but it doesn't adjust quantity + break; + case item_type::CLOTH: item->min_dimension = 10000; + item->quantity *= 10000; // BUGFIX - the game does not adjust here! + break; + default: + // no adjustment + break; } } } diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 10c8d241f0..1560409af9 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -16,6 +16,9 @@ #include "df/item.h" #include "df/job.h" #include "df/job_item_ref.h" +#include "df/machine.h" +#include "df/machine_info.h" +#include "df/machine_nodest.h" #include "df/map_block.h" #include "df/tile_designation.h" #include "df/tile_occupancy.h" @@ -23,10 +26,12 @@ #include #include +#include #include #include #include #include +#include #include using std::string; @@ -79,6 +84,8 @@ enum Reason { UNSUPPORTED = 6, // Has an unmovable item on top of the building job ITEM_IN_JOB = 7, + // Removing the building would orphan machine components + DANGER_OF_COLLAPSE = 8, }; inline bool isExternalReason(Reason reason) { @@ -101,6 +108,8 @@ static string reasonToString(Reason reason) { return "Would collapse immediately"; case Reason::ITEM_IN_JOB: return "Blocked by an unmovable item"; + case Reason::DANGER_OF_COLLAPSE: + return "Would collapse machinery"; default: return "External reason"; } @@ -113,11 +122,13 @@ using df::coord; // impassible constructions static const std::bitset<64> construction_impassible = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification); // constructions requiring same support as walls static const std::bitset<64> construction_wall_support = std::bitset<64>() .set(construction_type::Wall) + .set(construction_type::ReinforcedWall) .set(construction_type::Fortification) .set(construction_type::UpStair) .set(construction_type::UpDownStair); @@ -387,10 +398,16 @@ class SuspendManager { if (isSuitableAccess(pos)) return true; - // if a wall is being constructed below, the tile will be suitable + // wall-type tiles can never become suitable + auto tile_type = Maps::getTileType(pos); + if (!tile_type || isWallTerrain(*tile_type)) + return false; + + // other tiles can become suitable if a wall is being constructed below auto below = Buildings::findAtTile(coord(pos.x,pos.y,pos.z-1)); if (below && below->getType() == df::building_type::Construction && - below->getSubtype() == construction_type::Wall) + (below->getSubtype() == construction_type::Wall || + below->getSubtype() == construction_type::ReinforcedWall)) return true; return false; @@ -463,7 +480,7 @@ class SuspendManager { static bool riskBlocking(color_ostream &out, df::job* job) { if (job->job_type != job_type::ConstructBuilding) return false; - TRACE(cycle,out).print("risk blocking: check construction job %d\n", job->id); + TRACE(cycle,out).print("risk blocking: check construction job {}\n", job->id); auto building = Job::getHolder(job); if (!building || !isImpassable(building)) @@ -476,7 +493,7 @@ class SuspendManager { return false; auto risk = riskOfStuckConstructionAt(pos); - TRACE(cycle,out).print(" risk is %d\n", risk); + TRACE(cycle,out).print(" risk is {}\n", risk); // TOTHINK: on a large grid, this will compute the same risk up to 5 times for (auto npos : neighbors | transform(around(pos))) { @@ -496,7 +513,7 @@ class SuspendManager { if (!building || building->getType() != df::building_type::Construction) return false; - TRACE(cycle,out).print("check (construction) construction job %d for support\n", job->id); + TRACE(cycle,out).print("check (construction) construction job {} for support\n", job->id); coord pos(building->centerx,building->centery,building->z); @@ -533,6 +550,73 @@ class SuspendManager { return true; } + // A machine node is anchored only if its entire footprint sits on + // non-open tiles. Nodes hanging over open space survive only through + // graph connectivity to an anchored node; the stability pass destroys + // any connected component with no anchor (verified in-game). + static bool isAnchoredToTerrain(df::building *bld) { + for (auto x = bld->x1; x <= bld->x2; ++x) { + for (auto y = bld->y1; y <= bld->y2; ++y) { + auto tile_type = Maps::getTileType(x, y, bld->z); + if (!tile_type || tileShape(*tile_type) == df::enums::tiletype_shape::EMPTY) + return false; + } + } + return true; + } + + // Would removing this building's node leave a connected component of + // its machine with no anchored node? + static bool wouldOrphanMachine(df::building *bld) { + auto machine_info = bld->getMachineInfo(); + if (!machine_info) + return false; + auto machine = df::machine::find(machine_info->machine_id); + if (!machine) + return false; + + auto &nodes = machine->components; + int target = -1; + for (size_t i = 0; i < nodes.size(); ++i) { + if (nodes[i] && nodes[i]->building_id == bld->id) { + target = i; + break; + } + } + if (target < 0 || nodes.size() == 1) + return false; + + // union-find over the remaining nodes + vector parent(nodes.size()); + std::iota(parent.begin(), parent.end(), 0); + std::function root = [&](int i) { + while (parent[i] != i) { + parent[i] = parent[parent[i]]; + i = parent[i]; + } + return i; + }; + for (size_t i = 0; i < nodes.size(); ++i) { + if ((int)i == target || !nodes[i]) + continue; + for (auto c : nodes[i]->connections) { + if (c < 0 || c >= (int)nodes.size() || c == target || !nodes[c]) + continue; + parent[root(i)] = root(c); + } + } + + std::unordered_map anchored; + for (size_t i = 0; i < nodes.size(); ++i) { + if ((int)i == target || !nodes[i]) + continue; + auto other = df::building::find(nodes[i]->building_id); + int r = root(i); + anchored[r] = anchored[r] || (other && isAnchoredToTerrain(other)); + } + return std::ranges::any_of(anchored, [](auto &p) { return !p.second; }); + } + void suspendBuilding(df::building *building, Reason reason){ for (auto job : building->jobs) if (job->job_type == df::job_type::ConstructBuilding) @@ -639,7 +723,7 @@ class SuspendManager { void refresh(color_ostream &out) { - DEBUG(cycle,out).print("starting refresh, prevent blocking is %s\n", + DEBUG(cycle,out).print("starting refresh, prevent blocking is {}\n", prevent_blocking ? "true" : "false"); suspensions.clear(); leadsToDeadend.clear(); @@ -649,6 +733,13 @@ class SuspendManager { // check carving/detailing jobs and suspend buildings over them preserveDesigations(job); + if (job->job_type == df::job_type::DestroyBuilding) { + auto building = Job::getHolder(job); + if (building && wouldOrphanMachine(building)) + suspensions[job->id] = Reason::DANGER_OF_COLLAPSE; + continue; + } + // remaining checks only apply to construction jobs if (!isConstructionJob(job)) continue; @@ -685,7 +776,7 @@ class SuspendManager { if (building && buildingOnDesignation(building)) suspensions[job->id]=Reason::ERASE_DESIGNATION; } - DEBUG(cycle,out).print("finished refresh: found %zu reasons for suspension\n",suspensions.size()); + DEBUG(cycle,out).print("finished refresh: found {} reasons for suspension\n",suspensions.size()); } void do_cycle (color_ostream &out, bool unsuspend_everything = false) @@ -700,7 +791,7 @@ class SuspendManager { Reason reason; - for (auto job : df::global::world->jobs.list | std::views::filter(isConstructionJob)) + for (auto job : df::global::world->jobs.list | std::views::filter(isSuspendableJob)) { if (job->flags.bits.suspend && !suspensions.contains(job->id)) { unsuspend(job); // suspended for no reason @@ -712,7 +803,7 @@ class SuspendManager { } } } - DEBUG(cycle,out).print("suspended %zu constructions and unsuspended %zu constructions\n", + DEBUG(cycle,out).print("suspended {} constructions and unsuspended {} constructions\n", num_suspend, num_unsuspend); } @@ -720,6 +811,10 @@ class SuspendManager { return job->job_type == job_type::ConstructBuilding; } + static bool isSuspendableJob(df::job *job) { + return isConstructionJob(job) || job->job_type == job_type::DestroyBuilding; + } + bool keptSuspended(df::job *job) { Reason reason; if (tryGetReason(job,reason) && !isExternalReason(reason)) @@ -764,7 +859,7 @@ static void do_cycle(color_ostream &out); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); suspendmanager_instance = std::make_unique(); eventhandler_instance = std::make_unique(plugin_self,jobCompletedHandler,1); @@ -785,13 +880,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector prevent_blocking = config.get_bool(CONFIG_PREVENT_BLOCKING); - DEBUG(control,out).print("loading persisted state: enabled is %s / prevent_blocking is %s\n", + DEBUG(control,out).print("loading persisted state: enabled is {} / prevent_blocking is {}\n", is_enabled ? "true" : "false", suspendmanager_instance->prevent_blocking ? "true" : "false"); if(is_enabled) { @@ -846,7 +941,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -862,16 +957,16 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.size() == 0) { if (!is_enabled) { - out.print("%s is disabled\n", plugin_name); + out.print("{} is disabled\n", plugin_name); } else { out.print( - "%s is enabled %s supending blocking jobs\n", plugin_name, + "{} is enabled {} suspending blocking jobs\n", plugin_name, suspendmanager_instance->prevent_blocking ? "and" : "but not"); } return CR_OK; @@ -882,25 +977,20 @@ static command_result do_command(color_ostream &out, vector ¶meters) return plugin_enable(out,true); } else if (parameters[0] == "disable") { return plugin_enable(out,false); - } else if (parameters[0] == "set" && parameters[1] == "preventblocking") { + } else if (parameters.size() == 3 && parameters[0] == "set" && parameters[1] == "preventblocking") { if (parameters[2] == "true") { suspendmanager_instance->prevent_blocking = true; config.set_bool(CONFIG_PREVENT_BLOCKING, true); - if (is_enabled) { - do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); - } - return CR_OK; } else if (parameters[2] == "false") { suspendmanager_instance->prevent_blocking = false; config.set_bool(CONFIG_PREVENT_BLOCKING, false); - if (is_enabled) { - do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); - } - return CR_OK; } else return CR_WRONG_USAGE; + if (is_enabled) { + do_cycle(out); + out.print("{}", suspendmanager_instance->getStatus(out)); + } + return CR_OK; } else { return CR_WRONG_USAGE; } @@ -915,7 +1005,7 @@ static void jobCompletedHandler(color_ostream& out, void* ptr) { TRACE(cycle,out).print("job completed/initiated handler called\n"); df::job* job = static_cast(ptr); if (SuspendManager::isConstructionJob(job)) { - DEBUG(cycle,out).print("construction job initiated/completed (tick: %d)\n", world->frame_counter); + DEBUG(cycle,out).print("construction job initiated/completed (tick: {})\n", world->frame_counter); cycle_needed = true; } @@ -930,7 +1020,7 @@ static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; cycle_needed = false; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); suspendmanager_instance->do_cycle(out); } diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index ad96302887..1018999ec4 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -2,6 +2,8 @@ * Tailor plugin. Automatically manages keeping your dorfs clothed. */ +#include + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -12,7 +14,9 @@ #include "modules/Units.h" #include "modules/World.h" +#include "df/building.h" #include "df/creature_raw.h" +#include "df/descriptor_color.h" #include "df/historical_entity.h" #include "df/item.h" #include "df/item_flags.h" @@ -25,6 +29,10 @@ #include "df/manager_order.h" #include "df/material.h" #include "df/plotinfost.h" +#include "df/reaction.h" +#include "df/reaction_reagent.h" +#include "df/reaction_reagent_itemst.h" +#include "df/reaction_product_itemst.h" #include "df/unit.h" #include "df/world.h" @@ -46,7 +54,9 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string CONFIG_KEY_2 = string(plugin_name) + "/config_2"; static PersistentDataItem config; +static PersistentDataItem config2; enum ConfigValues { CONFIG_IS_ENABLED = 0, @@ -58,6 +68,11 @@ enum ConfigValues { CONFIG_CONFISCATE = 6 }; +enum Config2Values +{ + CONFIG_AUTOMATE_DYE = 0 +}; + static const int32_t CYCLE_TICKS = 1231; // one day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle @@ -144,11 +159,17 @@ class Tailor { bool confiscate = true; + bool automate_dye = false; + public: void set_confiscate(bool f){ confiscate = f; } bool get_confiscate() { return confiscate; } + void set_automate_dye(bool f) { automate_dye = f; } + + bool get_automate_dye() { return automate_dye; } + void sync_material_order() { material_order.clear(); @@ -207,8 +228,8 @@ class Tailor { 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); + DEBUG(cycle).print("tailor: {} {} of size {} found\n", + i.second, ENUM_KEY_STR(item_type, t), size); } } } @@ -227,7 +248,7 @@ class Tailor { // only count dyed std::string d; i->getItemDescription(&d, 0); - TRACE(cycle).print("tailor: skipping undyed %s\n", DF2CONSOLE(d).c_str()); + TRACE(cycle).print("tailor: skipping undyed {}\n", DF2CONSOLE(d)); continue; } MaterialInfo mat(i); @@ -247,7 +268,7 @@ class Tailor { { std::string d; i->getItemDescription(&d, 0); - DEBUG(cycle).print("tailor: weird cloth item found: %s (%d)\n", DF2CONSOLE(d).c_str(), i->id); + DEBUG(cycle).print("tailor: weird cloth item found: {} ({})\n", DF2CONSOLE(d), i->id); } } } @@ -259,7 +280,7 @@ class Tailor { supply[M_LEATHER] += i->getStackSize(); } - DEBUG(cycle).print("tailor: available silk %d yarn %d cloth %d leather %d adamantine %d\n", + DEBUG(cycle).print("tailor: available silk {} yarn {} cloth {} leather {} adamantine {}\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER], supply[M_ADAMANTINE]); } @@ -276,7 +297,7 @@ class Tailor { for (auto inv : u->inventory) { - if (inv->mode != df::unit_inventory_item::Worn) + if (inv->mode != df::inv_item_role_type::Worn) continue; // skip non-clothing if (!inv->item->isClothing()) @@ -310,9 +331,9 @@ class Tailor { { if (ordered.count(ty) == 0) { - DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement\n", - DF2CONSOLE(description).c_str(), isize, - DF2CONSOLE(Units::getReadableName(u)).c_str(), usize); + DEBUG(cycle).print ("tailor: {} (size {}) worn by {} (size {}) needs replacement\n", + DF2CONSOLE(description), isize, + DF2CONSOLE(Units::getReadableName(u)), usize); needed[std::make_pair(ty, usize)] += 1; ordered.insert(ty); } @@ -325,10 +346,10 @@ class Tailor { bool confiscated = Items::setOwner(w, NULL); INFO(cycle).print( - "tailor: %s %s from %s.\n", + "tailor: {} {} from {}.\n", (confiscated ? "confiscated" : "could not confiscate"), - DF2CONSOLE(description).c_str(), - DF2CONSOLE(Units::getReadableName(u)).c_str() + DF2CONSOLE(description), + DF2CONSOLE(Units::getReadableName(u)) ); } @@ -342,10 +363,10 @@ class Tailor { { if (equipped.count(ty) == 0 && ordered.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(), + TRACE(cycle).print("tailor: one {} of size {} needed to cover {}\n", + ENUM_KEY_STR(item_type, ty), usize, - DF2CONSOLE(Units::getReadableName(u)).c_str()); + DF2CONSOLE(Units::getReadableName(u))); needed[std::make_pair(ty, usize)] += 1; } } @@ -402,7 +423,7 @@ class Tailor { { const df::job_type j = jj->second; orders[std::make_tuple(j, sub, size)] += count; - DEBUG(cycle).print("tailor: %s times %d of size %d ordered\n", ENUM_KEY_STR(job_type, j).c_str(), count, size); + DEBUG(cycle).print("tailor: {} times {} of size {} ordered\n", ENUM_KEY_STR(job_type, j), count, size); } } } @@ -415,56 +436,245 @@ class Tailor { if (f == jobTypeMap.end()) continue; - int race = o->specdata.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", - DF2CONSOLE(m.name).c_str(), o->amount_left); + TRACE(cycle).print("tailor: supply of {} reduced by {} due to being required for an existing order\n", + DF2CONSOLE(m.name), 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 + using jiqt = decltype(df::reaction_reagent_itemst::quantity); - int size = world->raws.creatures.all[race]->adultsize; + int get_reaction_max_count(df::reaction* r, jiqt lim = std::numeric_limits::max()) + { + jiqt max = lim; - auto tt = jobTypeMap.find(o->job_type); - if (tt == jobTypeMap.end()) + for (auto rr : r->reagents) + { + if (rr->getType() == df::reaction_reagent_type::item) { - continue; + max = std::min(get_reagent_max_count(virtual_cast(rr), max), max); + } + } + + return max; + } + + int get_reagent_max_count(df::reaction_reagent_itemst* r, jiqt lim = std::numeric_limits::max()) + { + df::reaction_reagent_itemst* t = df::allocate(); + *t = *r; + jiqt orig = t->quantity; + + std::vector tbc; + std::vector toc; + std::vector trc; + std::vector tcc; + + auto test = [&] (jiqt m) { + t->quantity = orig * m; + return t->have_enough_from_precalc_info(NULL, &tbc, &toc, &trc, &tcc, -1); + }; + + jiqt max = lim / orig; + + jiqt lo = 0; + jiqt hi = max; + + // exit fast when 0 + if (test(1)) + { + lo = 1; + + while (hi > lo + 1) + { + // can't do (hi+lo)/2 because hi+lo may overflow! + jiqt mult = hi / 2 + lo / 2; + if (mult == lo) mult += 1; + if (test(mult)) + lo = mult; + else + hi = mult; + } + } + + delete t; + + return lo; + } + + int count_dyeables(df::item_type ty) + { + auto reagent = df::allocate(); + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags2.bits.dyeable = true; + reagent->has_tool_use = df::tool_uses::NONE; + if (ty == df::item_type::SKIN_TANNED) + { + reagent->quantity = 1; + reagent->min_dimension = 1; + } + else + { + reagent->quantity = 10000; + reagent->min_dimension = 10000; + } + reagent->item_type = ty; + return get_reagent_max_count(reagent); + } + + int count_dyes() + { + auto reagent = df::allocate(); + reagent->item_type = df::item_type::NONE; + reagent->item_subtype = -1; + reagent->mat_type = -1; + reagent->mat_index = -1; + reagent->flags1.bits.unrotten = true; + reagent->flags2.bits.dye = true; + reagent->has_tool_use = df::tool_uses::NONE; + reagent->quantity = 1; + int count = get_reagent_max_count(reagent); + delete reagent; + return count; + } + + using oqt = decltype(df::manager_order::amount_total); + + using color_type = decltype(MaterialInfo::material->powder_dye); + + static auto product_is_dye (df::reaction_product* r) -> bool + { + if (r->getType() == df::reaction_product_type::item) + { + auto rr = virtual_cast (r); + auto mat = MaterialInfo(rr->mat_type, rr->mat_index); + return mat.material && mat.material->flags.is_set(df::enums::material_flags::IS_DYE); + } + return false; + }; + + oqt order_dye_from_reaction(df::reaction* r, int c = 1) + { + std::string descr; + auto dye = std::ranges::find_if(r->products, product_is_dye); + assert(dye != r->products.end()); + auto pp = virtual_cast(*dye); + assert(pp != nullptr); + + pp->getDescription(&descr); + + auto [_, n] = get_or_create_order(c, df::job_type::CustomReaction, -1, -1, 0, r->code); + if (n > 0) + { + INFO(cycle).print("tailor: ordered {} {}\n", c, DF2CONSOLE(descr)); + } + return n; + } + + oqt order_dye_cloth(int c = 1) + { + auto [_, n] = get_or_create_order(c, df::job_type::DyeCloth, -1, -1, 0); + return n; + } + + void make_dyes(int count) + { + auto reaction_produces_dye = [] (df::reaction* r) { + return std::ranges::any_of(r->products, product_is_dye); + }; + + for (auto r : std::ranges::views::filter(world->raws.reactions.reactions, reaction_produces_dye)) + { + int max = get_reaction_max_count(r,1); + + if (max > 0) + { + max = order_dye_from_reaction(r, max); } - 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(), - size); + count = std::max(0, count - max); + if (count <= 0) + break; } + } + + int count_dye_cloth_orders() + { + auto f = [] (df::manager_order* o) { + return o->job_type == df::job_type::DyeCloth; + }; + int sum = 0; + for (auto o : std::ranges::views::filter(world->manager_orders.all, f)) + { + sum += o->amount_left; + } + return sum; } - static df::manager_order * get_existing_order(df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat) { - for (auto order : world->manager_orders.all) { - if (order->job_type == ty && - order->item_type == df::item_type::NONE && - order->item_subtype == sub && - order->mat_type == -1 && - order->mat_index == -1 && - order->specdata.hist_figure_id == hfid && - order->material_category.whole == mcat.whole && - order->frequency == df::manager_order::T_frequency::OneTime) - return order; + static std::pair get_or_create_order(oqt c, df::job_type ty, int16_t sub, int32_t hfid, df::job_material_category mcat, std::string custom_reaction = "") + { + auto f = [&] (df::manager_order* order) { + return order->job_type == ty && + order->item_type == df::item_type::NONE && + order->item_subtype == sub && + order->mat_type == -1 && + order->mat_index == -1 && + order->specdata.race == hfid && + order->material_category.whole == mcat.whole && + order->frequency == df::workquota_frequency_type::OneTime && + order->reaction_name == custom_reaction; + }; + + auto orderIt = std::ranges::find_if(world->manager_orders.all, f); + + if (orderIt != world->manager_orders.all.end()) + { + auto o = *orderIt; + int chg = 0; + if (o->amount_left > 0) + { + int prev = o->amount_left; + o->amount_left = std::max(c, o->amount_left); + o->amount_total = std::max(c, o->amount_total); + chg = o->amount_left - prev; + } + return {o, chg}; } - return NULL; + + auto order = new df::manager_order; + order->job_type = ty; + order->item_type = df::item_type::NONE; + order->item_subtype = sub; + order->reaction_name = custom_reaction; + order->specdata.race = hfid; + order->material_category = mcat; + order->mat_type = -1; + order->mat_index = -1; + order->amount_left = c; + order->amount_total = c; + order->status.bits.validated = false; + order->status.bits.active = false; + order->frequency = df::workquota_frequency_type::OneTime; + order->id = world->manager_orders.manager_order_next_id++; + + world->manager_orders.all.push_back(order); + + return {order, c}; } int place_orders() { int ordered = 0; + int skipped = 0; auto entity = world->entities.all[plotinfo->civ_id]; for (auto& o : orders) @@ -478,7 +688,7 @@ class Tailor { if (sizes.count(size) == 0) { - WARN(cycle).print("tailor: cannot determine race for clothing of size %d, skipped\n", + WARN(cycle).print("tailor: cannot determine race for clothing of size {}, skipped\n", size); continue; } @@ -529,11 +739,11 @@ class Tailor { if (!can_make) { - INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", DF2CONSOLE(name_p).c_str()); + INFO(cycle).print("tailor: civilization cannot make {}, skipped\n", DF2CONSOLE(name_p)); continue; } - DEBUG(cycle).print("tailor: ordering %d %s\n", count, DF2CONSOLE(name_p).c_str()); + DEBUG(cycle).print("tailor: ordering {} {}\n", count, DF2CONSOLE(name_p)); for (auto& m : material_order) { @@ -548,54 +758,69 @@ class Tailor { if (supply[m] < count + res) { c = supply[m] - res; - TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", - count, c, DF2CONSOLE(m.name).c_str()); + TRACE(cycle).print("tailor: order reduced from {} to {} to protect reserves of {}\n", + count, c, DF2CONSOLE(m.name)); + skipped += (count - c); } supply[m] -= c; - auto order = get_existing_order(ty, sub, sizes[size], m.job_material); - if (order) { - if (order->amount_total > 0) { - order->amount_left += c; - order->amount_total += c; - } - } else { - order = new df::manager_order; - order->job_type = ty; - order->item_type = df::item_type::NONE; - order->item_subtype = sub; - order->mat_type = -1; - order->mat_index = -1; - order->amount_left = c; - order->amount_total = c; - order->status.bits.validated = false; - order->status.bits.active = false; - order->id = world->manager_orders.manager_order_next_id++; - order->specdata.hist_figure_id = sizes[size]; - order->material_category = m.job_material; - - world->manager_orders.all.push_back(order); - } - - INFO(cycle).print("tailor: added order #%d for %d %s %s, sized for %s\n", - order->id, - c, - bitfield_to_string(order->material_category).c_str(), - DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), - DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1]).c_str() - ); + auto [order,n] = get_or_create_order(c, ty, sub, sizes[size], m.job_material); - count -= c; - ordered += c; + if (n > 0) + { + INFO(cycle).print("tailor: added order #{} for {} {} {}, sized for {}\n", + order->id, + n, + bitfield_to_string(order->material_category), + DF2CONSOLE((c > 1) ? name_p : name_s), + DF2CONSOLE(world->raws.creatures.all[order->specdata.hist_figure_id]->name[1])); + + count -= n; + ordered += n; + } } else { - TRACE(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); + skipped += count; + DEBUG(cycle).print("tailor: material {} skipped due to lack of reserves, {} available\n", DF2CONSOLE(m.name), supply[m]); } } } } + + if (skipped > 0) + { + INFO(cycle).print("tailor: {} item{} not ordered due to a lack of materials\n", skipped, skipped != 1 ? "s" : ""); + + if (automate_dye) + { + int available_dyes = count_dyes(); + int available_dyeable_cloth = count_dyeables(df::item_type::CLOTH); + int dye_cloth_orders = count_dye_cloth_orders(); + + DEBUG(cycle).print("tailor: available dyes = {}, available dyeable cloth = {}, dye cloth orders = {}\n", + available_dyes, available_dyeable_cloth, dye_cloth_orders); + int to_dye = std::min(skipped, std::min(available_dyes, available_dyeable_cloth) - dye_cloth_orders); + DEBUG(cycle).print("tailor: to dye = {}\n", to_dye); + if (to_dye > 0) + { + int dyed = order_dye_cloth(to_dye); + if (dyed > 0) + { + INFO(cycle).print("tailor: dyeing {} cloth\n", to_dye); + } + } + + int dyes_to_make = available_dyes - to_dye; + if (dyes_to_make > 0) + { + INFO(cycle).print("tailor: ordering up to {} dyes\n", dyes_to_make); + make_dyes(dyes_to_make); + } + } + } + return ordered; } @@ -618,7 +843,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) static int do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); tailor_instance = std::make_unique(); @@ -633,19 +858,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector set_confiscate(config.get_bool(CONFIG_CONFISCATE)); - DEBUG(control,out).print("loading persisted confiscation state: %s\n", + DEBUG(control,out).print("loading persisted confiscation state: {}\n", tailor_instance->get_confiscate() ? "true" : "false"); + tailor_instance->set_automate_dye(config2.get_bool(CONFIG_AUTOMATE_DYE)); + DEBUG(control, out).print("loading persisted dye automation state: {}\n", + tailor_instance->get_automate_dye() ? "true" : "false"); + tailor_instance->sync_material_order(); return CR_OK; @@ -691,7 +933,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -703,14 +945,14 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { int ordered = do_cycle(out); if (0 < ordered) - out.print("tailor: ordered %d items of clothing\n", ordered); + out.print("tailor: ordered {} items of clothing\n", ordered); } return CR_OK; } static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -733,7 +975,7 @@ static int do_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); return tailor_instance->do_cycle(); } @@ -744,7 +986,7 @@ static int do_cycle(color_ostream &out) { static void tailor_doCycle(color_ostream &out) { DEBUG(control,out).print("entering tailor_doCycle\n"); - out.print("ordered %d items of clothing\n", do_cycle(out)); + out.print("ordered {} items of clothing\n", do_cycle(out)); } // remember, these are ONE-based indices from Lua @@ -766,7 +1008,7 @@ static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx, static void tailor_setConfiscate(color_ostream& out, bool enable) { - DEBUG(control,out).print("%s confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); + DEBUG(control,out).print("{} confiscation of tattered clothing \n", enable ? "enabling" : "disabling"); config.set_bool(CONFIG_CONFISCATE, enable); tailor_instance->set_confiscate(enable); } @@ -776,6 +1018,18 @@ static bool tailor_getConfiscate(color_ostream& out) return tailor_instance->get_confiscate(); } +static void tailor_setAutomateDye(color_ostream& out, bool enable) +{ + DEBUG(control, out).print("{} automation of dye\n", enable ? "enabling" : "disabling"); + config2.set_bool(CONFIG_AUTOMATE_DYE, enable); + tailor_instance->set_automate_dye(enable); +} + +static bool tailor_getAutomateDye(color_ostream& out) +{ + return tailor_instance->get_automate_dye(); +} + static int tailor_getMaterialPreferences(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) @@ -793,6 +1047,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), DFHACK_LUA_FUNCTION(tailor_setConfiscate), DFHACK_LUA_FUNCTION(tailor_getConfiscate), + DFHACK_LUA_FUNCTION(tailor_setAutomateDye), + DFHACK_LUA_FUNCTION(tailor_getAutomateDye), DFHACK_LUA_END }; diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp index 95c5bd4424..c0b30639a9 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -45,6 +45,11 @@ using std::set; #include "modules/MapCache.h" #include "modules/Maps.h" +#include "df/coord2d.h" +#include "df/map_block_column.h" +#include "df/plant.h" +#include "df/plant_tree_info.h" +#include "df/plant_type.h" #include "df/tile_dig_designation.h" #include "df/world.h" @@ -68,9 +73,9 @@ struct tiletypes_options { static struct_identity _identity; }; static const struct_field_info tiletypes_options_fields[] = { - { struct_field_info::PRIMITIVE, "help", offsetof(tiletypes_options, help), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "quiet", offsetof(tiletypes_options, quiet), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::SUBSTRUCT, "cursor", offsetof(tiletypes_options, cursor), &df::coord::_identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "help", offsetof(tiletypes_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "quiet", offsetof(tiletypes_options, quiet), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "cursor", offsetof(tiletypes_options, cursor), df::identity_traits::get(), 0, 0}, { struct_field_info::END } }; struct_identity tiletypes_options::_identity(sizeof(tiletypes_options), &df::allocator_fn, NULL, "tiletypes_options", NULL, tiletypes_options_fields); @@ -82,7 +87,8 @@ static const std::map, df::til }; static const uint16_t UNDERGROUND_TEMP = 10015; -static const char * HISTORY_FILE = "dfhack-config/tiletypes.history"; +static const std::filesystem::path get_history_file() { return Core::getInstance().getConfigPath() / "tiletypes.history"; } + CommandHistory tiletypes_hist; command_result df_tiletypes (color_ostream &out, vector & parameters); @@ -92,7 +98,7 @@ command_result df_tiletypes_here_point (color_ostream &out, vector & pa DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - tiletypes_hist.load(HISTORY_FILE); + tiletypes_hist.load(get_history_file()); commands.push_back(PluginCommand("tiletypes", "Paints tiles of specified types onto the map.", df_tiletypes, true, true)); commands.push_back(PluginCommand("tiletypes-command", "Run tiletypes commands (seperated by ' ; ')", df_tiletypes_command)); commands.push_back(PluginCommand("tiletypes-here", "Repeat tiletypes command at cursor (with brush)", df_tiletypes_here)); @@ -102,7 +108,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector tree_info) // grown tree; pos is the trunk base + return material == tiletype_material::TREE + || material == tiletype_material::ROOT + || material == tiletype_material::MUSHROOM; + if (plant->type == plant_type::DRY_TREE || plant->type == plant_type::WET_TREE) + // unsprouted tree; lives on a ground tile until it grows + return shape != tiletype_shape::NONE && shape != tiletype_shape::EMPTY + && material != tiletype_material::AIR; + // shrubs and saplings + return shape == tiletype_shape::SHRUB || shape == tiletype_shape::SAPLING; +} + +// delete a plant record, including tree_info for grown trees; the tree's +// remaining tiles are left as-is for the user to paint over or collapse +static void removePlant(df::plant *plant) +{ + vector *vec = nullptr; + switch (plant->type) + { // Remove from the per-type vector + case plant_type::DRY_TREE: vec = &world->plants.tree_dry; break; + case plant_type::WET_TREE: vec = &world->plants.tree_wet; break; + case plant_type::DRY_PLANT: vec = &world->plants.shrub_dry; break; + case plant_type::WET_PLANT: vec = &world->plants.shrub_wet; break; + } + if (vec) + std::erase(*vec, plant); + + auto col = Maps::getBlockColumn((plant->pos.x / 48) * 3, (plant->pos.y / 48) * 3); + if (col) + std::erase(col->plants, plant); + + if (plant->tree_info) + { + delete plant->tree_info->body; + delete plant->tree_info->roots; + delete plant->tree_info; + } + + std::erase(world->plants.all, plant); + delete plant; +} + +// remove the plant rooted at pos if the painted tile can no longer host it +static void removePlantIfUnsupported(const df::coord &pos) +{ + auto plant = Maps::getPlantAtTile(pos); + if (!plant) + return; + auto tt = Maps::getTileType(pos); + if (!tt || !plantTileSupported(plant, *tt)) + removePlant(plant); +} + static bool paintTileProcessing(MapExtras::Block* block, const df::coord2d& blockPos, const TileType& target) { df::tiletype source = block->tiletypeAt(blockPos); df::tile_designation des = block->DesignationAt(blockPos); @@ -1068,6 +1132,17 @@ static PaintResult paintArea(MapExtras::MapCache& map, const df::coord& pos1, co else if (target.aquifer > 0) Maps::setAreaAquifer(pos1, pos2, target.aquifer == 2, filter); + df::coord lo(std::min(pos1.x, pos2.x), std::min(pos1.y, pos2.y), std::min(pos1.z, pos2.z)); + df::coord hi(std::max(pos1.x, pos2.x), std::max(pos1.y, pos2.y), std::max(pos1.z, pos2.z)); + for (int16_t z = lo.z; z <= hi.z; z++) + for (int16_t x = lo.x; x <= hi.x; x++) + for (int16_t y = lo.y; y <= hi.y; y++) + { + df::coord pos(x, y, z); + if (filter(pos, nullptr)) + removePlantIfUnsupported(pos); + } + // force the game to recompute its walkability cache on the next tick world->reindex_pathfinding = true; } @@ -1114,6 +1189,8 @@ static PaintResult paintTile(MapExtras::MapCache &map, const df::coord &pos, else if (target.aquifer > 0) Maps::setTileAquifer(pos, target.aquifer == 2); + removePlantIfUnsupported(pos); + // force the game to recompute its walkability cache on the next tick world->reindex_pathfinding = true; } @@ -1156,7 +1233,7 @@ command_result executePaintJob(color_ostream &out, } if (!opts.quiet) - out.print("Cursor coords: (%d, %d, %d)\n", + out.print("Cursor coords: ({}, {}, {})\n", cursor.x, cursor.y, cursor.z); MapExtras::MapCache map; @@ -1196,9 +1273,9 @@ command_result executePaintJob(color_ostream &out, } if (failures > 0) - out.printerr("Could not update %d tiles of %zu.\n", failures, all_tiles.size()); + out.printerr("Could not update {} tiles of {}.\n", failures, all_tiles.size()); else if (!opts.quiet) - out.print("Processed %zu tiles.\n", all_tiles.size()); + out.print("Processed {} tiles.\n", all_tiles.size()); if (map.WriteAll()) { @@ -1399,57 +1476,57 @@ command_result df_tiletypes_here_point (color_ostream &out, vector & pa static bool setTile(color_ostream& out, df::coord pos, TileType target) { if (!Maps::isValidTilePos(pos)) { - out.printerr("Invalid map position: %d, %d, %d\n", pos.x, pos.y, pos.z); + out.printerr("Invalid map position: ({}, {}, {})\n", pos.x, pos.y, pos.z); return false; } if (!is_valid_enum_item(target.shape)) { - out.printerr("Invalid shape type: %d\n", target.shape); + out.printerr("Invalid shape type: {}\n", ENUM_AS_STR(target.shape)); return false; } if (!is_valid_enum_item(target.material)) { - out.printerr("Invalid material type: %d\n", target.material); + out.printerr("Invalid material type: {}\n", ENUM_AS_STR(target.material)); return false; } if (!is_valid_enum_item(target.special)) { - out.printerr("Invalid special type: %d\n", target.special); + out.printerr("Invalid special type: {}\n", ENUM_AS_STR(target.special)); return false; } if (!is_valid_enum_item(target.variant)) { - out.printerr("Invalid variant type: %d\n", target.variant); + out.printerr("Invalid variant type: {}\n", ENUM_AS_STR(target.variant)); return false; } if (target.hidden < -1 || target.hidden > 1) { - out.printerr("Invalid hidden value: %d\n", target.hidden); + out.printerr("Invalid hidden value: {}\n", target.hidden); return false; } if (target.light < -1 || target.light > 1) { - out.printerr("Invalid light value: %d\n", target.light); + out.printerr("Invalid light value: {}\n", target.light); return false; } if (target.subterranean < -1 || target.subterranean > 1) { - out.printerr("Invalid subterranean value: %d\n", target.subterranean); + out.printerr("Invalid subterranean value: {}\n", target.subterranean); return false; } if (target.skyview < -1 || target.skyview > 1) { - out.printerr("Invalid skyview value: %d\n", target.skyview); + out.printerr("Invalid skyview value: {}\n", target.skyview); return false; } if (target.aquifer < -1 || target.aquifer > 2) { - out.printerr("Invalid aquifer value: %d\n", target.aquifer); + out.printerr("Invalid aquifer value: {}\n", target.aquifer); return false; } if (target.autocorrect < 0 || target.autocorrect > 1) { - out.printerr("Invalid autocorrect value: %d\n", target.autocorrect); + out.printerr("Invalid autocorrect value: {}\n", target.autocorrect); return false; } if (target.material == df::tiletype_material::STONE) { if (target.stone_material != -1 && !isStoneInorganic(target.stone_material)) { - out.printerr("Invalid stone material: %d\n", target.stone_material); + out.printerr("Invalid stone material: {}\n", target.stone_material); return false; } if (!is_valid_enum_item(target.vein_type)) { - out.printerr("Invalid vein type: %d\n", target.vein_type); + out.printerr("Invalid vein type: {}\n", ENUM_AS_STR(target.vein_type)); return false; } } diff --git a/plugins/timestream.cpp b/plugins/timestream.cpp index a472c2efec..2742416760 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -32,12 +32,19 @@ #include "df/activity_event_worshipst.h" #include "df/building_nest_boxst.h" #include "df/building_trapst.h" +#include "df/buildingitemst.h" +#include "df/caravan_state.h" +#include "df/flow_info.h" +#include "df/flow_type.h" #include "df/init.h" #include "df/item_eggst.h" +#include "df/plotinfost.h" #include "df/unit.h" #include "df/world.h" #include +#include +#include using std::string; using std::vector; @@ -51,6 +58,8 @@ REQUIRE_GLOBAL(cur_year_tick); REQUIRE_GLOBAL(cur_year_tick_advmode); REQUIRE_GLOBAL(init); REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(flows); +REQUIRE_GLOBAL(plotinfo); namespace DFHack { // for configuration-related logging @@ -77,7 +86,7 @@ static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); + DEBUG(control,out).print("initializing {}\n", plugin_name); commands.push_back(PluginCommand( plugin_name, @@ -156,11 +165,12 @@ static void reset_ephemeral_state() { static void do_disable() { EventManager::unregisterAll(plugin_self); + is_enabled = false; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -168,7 +178,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", + DEBUG(control,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) { @@ -179,7 +189,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { do_disable(); } } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", + DEBUG(control,out).print("{} from the API, but already {}; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -187,7 +197,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_shutdown(color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); + DEBUG(control,out).print("shutting down {}\n", plugin_name); return CR_OK; } @@ -219,7 +229,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { if (config.get_bool(CONFIG_IS_ENABLED)) { plugin_enable(out, true); } - DEBUG(control,out).print("loading persisted enabled state: %s\n", + DEBUG(control,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -228,7 +238,7 @@ DFhackCExport command_result plugin_load_site_data(color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", + DEBUG(control,out).print("world unloaded; disabling {}\n", plugin_name); do_disable(); } @@ -244,7 +254,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } @@ -268,7 +278,7 @@ static void record_coverage(color_ostream &out) { if (coverage_slot >= NUM_COVERAGE_TICKS) return; if (!tick_coverage[coverage_slot]) { - DEBUG(cycle,out).print("recording coverage for slot: %u", coverage_slot); + DEBUG(cycle,out).print("recording coverage for slot: {}", coverage_slot); } tick_coverage[coverage_slot] = true; } @@ -312,12 +322,100 @@ static int32_t clamp_coverage(int32_t timeskip) { return timeskip; } +using df_tick_type = std::remove_reference_t; + +static df_tick_type flow_next_required_tick(int flow_index) +{ + using namespace df::enums::flow_type; + + const auto flow = (*flows)[flow_index]; + + if (flow == nullptr || flow->flags.bits.DEAD) + return std::numeric_limits::max(); + + const auto cur_tick = *cur_year_tick; + + struct update_parameters_t { + int speed; + int cycle; + }; + + const auto [speed, cycle] = [flow] () -> update_parameters_t { + switch (flow->type) + { + case ItemCloud: + case MaterialDust: + case MaterialGas: + case MaterialVapor: + if (flow->flags.bits.CREEPING) + return update_parameters_t{10, 100}; + else + return update_parameters_t{1, 5}; + case Dragonfire: + case Fire: + case Web: + return update_parameters_t{1, 3}; + default: + return update_parameters_t{10, 100}; + } + }(); + + const int stride = cycle / speed; + + const int phase = (flow_index % stride) * speed; + + const int cur_phase = cur_tick % cycle; + + return cur_tick + (phase - cur_phase) + ((cur_phase > phase) ? cycle : 0); +} + +static df_tick_type flows_next_required_tick() { + if (flows == nullptr || flows->empty()) + return std::numeric_limits::max(); + + auto flow_indices = std::views::iota(0, (int)flows->size()); + auto next_flow = std::ranges::min(flow_indices, {}, flow_next_required_tick); + + return flow_next_required_tick(next_flow); +} + +static bool detect_caravans() +{ + auto& caravans = df::global::plotinfo->caravans; + return std::any_of(caravans.begin(), caravans.end(), [](auto caravan) { + if (caravan->trade_state != df::caravan_state::T_trade_state::AtDepot) + return false; + auto car_civ = caravan->entity; + auto& units = world->units.active; + return std::any_of(units.begin(), units.end(), [car_civ] (auto un) { + return (un->civ_id == car_civ + && DFHack::Units::isMerchant(un) + && std::any_of(un->inventory.begin(), un->inventory.end(), [] (auto inv_item) { + return inv_item->item && inv_item->item->flags.bits.trader; + })); + }); + }); +} + +static bool breathing_difficulties() +{ + auto& units = df::global::world->units.all; + return std::any_of(units.begin(), units.end(), [] (auto u) { return Units::getBreathingState(u) != Units::breathing_state::FINE; }); +} + static int32_t clamp_timeskip(int32_t timeskip) { if (timeskip <= 0) return 0; + // timeskip cannot be applied when caravans are loading/unloading because we don't know how to jog that timer + if (detect_caravans()) + return 0; + // timeskip cannot be applied when any unit has breathing difficulties + if (breathing_difficulties()) + return 0; int32_t next_tick = *cur_year_tick + 1; timeskip = std::min(timeskip, get_next_trigger_year_tick(next_tick) - next_tick); timeskip = std::min(timeskip, get_next_birthday(next_tick) - next_tick); + timeskip = std::min(timeskip, flows_next_required_tick() - next_tick); return clamp_coverage(timeskip); } @@ -329,23 +427,21 @@ static void increment_counter(T *obj, FT T::*field, int32_t timeskip) { } template -static void decrement_counter(T *obj, FT T::*field, int32_t timeskip) { +static void decrement_counter(T *obj, FT T::*field, int32_t timeskip, int32_t floor = 1) { if (obj->*field <= 0) return; // TODO: check for overflow/underflow int32_t cur_val = static_cast(obj->*field); - obj->*field = static_cast(std::max(1, cur_val - timeskip)); + obj->*field = static_cast(std::max(floor, cur_val - timeskip)); } static void adjust_unit_counters(df::unit * unit, int32_t timeskip) { auto * c1 = &unit->counters; - decrement_counter(c1, &df::unit::T_counters::think_counter, timeskip); + decrement_counter(c1, &df::unit::T_counters::job_counter, timeskip); decrement_counter(c1, &df::unit::T_counters::swap_counter, timeskip); - decrement_counter(c1, &df::unit::T_counters::winded, timeskip); decrement_counter(c1, &df::unit::T_counters::stunned, timeskip); - decrement_counter(c1, &df::unit::T_counters::unconscious, timeskip); - decrement_counter(c1, &df::unit::T_counters::suffocation, timeskip); + decrement_counter(c1, &df::unit::T_counters::unconscious, timeskip, 2); decrement_counter(c1, &df::unit::T_counters::webbed, timeskip); decrement_counter(c1, &df::unit::T_counters::soldier_mood_countdown, timeskip); decrement_counter(c1, &df::unit::T_counters::pain, timeskip); @@ -409,7 +505,9 @@ static void adjust_activities(color_ostream &out, int32_t timeskip) { for (auto act : world->activities.all) { for (auto ev : act->events) { switch (ev->getType()) { - using namespace df::enums::activity_event_type; + using namespace df::enums::activity_event_type; + case NONE: + break; case TrainingSession: // no counters @@ -613,7 +711,7 @@ static void adjust_items(color_ostream &out, int32_t timeskip) { } static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + DEBUG(cycle,out).print("running {} cycle\n", plugin_name); // mark that we have recently run cycle_timestamp = world->frame_counter; @@ -644,7 +742,7 @@ static void do_cycle(color_ostream &out) { // don't let our deficit grow unbounded if we can never catch up timeskip_deficit = std::min(desired_timeskip - float(timeskip), 100.0F); - DEBUG(cycle,out).print("cur_year_tick: %d, real_fps: %d, timeskip: (%d, +%.2f)\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); + DEBUG(cycle,out).print("cur_year_tick: {}, real_fps: {}, timeskip: ({}, +{:.2f})\n", *cur_year_tick, real_fps, timeskip, timeskip_deficit); if (timeskip <= 0) return; @@ -666,7 +764,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto unit = df::unit::find(unit_id); if (!unit) return; - DEBUG(event,out).print("registering new unit %d (%s)\n", unit->id, Units::getReadableName(unit).c_str()); + DEBUG(event,out).print("registering new unit {} ({})\n", unit->id, Units::getReadableName(unit)); register_birthday(unit); } @@ -675,7 +773,7 @@ static void on_new_active_unit(color_ostream& out, void* data) { // static void timestream_setFps(color_ostream &out, int fps) { - DEBUG(cycle,out).print("timestream_setFps: %d\n", fps); + DEBUG(cycle,out).print("timestream_setFps: {}\n", fps); config.set_int(CONFIG_TARGET_FPS, clamp_fps_to_valid(fps)); } diff --git a/plugins/tubefill.cpp b/plugins/tubefill.cpp index 46f7101fab..cab0241bd4 100644 --- a/plugins/tubefill.cpp +++ b/plugins/tubefill.cpp @@ -116,6 +116,6 @@ command_result tubefill(color_ostream &out, std::vector & params) } } } - out.print("Found and changed %" PRId64 " tiles.\n", count); + out.print("Found and changed {} tiles.\n", count); return CR_OK; } diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 74276bba1c..eeed5027a8 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -14,7 +14,9 @@ using std::string; using namespace DFHack; #include "tweaks/adamantine-cloth-wear.h" +#include "tweaks/animaltrap-reuse.h" #include "tweaks/craft-age-wear.h" +#include "tweaks/drawbridge-tiles.h" #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" #include "tweaks/flask-contents.h" @@ -53,8 +55,12 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector ¶meters) { if (parameters.empty() || parameters[0] == "list") { out.print("tweaks:\n"); for (auto & entry : get_status()) - out.print(" %25s: %s\n", entry.first.c_str(), entry.second ? "enabled" : "disabled"); + out.print(" {:25}: {}\n", entry.first, entry.second ? "enabled" : "disabled"); return CR_OK; } diff --git a/plugins/tweak/tweaks/animaltrap-reuse.h b/plugins/tweak/tweaks/animaltrap-reuse.h new file mode 100644 index 0000000000..097e86e018 --- /dev/null +++ b/plugins/tweak/tweaks/animaltrap-reuse.h @@ -0,0 +1,84 @@ +#include "modules/Job.h" +#include "modules/Items.h" + +#include "df/building_animaltrapst.h" +#include "df/buildingitemst.h" +#include "df/general_ref_building_holderst.h" +#include "df/general_ref_contained_in_itemst.h" +#include "df/general_ref_contains_itemst.h" +#include "df/job.h" + +using namespace df::enums; + +struct animaltrap_reuse_hook : df::building_animaltrapst { + typedef df::building_animaltrapst interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, updateAction, ()) + { + // Skip processing if the trap isn't fully built + if (getBuildStage() != getMaxBuildStage()) return; + + // The first item is always the trap itself, and the second item (if any) is always either the Bait or the caught Vermin + if ((contained_items.size() > 1) && (contained_items[1]->item->getType() == df::item_type::VERMIN)) + { + auto trap = contained_items[0]->item; + auto vermin = contained_items[1]->item; + + // Make sure we don't already have a "Release Small Creature" job + for (size_t j = 0; j < jobs.size(); j++) + { + if (jobs[j]->job_type == df::job_type::ReleaseSmallCreature) + return; + // Also bail out if the player marked the building for destruction + if (jobs[j]->job_type == df::job_type::DestroyBuilding) + return; + } + + // Create the job + auto job = new df::job(); + Job::linkIntoWorld(job, true); + + job->job_type = df::job_type::ReleaseSmallCreature; + job->pos.x = centerx; + job->pos.y = centery; + job->pos.z = z; + + // Attach the vermin to the job + Job::attachJobItem(job, vermin, df::job_role_type::Hauled, -1, -1); + + // Link the job to the building + df::general_ref *ref = df::allocate(); + ref->setID(id); + job->general_refs.push_back(ref); + + jobs.push_back(job); + + // Hack: put the vermin inside the trap ITEM right away, otherwise the job will cancel. + // Normally, this doesn't happen until after the trap is deconstructed, but the game still + // seems to handle everything correctly and doesn't leave any bad references afterwards. + if (!Items::getContainer(vermin)) + { + // We can't use Items::moveToContainer, because that would remove it from the Building + // (and cause the game to no longer recognize the trap as being "full"). + // Instead, manually add the references and set the necessary bits. + + ref = df::allocate(); + ref->setID(trap->id); + vermin->general_refs.push_back(ref); + + ref = df::allocate(); + ref->setID(vermin->id); + trap->general_refs.push_back(ref); + + vermin->flags.bits.in_inventory = true; + trap->flags.bits.weight_computed = false; + // Don't set this flag here (even though it would normally get set), + // since the game doesn't clear it after the vermin gets taken out + // trap->flags.bits.container = true; + } + return; + } + INTERPOSE_NEXT(updateAction)(); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(animaltrap_reuse_hook, updateAction); diff --git a/plugins/tweak/tweaks/drawbridge-tiles.h b/plugins/tweak/tweaks/drawbridge-tiles.h new file mode 100644 index 0000000000..287c248924 --- /dev/null +++ b/plugins/tweak/tweaks/drawbridge-tiles.h @@ -0,0 +1,60 @@ +// Make raised drawbridge tiles indicate the bridge's direction + +#include "df/building_drawbuffer.h" +#include "df/building_bridgest.h" + +struct drawbridge_tiles_hook : df::building_bridgest { + typedef df::building_bridgest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (uint32_t curtick, df::building_drawbuffer *buf, int16_t z_offset)) + { + static const unsigned char tiles[4][3] = + { + { 0xB7, 0xB6, 0xBD }, + { 0xD6, 0xC7, 0xD3 }, + { 0xD4, 0xCF, 0xBE }, + { 0xD5, 0xD1, 0xB8 }, + }; + INTERPOSE_NEXT(drawBuilding)(curtick, buf, z_offset); + + // Only redraw "raised" drawbridges + if (!gate_flags.bits.raised || direction == -1) + return; + + // Figure out the extents and the axis + int p1, p2; + bool iy = false; + switch (direction) + { + case 0: // Left + case 1: // Right + p1 = buf->y1; p2 = buf->y2; iy = true; + break; + case 2: // Up + case 3: // Down + p1 = buf->x1; p2 = buf->x2; + break; + default: + // Already ignoring retracting above + return; + } + + int x = 0, y = 0; + if (p1 == p2) + buf->tile[0][0] = tiles[direction][1]; + else for (int p = p1; p <= p2; p++) + { + if (p == p1) + buf->tile[x][y] = tiles[direction][0]; + else if (p == p2) + buf->tile[x][y] = tiles[direction][2]; + else + buf->tile[x][y] = tiles[direction][1]; + if (iy) + y++; + else + x++; + } + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(drawbridge_tiles_hook, drawBuilding); diff --git a/plugins/work-now.cpp b/plugins/work-now.cpp index ca9f1ffe06..6844a23623 100644 --- a/plugins/work-now.cpp +++ b/plugins/work-now.cpp @@ -44,13 +44,13 @@ static void cleanup() { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; - DEBUG(log,out).print("%s from the API; persisting\n", + DEBUG(log,out).print("{} from the API; persisting\n", is_enabled ? "enabled" : "disabled"); config.set_bool(CONFIG_IS_ENABLED, is_enabled); if (enable) @@ -58,7 +58,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { else cleanup(); } else { - DEBUG(log,out).print("%s from the API, but already %s; no action\n", + DEBUG(log,out).print("{} from the API, but already {} ; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } @@ -81,7 +81,7 @@ DFhackCExport command_result plugin_load_site_data (color_ostream &out) { } is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(log,out).print("loading persisted enabled state: %s\n", + DEBUG(log,out).print("loading persisted enabled state: {}\n", is_enabled ? "true" : "false"); return CR_OK; @@ -98,7 +98,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan poke_idlers(); } else if (e == DFHack::SC_WORLD_UNLOADED) { if (is_enabled) { - DEBUG(log,out).print("world unloaded; disabling %s\n", + DEBUG(log,out).print("world unloaded; disabling {}\n", plugin_name); is_enabled = false; } @@ -109,12 +109,12 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static command_result work_now(color_ostream& out, vector& parameters) { if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); return CR_FAILURE; } if (parameters.empty() || parameters[0] == "status") { - out.print("work_now is %sactively poking idle dwarves.\n", is_enabled ? "" : "not "); + out.print("work_now is {}actively poking idle dwarves.\n", is_enabled ? "" : "not "); return CR_OK; } diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 2b1d91b826..24c3a67a3d 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -410,7 +410,7 @@ static void stop_protect(color_ostream &out) pending_recover.clear(); if (!known_jobs.empty()) - out.print("Unprotecting %zd jobs.\n", known_jobs.size()); + out.print("Unprotecting {} jobs.\n", known_jobs.size()); for (TKnownJobs::iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) delete it->second; @@ -437,7 +437,7 @@ static void start_protect(color_ostream &out) check_lost_jobs(out, 0); if (!known_jobs.empty()) - out.print("Protecting %zd jobs.\n", known_jobs.size()); + out.print("Protecting {} jobs.\n", known_jobs.size()); } static void init_state(color_ostream &out) @@ -456,7 +456,7 @@ static void init_state(color_ostream &out) if (get_constraint(out, items[i].val(), &items[i])) continue; - out.printerr("Lost constraint %s\n", items[i].val().c_str()); + out.printerr("Lost constraint {}\n", items[i].val()); World::DeletePersistentData(items[i]); } @@ -503,8 +503,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) pj->holder = df::building::find(pj->building_id); if (!pj->holder) { - out.printerr("Forgetting job %d (%s): holder building lost.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building lost.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -512,8 +512,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) // Check its state and postpone or cancel if invalid if (pj->holder->jobs.size() >= 10) { - out.printerr("Forgetting job %d (%s): holder building has too many jobs.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Forgetting job {} ({}): holder building has too many jobs.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); forget_job(out, pj); return true; } @@ -535,8 +535,8 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) { Job::deleteJobStruct(recovered); - out.printerr("Inconsistency: job %d (%s) already in list.\n", - pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type).c_str()); + out.printerr("Inconsistency: job {} ({}) already in list.\n", + pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); return true; } @@ -664,7 +664,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (tokens[0] == "ANY_CRAFT" || tokens[0] == "CRAFTS") { is_craft = true; } else if (!item.find(tokens[0]) || !item.isValid()) { - out.printerr("Cannot find item type: %s\n", tokens[0].c_str()); + out.printerr("Cannot find item type: {}\n", tokens[0]); return NULL; } @@ -674,7 +674,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str df::dfhack_material_category mat_mask; std::string maskstr = vector_get(tokens,1); if (!maskstr.empty() && !parseJobMaterialCategory(&mat_mask, maskstr)) { - out.printerr("Cannot decode material mask: %s\n", maskstr.c_str()); + out.printerr("Cannot decode material mask: {}\n", maskstr); return NULL; } @@ -684,7 +684,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str MaterialInfo material; std::string matstr = vector_get(tokens,2); if (!matstr.empty() && (!material.find(matstr) || !material.isValid())) { - out.printerr("Cannot find material: %s\n", matstr.c_str()); + out.printerr("Cannot find material: {}\n", matstr); return NULL; } @@ -692,7 +692,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str weight += (material.index >= 0 ? 5000 : 1000); if (mat_mask.whole && material.isValid() && !material.matches(mat_mask)) { - out.printerr("Material %s doesn't match mask %s\n", matstr.c_str(), maskstr.c_str()); + out.printerr("Material {} doesn't match mask {}\n", matstr, maskstr); return NULL; } @@ -724,7 +724,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (!found) { - out.printerr("Cannot parse token: %s\n", token.c_str()); + out.printerr("Cannot parse token: {}\n", token); return NULL; } } @@ -1184,9 +1184,9 @@ static void setJobResumed(color_ostream &out, ProtectedJob *pj, bool goal) if (goal != current) { - out.print("%s %s%s\n", + out.print("{} {}{}\n", (goal ? "Resuming" : "Suspending"), - shortJobDescription(pj->actual_job).c_str(), + shortJobDescription(pj->actual_job), (!goal || pj->isActuallyResumed() ? "" : " (delayed)")); } } @@ -1817,7 +1817,7 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (deleteConstraint(parameters[1])) return CR_OK; - out.printerr("Constraint not found: %s\n", parameters[1].c_str()); + out.printerr("Constraint not found: {}\n", parameters[1]); return CR_FAILURE; } else if (cmd == "unlimit-all") diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 612e042282..1e69b7e193 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -55,7 +55,6 @@ using std::vector; DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(ui_building_item_cursor); @@ -844,14 +843,15 @@ static void chainInfo(color_ostream & out, df::building* building, bool list_ref static df::building* getAssignableBuildingAtCursor(color_ostream& out) { // set building at cursor position to be new target building - if (cursor->x == -30000) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.printerr("No cursor; place cursor over activity zone, pen," " pasture, pit, pond, chain, or cage.\n"); return NULL; } - auto building_at_tile = Buildings::findAtTile(Gui::getCursorPos()); + auto building_at_tile = Buildings::findAtTile(pos); // cagezone wants a pen/pit as starting point if (isCage(building_at_tile)) @@ -861,7 +861,7 @@ static df::building* getAssignableBuildingAtCursor(color_ostream& out) } else { - auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); + auto zone_at_tile = Buildings::findPenPitAt(pos); if(!zone_at_tile) { out << "No pen/pasture, pit, or cage under cursor!" << endl; @@ -1069,7 +1069,8 @@ static command_result df_zone(color_ostream &out, vector & parameters) } else if(p0 == "zinfo") { - if (cursor->x == -30000) { + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) { out.color(COLOR_RED); out << "No cursor; place cursor over activity zone, chain, or cage." << endl; out.reset_color(); @@ -1081,10 +1082,10 @@ static command_result df_zone(color_ostream &out, vector & parameters) // (doesn't use the findXyzAtCursor() methods because zones might // overlap and contain a cage or chain) vector zones; - Buildings::findCivzonesAt(&zones, Gui::getCursorPos()); + Buildings::findCivzonesAt(&zones, pos); for (auto zone = zones.begin(); zone != zones.end(); ++zone) zoneInfo(out, *zone, verbose); - df::building* building = Buildings::findAtTile(Gui::getCursorPos()); + df::building* building = Buildings::findAtTile(pos); chainInfo(out, building, verbose); cageInfo(out, building, verbose); return CR_OK; diff --git a/scripts b/scripts index b1aa3b365d..d8685b1536 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 +Subproject commit d8685b153603aa39c03ad2500f4b0419b427804f diff --git a/test/core.lua b/test/core.lua index 58f139a8cb..7270f57067 100644 --- a/test/core.lua +++ b/test/core.lua @@ -1,8 +1,7 @@ config.target = 'core' local function clean_path(p) - -- todo: replace with dfhack.filesystem call? - return p:gsub('\\', '/'):gsub('//', '/'):gsub('/$', '') + return dfhack.filesystem.canonicalize(p) end local fs = dfhack.filesystem diff --git a/test/library/class_extended.lua b/test/library/class_extended.lua new file mode 100644 index 0000000000..a0c6db5338 --- /dev/null +++ b/test/library/class_extended.lua @@ -0,0 +1,296 @@ +config.target = 'core' + +function test.defclass_basic() + -- Test basic class creation + local MyClass = defclass(nil) + expect.eq(MyClass.__index, MyClass) + expect.table_eq(MyClass.ATTRS, {}) + expect.eq(MyClass.super, nil) + expect.ne(getmetatable(MyClass), nil) +end + +function test.defclass_with_parent() + -- Test class creation with parent + local ParentClass = defclass(nil) + local ChildClass = defclass(nil, ParentClass) + + expect.eq(ChildClass.super, ParentClass) + expect.eq(getmetatable(ChildClass).__index, ParentClass) +end + +function test.defclass_attrs() + -- Test class with ATTRS + local MyClass = defclass(nil) + MyClass.ATTRS { + attr1 = 'default1', + attr2 = 'default2', + } + + expect.eq(MyClass.ATTRS.attr1, 'default1') + expect.eq(MyClass.ATTRS.attr2, 'default2') +end + +function test.mkinstance() + -- Test basic instance creation + local MyClass = defclass(nil) + local instance = mkinstance(MyClass, {value = 100}) + + expect.eq(instance.value, 100) + expect.eq(getmetatable(instance), MyClass) +end + +function test.instance_inheritance() + -- Test instance method inheritance + local ParentClass = defclass(nil) + function ParentClass:parent_method() + return 'parent' + end + + local ChildClass = defclass(nil, ParentClass) + function ChildClass:child_method() + return 'child' + end + + local instance = ChildClass({}) + expect.eq(instance:parent_method(), 'parent') + expect.eq(instance:child_method(), 'child') +end + +function test.init_method() + -- Test init method + local MyClass = defclass(nil) + MyClass.ATTRS { + value = 0, + } + + function MyClass:init(init_table) + self.value = (init_table.value or 0) * 2 + end + + local instance = MyClass({value = 5}) + expect.eq(instance.value, 10) +end + +function test.preinit_postinit() + -- Test preinit and postinit methods + local call_order = {} + + local MyClass = defclass(nil) + function MyClass:preinit(init_table) + table.insert(call_order, 'preinit') + end + + function MyClass:init(init_table) + table.insert(call_order, 'init') + end + + function MyClass:postinit(init_table) + table.insert(call_order, 'postinit') + end + + MyClass({}) + + expect.eq(#call_order, 3) + expect.eq(call_order[1], 'preinit') + expect.eq(call_order[2], 'init') + expect.eq(call_order[3], 'postinit') +end + +function test.inheritance_init_order() + -- Test init order with inheritance + local call_order = {} + + local ParentClass = defclass(nil) + function ParentClass:preinit(init_table) + table.insert(call_order, 'parent_preinit') + end + + function ParentClass:init(init_table) + table.insert(call_order, 'parent_init') + end + + function ParentClass:postinit(init_table) + table.insert(call_order, 'parent_postinit') + end + + local ChildClass = defclass(nil, ParentClass) + function ChildClass:preinit(init_table) + table.insert(call_order, 'child_preinit') + end + + function ChildClass:init(init_table) + table.insert(call_order, 'child_init') + end + + function ChildClass:postinit(init_table) + table.insert(call_order, 'child_postinit') + end + + ChildClass({}) + + expect.eq(#call_order, 6) + expect.eq(call_order[1], 'child_preinit') + expect.eq(call_order[2], 'parent_preinit') + expect.eq(call_order[3], 'parent_init') + expect.eq(call_order[4], 'child_init') + expect.eq(call_order[5], 'parent_postinit') + expect.eq(call_order[6], 'child_postinit') +end + +function test.callback_method() + -- Test callback method + local MyClass = defclass(nil) + function MyClass:test_method(arg) + return arg * 2 + end + + local instance = MyClass({}) + local cb = instance:callback('test_method') + + expect.eq(cb(5), 10) +end + +function test.cb_getfield() + -- Test cb_getfield method + local MyClass = defclass(nil) + local instance = MyClass() + instance.value = 42 + + local getter = instance:cb_getfield('value') + expect.eq(getter(), 42) +end + +function test.cb_setfield() + -- Test cb_setfield method + local MyClass = defclass(nil) + local instance = MyClass() + instance.value = 42 + + local setter = instance:cb_setfield('value') + setter(100) + expect.eq(instance.value, 100) +end + +function test.assign_method() + -- Test assign method + local MyClass = defclass(nil) + local instance = MyClass({value = 1}) + + instance:assign({value = 2, new_field = 3}) + expect.eq(instance.value, 2) + expect.eq(instance.new_field, 3) +end + +-- function test.invoke_before() +-- -- Test invoke_before method +-- local call_order = {} + +-- local MyClass = defclass(nil) +-- function MyClass:test_method() +-- table.insert(call_order, 'original') +-- end + +-- function MyClass:invoke_before_test_method() +-- table.insert(call_order, 'before') +-- end + +-- local instance = MyClass({}) +-- instance:invoke_before('test_method') + +-- expect.eq(#call_order, 2) +-- expect.eq(call_order[1], 'before') +-- expect.eq(call_order[2], 'original') +-- end + +-- function test.invoke_after() +-- -- Test invoke_after method +-- local call_order = {} + +-- local MyClass = defclass(nil) +-- function MyClass:test_method() +-- table.insert(call_order, 'original') +-- end + +-- function MyClass:invoke_after_test_method() +-- table.insert(call_order, 'after') +-- end + +-- local instance = MyClass() +-- instance:invoke_after('test_method') + +-- expect.eq(#call_order, 2) +-- expect.eq(call_order[1], 'original') +-- expect.eq(call_order[2], 'after') +-- end + +function test.attrs_meta() + -- Test ATTRS metatable behavior + local MyClass = defclass(nil) + MyClass.ATTRS { + attr1 = 'value1', + attr2 = 'value2', + } + + -- Test that ATTRS can be called to add attributes + MyClass.ATTRS { + attr3 = 'value3', + } + + expect.eq(MyClass.ATTRS.attr1, 'value1') + expect.eq(MyClass.ATTRS.attr2, 'value2') + expect.eq(MyClass.ATTRS.attr3, 'value3') +end + +function test.default_nil() + -- Test DEFAULT_NIL behavior + local MyClass = defclass(nil) + MyClass.ATTRS { + optional = DEFAULT_NIL, + required = 'default', + } + + local instance1 = MyClass({}) + expect.eq(instance1.optional, nil) + expect.eq(instance1.required, 'default') + + local instance2 = MyClass({optional = 'provided'}) + expect.eq(instance2.optional, 'provided') + expect.eq(instance2.required, 'default') +end + +function test.class_reload() + -- Test that classes can be reloaded + local MyClass = defclass(nil) + MyClass.ATTRS { + version = 1, + } + + -- Simulate reload by updating ATTRS + MyClass.ATTRS { + version = 2, + new_attr = 'new', + } + + expect.eq(MyClass.ATTRS.version, 2) + expect.eq(MyClass.ATTRS.new_attr, 'new') +end + +function test.instance_field_access() + -- Test instance field access patterns + local MyClass = defclass(nil) + MyClass.ATTRS { + public_field = 'public', + } + + function MyClass:init(init_table) + self.private_field = 'private' + end + + local instance = MyClass({}) + expect.eq(instance.public_field, 'public') + expect.eq(instance.private_field, 'private') + + -- Test setting new fields + instance.dynamic_field = 'dynamic' + expect.eq(instance.dynamic_field, 'dynamic') +end diff --git a/test/library/dfhack_core.lua b/test/library/dfhack_core.lua new file mode 100644 index 0000000000..cdcadc4dce --- /dev/null +++ b/test/library/dfhack_core.lua @@ -0,0 +1,217 @@ +config.target = 'core' + +function test.safe_index() + -- Test basic indexing + local t = {a = 1, b = {c = 2}} + expect.eq(safe_index(t, 'a'), 1) + expect.eq(safe_index(t, 'b', 'c'), 2) + + -- Test nil handling + expect.eq(safe_index(nil, 'a'), nil) + expect.eq(safe_index(t, 'nonexistent'), nil) + expect.eq(safe_index(t, 'b', 'nonexistent'), nil) + + -- Test nested nil handling + expect.eq(safe_index(t, 'b', 'c', 'd'), nil) +end + +local function test_ensure_key() + local t = {} + + -- Test creating new key + local result = ensure_key(t, 'new_key') + expect.table_eq(result, {}) + expect.table_eq(t.new_key, {}) + + -- Test existing key + t.existing = {value = 1} + result = ensure_key(t, 'existing') + expect.eq(result, t.existing) + expect.eq(t.existing.value, 1) + + -- Test with default value + result = ensure_key(t, 'with_default', {default = true}) + expect.table_eq(result, {default = true}) +end + +function test.ensure_key() + test_ensure_key() +end + +-- local function test_ensure_keys() +-- local t = {} + +-- -- Test creating nested keys +-- local result = ensure_keys(t, 'level1', 'level2', 'level3') +-- expect.eq(result, t.level1.level2.level3) +-- expect.table_eq(t.level1, {}) +-- expect.table_eq(t.level1.level2, {}) +-- expect.table_eq(t.level1.level2.level3, {}) + +-- -- Test partial existing path +-- t.level1.level2.level3 = {existing = true} +-- result = ensure_keys(t, 'level1', 'level2', 'level3') +-- expect.eq(result, t.level1.level2.level3) +-- expect.true_(result.existing) +-- end +-- +-- function test.ensure_keys() +-- test_ensure_keys() +-- end + +local function test_copyall() + local t = {a = 1, b = 2, c = {nested = true}} + local copy = copyall(t) + + expect.table_eq(copy, t) + expect.ne(copy, t) -- Different table reference + expect.eq(copy.c, t.c) -- Shallow copy +end + +function test.copyall() + test_copyall() +end + +local function test_pos_functions() + -- Test pos2xyz + local pos = {x = 10, y = 20, z = 30} + local x, y, z = pos2xyz(pos) + expect.eq(x, 10) + expect.eq(y, 20) + expect.eq(z, 30) + + -- Test pos2xyz with invalid pos + local invalid_pos = {x = -30000} + expect.eq(pos2xyz(invalid_pos), nil) + + -- Test xyz2pos + local new_pos = xyz2pos(5, 10, 15) + expect.eq(new_pos.x, 5) + expect.eq(new_pos.y, 10) + expect.eq(new_pos.z, 15) + + -- Test xyz2pos with nil + local nil_pos = xyz2pos(nil, nil, nil) + expect.eq(nil_pos.x, -30000) + expect.eq(nil_pos.y, -30000) + expect.eq(nil_pos.z, -30000) + + -- Test same_xyz + expect.true_(same_xyz(pos, pos)) + expect.false_(same_xyz(pos, {x = 10, y = 20, z = 25})) + + -- Test pos2xy + local x2, y2 = pos2xy(pos) + expect.eq(x2, 10) + expect.eq(y2, 20) + + -- Test xy2pos + local pos2d = xy2pos(3, 7) + expect.eq(pos2d.x, 3) + expect.eq(pos2d.y, 7) + + -- Test same_xy + expect.true_(same_xy(pos, pos)) + expect.true_(same_xy(pos, {x = 10, y = 20, z = 99})) + expect.false_(same_xy(pos, {x = 10, y = 25, z = 30})) +end + +function test.pos_functions() + test_pos_functions() +end + +local function test_string_extensions() + -- Test startswith + expect.true_(("hello world"):startswith("hello")) + expect.false_(("hello world"):startswith("world")) + expect.true_(("test"):startswith("test")) + + -- Test endswith + expect.true_(("hello world"):endswith("world")) + expect.false_(("hello world"):endswith("hello")) + expect.true_(("test"):endswith("test")) + + -- Test split + local parts = ("a,b,c"):split(",") + expect.eq(parts[1], "a") + expect.eq(parts[2], "b") + expect.eq(parts[3], "c") + + -- Test split with default delimiter + local words = ("hello world test"):split() + expect.eq(words[1], "hello") + expect.eq(words[2], "world") + expect.eq(words[3], "test") + + -- Test trim + expect.eq((" hello "):trim(), "hello") + expect.eq(("\t\nworld\n\t"):trim(), "world") + + -- Test wrap + local wrapped = ("This is a long string that needs to be wrapped"):wrap(20) + expect.true_(#wrapped > 20) -- Should be multiple lines + + -- Test escape_pattern + local escaped = ("a+b*c"):escape_pattern() + expect.true_(escaped:find("%+")) + expect.true_(escaped:find("%*")) +end + +function test.string_extensions() + test_string_extensions() +end + +local function test_with_finalize() + local cleanup_called = false + local result + + dfhack.with_finalize( + function() cleanup_called = true end, + function() result = "success" end + ) + + expect.true_(cleanup_called) + expect.eq(result, "success") +end + +function test.with_finalize() + test_with_finalize() +end + +local function test_with_onerror() + local cleanup_called = false + local ok, err = pcall(function() + dfhack.with_onerror( + function() cleanup_called = true end, + function() error("test error") end + ) + end) + + expect.false_(ok) + expect.true_(cleanup_called) +end + +function test.with_onerror() + test_with_onerror() +end + +-- don't know how to test this function +-- local function test_mkmodule() +-- -- Test that mkmodule creates proper module structure +-- local test_module = mkmodule('test.temp_module') +-- expect.table_eq(test_module, {}) +-- end + +-- function test.mkmodule() +-- test_mkmodule() +-- end + +local function test_mkinstance() + local MyClass = defclass(nil) + local instance = mkinstance(MyClass, {value = 100}) + expect.eq(instance.value, 100) +end + +function test.mkinstance() + test_mkinstance() +end diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index a5748afd5f..5a7d006d59 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1655,6 +1655,18 @@ function test.select_all() screen:dismiss() end +function test.ignore_select_all_for_empty_text() + local text_area, screen, window = arrange_textarea({w=65}) + + expect.eq(read_rendered_text(text_area), '_'); + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), '_'); + + screen:dismiss() +end + function test.text_key_replace_selection() local text_area, screen, window = arrange_textarea({w=65}) @@ -1726,7 +1738,7 @@ function test.arrows_reset_selection() simulate_input_keys('CUSTOM_CTRL_A') - expect.eq(read_rendered_text(text_area), text .. '_'); + expect.eq(read_rendered_text(text_area), text); expect.eq(read_selected_text(text_area), text); @@ -1768,7 +1780,7 @@ function test.click_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1803,7 +1815,7 @@ function test.line_navigation_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1836,7 +1848,7 @@ function test.jump_begin_or_end_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -3057,7 +3069,7 @@ function test.fast_rewind_reset_selection() expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', + 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ diff --git a/test/library/gui_basics.lua b/test/library/gui_basics.lua new file mode 100644 index 0000000000..451199edc8 --- /dev/null +++ b/test/library/gui_basics.lua @@ -0,0 +1,289 @@ +config.target = 'core' + +local gui = require 'gui' + +function test.mkdims_xy() + local dims = gui.mkdims_xy(10, 20, 30, 40) + expect.eq(dims.x1, 10) + expect.eq(dims.y1, 20) + expect.eq(dims.x2, 30) + expect.eq(dims.y2, 40) + expect.eq(dims.width, 21) -- 30-10+1 + expect.eq(dims.height, 21) -- 40-20+1 +end + +function test.mkdims_wh() + local dims = gui.mkdims_wh(10, 20, 15, 25) + expect.eq(dims.x1, 10) + expect.eq(dims.y1, 20) + expect.eq(dims.x2, 24) -- 10+15-1 + expect.eq(dims.y2, 44) -- 20+25-1 + expect.eq(dims.width, 15) + expect.eq(dims.height, 25) +end + +function test.parse_inset() + -- Test with table + local l,t,r,b = gui.parse_inset({l = 5, r = 10, t = 3, b = 7}) + expect.eq(l, 5); + expect.eq(t, 3) + expect.eq(r, 10) + expect.eq(b, 7) + + -- Test with single value + local l,t,r,b = gui.parse_inset(8) + expect.eq(l, 8) + expect.eq(t, 8) + expect.eq(r, 8) + expect.eq(b, 8) + + -- Test with x/y shorthand + local l,t,r,b = gui.parse_inset({x = 4, y = 6}) + expect.eq(l, 4) + expect.eq(t, 6) + expect.eq(r, 4) + expect.eq(b, 6) +end + +function test.inset_frame() + local rect = gui.mkdims_wh(0, 0, 100, 50) + local inset = gui.inset_frame(rect, {l = 10, r = 10, t = 5, b = 5}) + + expect.eq(inset.x1, 10) + expect.eq(inset.y1, 5) + expect.eq(inset.x2, 89) -- 100-10-1 + expect.eq(inset.y2, 44) -- 50-5-1 + expect.eq(inset.width, 80) + expect.eq(inset.height, 40) +end + +function test.is_in_rect() + local rect = gui.mkdims_wh(10, 10, 20, 20) + + expect.true_(gui.is_in_rect(rect, 15, 15)) + expect.true_(gui.is_in_rect(rect, 10, 10)) + expect.true_(gui.is_in_rect(rect, 29, 29)) + expect.false_(gui.is_in_rect(rect, 9, 15)) + expect.false_(gui.is_in_rect(rect, 15, 9)) + expect.false_(gui.is_in_rect(rect, 30, 15)) + expect.false_(gui.is_in_rect(rect, 15, 30)) +end + +function test.ViewRect_basic() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(0, 0, 50, 30)} + + expect.eq(view_rect.x1, 0) + expect.eq(view_rect.y1, 0) + expect.eq(view_rect.x2, 49) + expect.eq(view_rect.y2, 29) + expect.eq(view_rect.width, 50) + expect.eq(view_rect.height, 30) +end + +function test.ViewRect_isDefunct() + local valid_rect = gui.ViewRect{rect = gui.mkdims_wh(0, 0, 50, 30)} + expect.false_(valid_rect:isDefunct()) + + local defunct_rect = gui.ViewRect{rect = gui.mkdims_wh(0, 0, 0, 0)} + expect.true_(defunct_rect:isDefunct()) +end + +function test.ViewRect_inClipGlobalXY() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(10, 10, 20, 20)} + + expect.true_(view_rect:inClipGlobalXY(15, 15)) + expect.true_(view_rect:inClipGlobalXY(10, 10)) + expect.true_(view_rect:inClipGlobalXY(29, 29)) + expect.false_(view_rect:inClipGlobalXY(9, 15)) + expect.false_(view_rect:inClipGlobalXY(15, 30)) +end + +function test.ViewRect_inClipLocalXY() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(10, 10, 20, 20)} + + expect.true_(view_rect:inClipLocalXY(5, 5)) -- Global 15,15 + expect.true_(view_rect:inClipLocalXY(0, 0)) -- Global 10,10 + expect.false_(view_rect:inClipLocalXY(-1, 5)) -- Global 9,15 + expect.false_(view_rect:inClipLocalXY(5, 20)) -- Global 15,30 +end + +function test.ViewRect_localXY() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(10, 10, 20, 20)} + + local lx, ly = view_rect:localXY(15, 25) + expect.eq(lx, 5) + expect.eq(ly, 15) +end + +function test.ViewRect_globalXY() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(10, 10, 20, 20)} + + local gx, gy = view_rect:globalXY(5, 15) + expect.eq(gx, 15) + expect.eq(gy, 25) +end + +function test.ViewRect_viewport() + local view_rect = gui.ViewRect{rect = gui.mkdims_wh(0, 0, 100, 100)} + local viewport = view_rect:viewport(10, 10, 20, 20) + + expect.eq(viewport.x1, 10) + expect.eq(viewport.y1, 10) + expect.eq(viewport.x2, 29) + expect.eq(viewport.y2, 29) + expect.eq(viewport.width, 20) + expect.eq(viewport.height, 20) +end + +function test.Painter_basic() + local painter = gui.Painter{rect = gui.mkdims_wh(0, 0, 50, 30)} + + expect.eq(painter.x, 0) + expect.eq(painter.y, 0) + expect.true_(painter:isValidPos()) +end + +function test.Painter_cursor() + local painter = gui.Painter{rect = gui.mkdims_wh(10, 10, 50, 30)} + + local cx, cy = painter:cursor() + expect.eq(cx, 0) + expect.eq(cy, 0) + + painter:seek(5, 10) + cx, cy = painter:cursor() + expect.eq(cx, 5) + expect.eq(cy, 10) +end + +function test.Painter_seek() + local painter = gui.Painter{rect = gui.mkdims_wh(0, 0, 50, 30)} + + painter:seek(10, 20) + expect.eq(painter.x, 10) + expect.eq(painter.y, 20) + + painter:seek(5) -- Only x + expect.eq(painter.x, 5) + expect.eq(painter.y, 20) -- y unchanged +end + +function test.Painter_advance() + local painter = gui.Painter{rect = gui.mkdims_wh(0, 0, 50, 30)} + + painter:advance(10, 5) + expect.eq(painter.x, 10) + expect.eq(painter.y, 5) + + painter:advance(5) -- Only x + expect.eq(painter.x, 15) + expect.eq(painter.y, 5) -- y unchanged +end + +function test.Painter_newline() + local painter = gui.Painter{rect = gui.mkdims_wh(0, 0, 50, 30)} + + painter:seek(10, 5) + painter:newline(3) + + expect.eq(painter.x, 3) + expect.eq(painter.y, 6) +end + +function test.Painter_viewport() + local painter = gui.Painter{rect = gui.mkdims_wh(0, 0, 100, 100)} + local viewport_painter = painter:viewport(10, 10, 20, 20) + + expect.eq(viewport_painter.x, 10) + expect.eq(viewport_painter.y, 10) + expect.eq(viewport_painter.width, 20) + expect.eq(viewport_painter.height, 20) +end + +function test.View_basic() + local view = gui.View{} + + expect.table_eq(view.subviews, {}) + expect.eq(view.focus, false) + expect.eq(#view.focus_group, 1) +end + +function test.View_addviews() + local parent = gui.View{} + local child1 = gui.View{view_id = 'child1'} + local child2 = gui.View{view_id = 'child2'} + + parent:addviews({child1, child2}) + + expect.eq(#parent.subviews, 2) + expect.eq(parent.subviews.child1, child1) + expect.eq(parent.subviews.child2, child2) + expect.eq(child1.parent_view, parent) + expect.eq(child2.parent_view, parent) +end + +function test.View_getPreferredFocusState() + local view = gui.View{} + expect.false_(view:getPreferredFocusState()) +end + +function test.View_setFocus() + local view = gui.View{} + + expect.false_(view.focus) + view:setFocus(true) + expect.true_(view.focus) + view:setFocus(false) + expect.false_(view.focus) +end + +function test.View_assign() + local view = gui.View{} + view:assign({custom_field = 'value', another_field = 42}) + + expect.eq(view.custom_field, 'value') + expect.eq(view.another_field, 42) +end + +function test.View_callback() + local view = gui.View{} + function view:test_method(arg) + return arg * 2 + end + + local cb = view:callback('test_method') + expect.eq(cb(5), 10) +end + +function test.View_cb_getfield() + local view = gui.View{} + view.test_field = 'test_value' + + local getter = view:cb_getfield('test_field') + expect.eq(getter(), 'test_value') +end + +function test.View_cb_setfield() + local view = gui.View{} + view.test_field = 'initial' + + local setter = view:cb_setfield('test_field') + setter('updated') + expect.eq(view.test_field, 'updated') +end + +-- do not know how to test this function - it has no specified behavior and is not used +-- function test.compute_frame_rect() +-- local rect = gui.compute_frame_rect(100, 50, {w = 80, h = 40}) + +-- expect.eq(rect.width, 80) +-- expect.eq(rect.height, 40) +-- expect.ge(rect.x1, 0) +-- expect.ge(rect.y1, 0) +-- end + +function test.blink_visible() + -- Test that blink_visible returns boolean + local result = gui.blink_visible(100) + expect.eq(type(result), 'boolean') +end diff --git a/test/library/maps.lua b/test/library/maps.lua new file mode 100644 index 0000000000..50ed66bb2e --- /dev/null +++ b/test/library/maps.lua @@ -0,0 +1,247 @@ +config.mode = 'fortress' +config.target = 'core' + +local function first_block() + return df.global.world.map.map_blocks[0] +end + +local function block_bounds(block) + local p = block.map_pos + return {p.x, p.y, p.z, p.x + 15, p.y + 15, p.z} +end + +function test.getTileBlockCoord() + local block = first_block() + local p = block.map_pos + local pos = xyz2pos(p.x + 5, p.y + 7, p.z) + + local bx, by, bz = dfhack.maps.getTileBlockCoord(pos) + expect.eq(p.x // 16, bx) + expect.eq(p.y // 16, by) + -- z is a level, not scaled by the conversion + expect.eq(p.z, bz) + + -- x,y,z positional argument form works too + local bx2, by2, bz2 = dfhack.maps.getTileBlockCoord(pos.x, pos.y, pos.z) + expect.eq(bx, bx2) + expect.eq(by, by2) + expect.eq(bz, bz2) + + -- every tile in the block resolves to the same block coord, which agrees + -- with the block actually returned by the block lookups + for dx = 0, 15 do for dy = 0, 15 do + local tx, ty, tz = dfhack.maps.getTileBlockCoord(p.x + dx, p.y + dy, p.z) + expect.eq(bx, tx) + expect.eq(by, ty) + expect.eq(bz, tz) + end end + expect.eq(dfhack.maps.getTileBlock(pos), + dfhack.maps.getBlock(bx, by, bz)) +end + +function test.getBlockOrigin() + local block = first_block() + local p = block.map_pos + + local ox, oy, oz = dfhack.maps.getBlockOrigin(p.x // 16, p.y // 16, p.z) + expect.eq(p.x, ox) + expect.eq(p.y, oy) + -- z is a level, not scaled by the conversion + expect.eq(p.z, oz) +end + +function test.getTileBlockOffset() + local block = first_block() + local p = block.map_pos + local pos = xyz2pos(p.x + 5, p.y + 7, p.z) + + local tx, ty, tz = dfhack.maps.getTileBlockOffset(pos) + expect.eq(5, tx) + expect.eq(7, ty) + expect.eq(p.z, tz) + + -- block origin + offset reconstructs the tile position + local bx, by, bz = dfhack.maps.getTileBlockCoord(pos) + local ox, oy, oz = dfhack.maps.getBlockOrigin(bx, by, bz) + expect.eq(pos.x, ox + tx) + expect.eq(pos.y, oy + ty) + expect.eq(pos.z, oz + tz) +end + +function test.forEachTile_counts_tiles() + local block = first_block() + local res = dfhack.maps.forEachTile(block_bounds(block)) + expect.eq(256, res.scanned) + expect.eq(256, res.matched) + expect.eq(0, res.changed) + expect.eq(0, res.constructed) + expect.false_(res.aborted) +end + +function test.forEachTile_filters_by_attribute() + local block = first_block() + local want = 0 + for x = 0, 15 do for y = 0, 15 do + if df.tiletype.attrs[block.tiletype[x][y]].material == + df.tiletype_material.STONE then + want = want + 1 + end + end end + + local res = dfhack.maps.forEachTile(block_bounds(block), { + material = {df.tiletype_material.STONE}, + }) + expect.eq(want, res.matched) +end + +function test.forEachTile_filters_by_tiletype() + local block = first_block() + local tt = block.tiletype[0][0] + local want = 0 + for x = 0, 15 do for y = 0, 15 do + if block.tiletype[x][y] == tt then want = want + 1 end + end end + + local res = dfhack.maps.forEachTile(block_bounds(block), {tiletype = tt}) + expect.eq(want, res.matched) +end + +function test.forEachTile_set_tiletype() + local block = first_block() + local p = block.map_pos + local orig = block.tiletype[0][0] + return dfhack.with_finalize(function() + block.tiletype[0][0] = orig + end, function() + local res = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, + {set_tiletype = df.tiletype.OpenSpace}) + expect.eq(1, res.matched) + expect.eq(1, res.changed) + expect.eq(df.tiletype.OpenSpace, block.tiletype[0][0]) + end) +end + +function test.forEachTile_tiletype_map() + local block = first_block() + local p = block.map_pos + local orig = block.tiletype[0][0] + return dfhack.with_finalize(function() + block.tiletype[0][0] = orig + end, function() + -- a map entry for a different tiletype must not touch this tile + local res = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, + {set_tiletype = {[df.tiletype.ConstructedWallLRUD] = + df.tiletype.OpenSpace}}) + expect.eq(1, res.matched) + expect.eq(0, res.changed) + expect.eq(orig, block.tiletype[0][0]) + + -- matching key rewrites the tile + local res2 = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, + {set_tiletype = {[orig] = df.tiletype.OpenSpace}}) + expect.eq(1, res2.changed) + expect.eq(df.tiletype.OpenSpace, block.tiletype[0][0]) + end) +end + +function test.forEachTile_designation_action_and_predicate() + local block = first_block() + local p = block.map_pos + local des = block.designation[0][0] + local orig = des.hidden + return dfhack.with_finalize(function() + des.hidden = orig + end, function() + local res = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, + {designation = {hidden = not orig}}) + expect.eq(1, res.matched) + expect.eq(not orig, des.hidden) + + -- the same spec used as a predicate now matches + local res2 = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, + {designation = {hidden = not orig}}) + expect.eq(1, res2.matched) + end) +end + +function test.forEachTile_callback() + local block = first_block() + local seen = 0 + local res = dfhack.maps.forEachTile(block_bounds(block), nil, + function(x, y, z, block_arg, localx, localy, tt) + seen = seen + 1 + expect.eq(block.map_pos.x, block_arg.map_pos.x) + expect.eq(x % 16, localx) + expect.eq(y % 16, localy) + expect.eq(block.tiletype[localx][localy], tt) + end) + expect.eq(256, seen) + expect.eq(256, res.matched) +end + +function test.forEachTile_callback_abort() + local block = first_block() + local calls = 0 + local res = dfhack.maps.forEachTile(block_bounds(block), nil, { + callback = function() + calls = calls + 1 + return false + end, + }) + expect.eq(1, calls) + expect.eq(1, res.matched) + expect.true_(res.aborted) +end + +function test.forEachTile_construct() + local block = first_block() + local p = block.map_pos + local orig = block.tiletype[0][0] + local pos = xyz2pos(p.x, p.y, p.z) + return dfhack.with_finalize(function() + block.tiletype[0][0] = orig + -- findAtTile can't remove; drop the record from the vector directly + local vec = df.global.world.event.constructions + for i = 0, #vec - 1 do + local c = vec[i] + if c.pos.x == p.x and c.pos.y == p.y and c.pos.z == p.z then + vec:erase(i) + break + end + end + end, function() + local res = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, { + construct = { + item_type = df.item_type.BLOCKS, + mat_type = 0, + mat_index = 0, + tiletype = df.tiletype.ConstructedFloor, + flags = {no_build_item = true}, + }, + }) + expect.eq(1, res.constructed) + expect.eq(1, res.changed) + expect.eq(df.tiletype.ConstructedFloor, block.tiletype[0][0]) + + local con = dfhack.constructions.findAtTile(pos) + expect.ne(nil, con) + if con then + expect.eq(df.item_type.BLOCKS, con.item_type) + expect.eq(orig, con.original_tile) + expect.true_(con.flags.no_build_item) + end + + -- a second scan must not duplicate the construction + local res2 = dfhack.maps.forEachTile( + {p.x, p.y, p.z, p.x, p.y, p.z}, nil, { + construct = {item_type = df.item_type.BLOCKS, mat_type = 0, mat_index = 0}, + }) + expect.eq(0, res2.constructed) + end) +end diff --git a/test/library/print.lua b/test/library/print.lua index 28a2e70370..ea2244d3e8 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -47,7 +47,7 @@ end local function new_int_vector() -- create a vector of integers by cloning one from world. we do it this way -- because we can't allocate typed vectors from lua directly. - local vector = df.global.world.busy_buildings:new() + local vector = df.global.world.building_uses.buildings:new() vector:resize(0) return vector end @@ -190,6 +190,31 @@ function test.printall_recurse() end) end +function test.printall_recurse_untagged_union() + local t = df.army_controller:new() + dfhack.with_temp_object(t, function() + printall_recurse(t.data) + expect.eq(3, mock_print.call_count) + validate_patterns(1, + {'^ 0 then + unit = u + break + end + end + expect.ne(unit, nil) + + local oldpath = utils.clone(unit.path, true) + + local retval = dfhack.units.setPathGoal(unit, xyz2pos(1, 2, 3), df.unit_path_goal.None) + expect.nil_(retval) + expect.eq(df.unit_path_goal.None, unit.path.goal) + expect.table_eq(xyz2pos(1, 2, 3), utils.clone(unit.path.dest, true)) + expect.table_eq({ x = {}, y = {}, z = {} }, utils.clone(unit.path.path, true)) + + unit.path:assign(oldpath) + expect.eq(oldpath.goal, unit.path.goal) + expect.table_eq(oldpath.dest, utils.clone(unit.path.dest, true)) + expect.table_eq(oldpath.path, utils.clone(unit.path.path, true)) +end diff --git a/test/plugins/3dveins.lua b/test/plugins/3dveins.lua new file mode 100644 index 0000000000..406d6817e9 --- /dev/null +++ b/test/plugins/3dveins.lua @@ -0,0 +1,18 @@ +config.mode = 'fortress' +config.target = '3dveins' + +function test.rewrites_veins() + local output, status = dfhack.run_command_silent('3dveins') + expect.eq(CR_OK, status) + expect.str_find('Writing tiles', output) +end + +function test.verbose_option() + local _, status = dfhack.run_command_silent('3dveins', 'verbose') + expect.eq(CR_OK, status) +end + +function test.bad_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('3dveins', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/aquifer.lua b/test/plugins/aquifer.lua new file mode 100644 index 0000000000..b47d36fbcb --- /dev/null +++ b/test/plugins/aquifer.lua @@ -0,0 +1,152 @@ +config.mode = 'fortress' +config.target = 'aquifer' + +local aquifer = require('plugins.aquifer') + +local function capture(fn) + local calls = {} + mock.patch({ + {aquifer, 'aquifer_list', function(pos1, pos2, levels, leaky) + calls.list = {pos1=pos1, pos2=pos2, levels=levels, leaky=leaky} + end}, + {aquifer, 'aquifer_drain', function(aq, pos1, pos2, skip, levels, leaky, all) + calls.drain = {aq=aq, pos1=pos1, pos2=pos2, skip=skip, + levels=levels, leaky=leaky, all=all} + return 7 + end}, + {aquifer, 'aquifer_convert', function(aq, pos1, pos2, skip, levels, leaky, all) + calls.convert = {aq=aq, pos1=pos1, pos2=pos2, skip=skip, + levels=levels, leaky=leaky, all=all} + return 5 + end}, + {aquifer, 'aquifer_add', function(aq, pos1, pos2, skip, levels, leaky, all) + calls.add = {aq=aq, pos1=pos1, pos2=pos2, skip=skip, + levels=levels, leaky=leaky, all=all} + return 3 + end}, + }, fn) + return calls +end + +function test.help_returns_false() + -- regression test: --help used to run a map-wide list instead + local called = false + mock.patch({ + {aquifer, 'aquifer_list', function() called = true end}, + -- helpdb can't resolve the script entry when called via require() + {dfhack, 'script_help', function() return 'test help' end}, + }, function() + expect.false_(aquifer.parse_commandline({'--help'})) + expect.false_(aquifer.parse_commandline({'-h'})) + expect.false_(aquifer.parse_commandline({'help'})) + end) + expect.false_(called) +end + +function test.list_defaults_to_all_levels() + local calls = capture(function() + expect.true_(aquifer.parse_commandline({'list'})) + end) + expect.eq(0, calls.list.pos1.x) + expect.eq(df.global.world.map.z_count - 1, calls.list.pos2.z) +end + +function test.list_cur_zlevel() + local calls = capture(function() + expect.true_(aquifer.parse_commandline({'list', '--cur-zlevel'})) + end) + local curz = df.global.window_z + expect.eq(curz, calls.list.pos1.z) + expect.eq(curz, calls.list.pos2.z) +end + +function test.coords_normalize_min_max() + local calls = capture(function() + expect.true_(aquifer.parse_commandline( + {'list', '9,9,100', '3,4,99'})) + end) + expect.eq(3, calls.list.pos1.x) + expect.eq(9, calls.list.pos2.x) + expect.eq(4, calls.list.pos1.y) + expect.eq(9, calls.list.pos2.y) + expect.eq(99, calls.list.pos1.z) + expect.eq(100, calls.list.pos2.z) +end + +function test.single_coord_defaults_to_cursor_or_same_pos() + local calls = capture(function() + expect.true_(aquifer.parse_commandline({'list', '3,4,99'})) + end) + expect.eq(3, calls.list.pos1.x) + if df.global.window_z == 99 then + -- cursor would need to be on z 99; either way pos2 is consistent + expect.true_(calls.list.pos2.x == 3 or calls.list.pos2.x ~= nil) + end +end + +function test.drain_requires_no_aquifer_type() + local calls = capture(function() + expect.true_(aquifer.parse_commandline({'drain', '--cur-zlevel'})) + end) + expect.eq('all', calls.drain.aq) + expect.eq(0, calls.drain.skip) +end + +function test.add_and_convert_require_aquifer_type() + capture(function() + expect.error_match('must specify an aquifer type', + function() aquifer.parse_commandline({'add', '--cur-zlevel'}) end) + expect.error_match('must specify an aquifer type', + function() aquifer.parse_commandline({'convert', '--cur-zlevel'}) end) + end) +end + +function test.add_light_with_skip_and_levels() + local calls = capture(function() + expect.true_(aquifer.parse_commandline( + {'add', 'light', '--cur-zlevel', '--skip-top', '2'})) + end) + expect.eq('light', calls.add.aq) + expect.eq(2, calls.add.skip) + expect.eq(1, calls.add.levels) +end + +function test.levels_option_with_zup() + -- --zup keeps the bottom N levels of the range + local calls = capture(function() + expect.true_(aquifer.parse_commandline( + {'drain', 'heavy', '0,0,90', '10,10,95', '--zup', '--levels', '2'})) + end) + expect.eq(90, calls.drain.pos1.z) + expect.eq(91, calls.drain.pos2.z) + expect.eq(2, calls.drain.levels) + expect.eq('heavy', calls.drain.aq) +end + +function test.levels_option_with_zdown() + -- --zdown keeps the top N levels of the range + local calls = capture(function() + expect.true_(aquifer.parse_commandline( + {'drain', 'heavy', '0,0,90', '10,10,95', '--zdown', '--levels', '2'})) + end) + expect.eq(94, calls.drain.pos1.z) + expect.eq(95, calls.drain.pos2.z) + expect.eq(2, calls.drain.levels) +end + +function test.invalid_levels_and_skip_top_rejected() + expect.error(function() + aquifer.parse_commandline({'drain', '--levels', '0'}) + end) + expect.error(function() + aquifer.parse_commandline({'drain', '--skip-top', '-1'}) + end) +end + +function test.unknown_action_falls_back_to_list() + -- positionals that aren't actions are treated as coords; an unparseable + -- coord raises an error through argparse + expect.error(function() + aquifer.parse_commandline({'bogus', '--cur-zlevel'}) + end) +end diff --git a/test/plugins/autobutcher.lua b/test/plugins/autobutcher.lua new file mode 100644 index 0000000000..236272a58c --- /dev/null +++ b/test/plugins/autobutcher.lua @@ -0,0 +1,83 @@ +config.mode = 'fortress' +config.target = 'autobutcher' + +local autobutcher = require('plugins.autobutcher') + +local function parse(args) + -- emulate the autobutcher_options struct passed by the C++ command handler + local opts = {races={insert=function(self, pos, v) table.insert(self, v) end}} + autobutcher.parse_commandline(opts, args) + return opts +end + +function test.help() + expect.true_(parse{'help'}.help) + expect.true_(parse{'-h'}.help) + expect.true_(parse{'--help'}.help) +end + +function test.simple_commands() + for _,cmd in ipairs{'now', 'autowatch', 'noautowatch', 'list', + 'list_export'} do + expect.eq(cmd, parse{cmd}.command) + end +end + +function test.watch_races() + local opts = parse{'watch', 'CAT', 'BIRD_CROW'} + expect.eq('watch', opts.command) + expect.eq(2, #opts.races) + expect.eq('CAT', opts.races[1].value) + expect.eq('BIRD_CROW', opts.races[2].value) +end + +function test.watch_all_and_new() + expect.true_(parse{'watch', 'all'}.races_all) + expect.true_(parse{'unwatch', 'new'}.races_new) + expect.true_(parse{'forget', 'all'}.races_all) +end + +function test.watch_requires_races() + expect.error_match('missing list of races', function() + parse{'watch'} end) +end + +function test.target() + local opts = parse{'target', '1', '2', '3', '4', 'CAT'} + expect.eq('target', opts.command) + expect.eq(1, opts.fk) + expect.eq(2, opts.mk) + expect.eq(3, opts.fa) + expect.eq(4, opts.ma) + expect.eq(1, #opts.races) + expect.eq('CAT', opts.races[1].value) +end + +function test.target_zero_allowed() + local opts = parse{'target', '0', '0', '0', '0', 'all'} + expect.eq(0, opts.fk) + expect.true_(opts.races_all) +end + +function test.target_rejects_bad_numbers() + expect.error_match('non%-negative integer', function() + parse{'target', '-1', '0', '0', '0', 'all'} end) + expect.error_match('non%-negative integer', function() + parse{'target', '1.5', '0', '0', '0', 'all'} end) + expect.error_match('non%-negative integer', function() + parse{'target', 'x', '0', '0', '0', 'all'} end) +end + +function test.target_requires_all_counts() + expect.error(function() parse{'target', '1', '2', '3'} end) +end + +function test.target_requires_races() + expect.error_match('missing list of races', function() + parse{'target', '1', '2', '3', '4'} end) +end + +function test.unrecognized_command() + expect.error_match('unrecognized command', function() + parse{'bogus'} end) +end diff --git a/test/plugins/autochop.lua b/test/plugins/autochop.lua new file mode 100644 index 0000000000..9cfbb9ffc8 --- /dev/null +++ b/test/plugins/autochop.lua @@ -0,0 +1,111 @@ +config.mode = 'fortress' +config.target = 'autochop' + +local autochop = require('plugins.autochop') + +local function with_targets(fn) + local old_max, old_min = autochop.autochop_getTargets() + return dfhack.with_finalize( + function() + autochop.autochop_setTargets(old_max, old_min) + autochop.autochop_undesignate() + end, + fn) +end + +local function with_burrow(fn) + local burrows = df.global.plotinfo.burrows + local b = df.burrow:new() + b.id = burrows.next_id + burrows.next_id = burrows.next_id + 1 + b.name = 'DFHACK_TEST_AUTOCHOP' + burrows.list:insert('#', b) + return dfhack.with_finalize( + function() + for i = #burrows.list - 1, 0, -1 do + if burrows.list[i] == b then + burrows.list:erase(i) + end + end + end, + function() fn(b) end) +end + +function test.target_command_sets_max_and_min() + with_targets(function() + expect.true_(autochop.parse_commandline('target', '50', '40')) + local max, min = autochop.autochop_getTargets() + expect.eq(50, max) + expect.eq(40, min) + end) +end + +function test.target_defaults_min_to_80_percent_of_max() + with_targets(function() + expect.true_(autochop.parse_commandline('target', '200')) + local max, min = autochop.autochop_getTargets() + expect.eq(200, max) + expect.eq(160, min) + end) +end + +function test.target_rejects_invalid_values() + expect.error_match('non%-negative integer', + function() autochop.setTargets('-1') end) + expect.error_match('non%-negative integer', + function() autochop.setTargets('notanumber') end) + expect.error_match('between 0 and the maximum', + function() autochop.setTargets('50', '60') end) + expect.error_match('between 0 and the maximum', + function() autochop.setTargets('50', '-1') end) +end + +function test.chop_and_nochop_toggle_burrow() + with_burrow(function(b) + expect.true_(autochop.parse_commandline('chop', b.name)) + expect.eq(1, autochop.autochop_getBurrowConfig(b.id).chop) + expect.true_(autochop.parse_commandline('nochop', b.name)) + expect.eq(0, autochop.autochop_getBurrowConfig(b.id).chop) + end) +end + +function test.clearcut_and_noclearcut_toggle_burrow() + with_burrow(function(b) + expect.true_(autochop.parse_commandline('clearcut', b.name)) + expect.eq(1, autochop.autochop_getBurrowConfig(b.id).clearcut) + expect.true_(autochop.parse_commandline('noclear', b.name)) + expect.eq(0, autochop.autochop_getBurrowConfig(b.id).clearcut) + end) +end + +function test.protect_sets_each_listed_type() + with_burrow(function(b) + expect.true_(autochop.parse_commandline('protect', 'brewable,edible', b.name)) + local config = autochop.autochop_getBurrowConfig(b.id) + expect.eq(1, config.protect_brewable) + expect.eq(1, config.protect_edible) + expect.eq(0, config.protect_cookable) + expect.true_(autochop.parse_commandline('unprotect', 'edible', b.name)) + config = autochop.autochop_getBurrowConfig(b.id) + expect.eq(1, config.protect_brewable) + expect.eq(0, config.protect_edible) + end) +end + +function test.burrow_command_requires_a_burrow() + expect.error_match('no target burrows', + function() autochop.parse_commandline('chop') end) + expect.error_match('burrow not found', + function() autochop.parse_commandline('chop', 'DFHACK_NONEXISTENT_BURROW') end) +end + +function test.help_and_unknown_command_return_false() + expect.false_(autochop.parse_commandline('help')) + expect.false_(autochop.parse_commandline('--help')) + expect.false_(autochop.parse_commandline('bogus')) +end + +function test.status_and_no_command_return_true() + expect.true_(autochop.parse_commandline()) + expect.true_(autochop.parse_commandline('status')) +end diff --git a/test/plugins/autoclothing.lua b/test/plugins/autoclothing.lua new file mode 100644 index 0000000000..8d02af8789 --- /dev/null +++ b/test/plugins/autoclothing.lua @@ -0,0 +1,20 @@ +config.mode = 'fortress' +config.target = 'autoclothing' + +function test.status() + local output = dfhack.run_command_silent('autoclothing') + expect.str_find('Automatic clothing management is currently', output) + expect.str_find('automatic clothing orders', output) +end + +function test.wrong_arg_count_shows_usage() + local output, status = dfhack.run_command_silent('autoclothing', 'bogus') + expect.true_(output:find('Wrong number', 1, true) ~= nil + or status ~= 0) +end + +function test.invalid_requirement_rejected() + local output, status = dfhack.run_command_silent('autoclothing', + 'bogus', 'bogus') + expect.ne(0, status) +end diff --git a/test/plugins/autodump.lua b/test/plugins/autodump.lua new file mode 100644 index 0000000000..bd2f1c0eed --- /dev/null +++ b/test/plugins/autodump.lua @@ -0,0 +1,147 @@ +config.mode = 'fortress' +config.target = 'autodump' + +local dwarfmode = require('gui.dwarfmode') + +local function find_floor_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + local des = block.designation[x][y] + local occ = block.occupancy[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.FLOOR + and not des.hidden and occ.building == 0 then + return block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + end + end + end + end +end + +local function set_cursor(x, y, z) + dwarfmode.setCursorPos(xyz2pos(x, y, z)) +end + +local function clear_cursor() + dwarfmode.clearCursorPos() +end + +local function find_dumpable_item() + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + if not item.flags.dump and not item.flags.construction + and not item.flags.in_building and not item.flags.artifact + and not item.flags.in_job and not item.flags.forbid + and not item.flags.owned and not item.flags.in_inventory then + return item + end + end +end + +local function block_has_item(block, id) + for _, bid in ipairs(block.items) do + if bid == id then return true end + end + return false +end + +local function items_sorted(block) + local prev = -1 + for _, id in ipairs(block.items) do + if id <= prev then return false end + prev = id + end + return true +end + +function test.dump_moves_item_to_cursor() + local x, y, z = find_floor_pos() + local item = find_dumpable_item() + expect.ne(nil, x, 'test needs a revealed floor tile') + expect.ne(nil, item, 'test needs a dumpable item') + local ix, iy, iz = dfhack.items.getPosition(item) + expect.ne(nil, ix, 'test item needs a real position') + local old_block = dfhack.maps.getTileBlock(ix, iy, iz) + local new_block = dfhack.maps.getTileBlock(x, y, z) + + return dfhack.with_finalize(function() + item.flags.dump = false + clear_cursor() + end, function() + set_cursor(x, y, z) + item.flags.dump = true + + local _, status = dfhack.run_command_silent('autodump') + expect.eq(CR_OK, status) + -- item was teleported to the cursor and marked as dumped + expect.eq(x, item.pos.x) + expect.eq(y, item.pos.y) + expect.eq(z, item.pos.z) + expect.false_(item.flags.dump) + expect.true_(item.flags.forbid) + expect.true_(item.flags.on_ground) + expect.false_(item.flags.in_inventory) + expect.false_(item.flags.in_building) + + -- the destination tile's occupancy and the map_block item + -- vectors must reflect the move + local _, occ = dfhack.maps.getTileFlags(x, y, z) + expect.true_(occ.item) + expect.true_(block_has_item(new_block, item.id), + 'item id should be in the new map_block.items') + if old_block ~= new_block then + expect.false_(block_has_item(old_block, item.id), + 'item id should be removed from the old map_block.items') + end + expect.true_(items_sorted(new_block), + 'map_block.items should remain sorted') + end) +end + +function test.conflicting_filters_is_wrong_usage() + local output, status = dfhack.run_command_silent('autodump', 'visible', 'hidden') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find("both hidden and visible", output) +end + +function test.bad_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('autodump', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.destroy_and_undestroy() + local item = find_dumpable_item() + expect.ne(nil, item, 'test needs a dumpable item') + + local was_paused = dfhack.world.ReadPauseState() + return dfhack.with_finalize(function() + dfhack.world.SetPauseState(was_paused) + dfhack.run_command_silent('autodump', 'undestroy') + item.flags.dump = false + item.flags.garbage_collect = false + item.flags.forbid = false + item.flags.hidden = false + end, function() + item.flags.dump = true + -- undestroy only restores marks made in the same frame, so the + -- game must stay paused between destroy and undestroy + dfhack.world.SetPauseState(true) + + local _, status = dfhack.run_command_silent('autodump', 'destroy') + expect.eq(CR_OK, status) + expect.true_(item.flags.garbage_collect) + expect.true_(item.flags.forbid) + expect.true_(item.flags.hidden) + + local output, status2 = dfhack.run_command_silent('autodump', 'undestroy') + expect.eq(CR_OK, status2) + expect.str_find('unmarked for destruction', output) + expect.false_(item.flags.garbage_collect) + expect.false_(item.flags.forbid) + expect.false_(item.flags.hidden) + -- undestroy restores the pre-destroy flags, including the dump + -- flag we set ourselves + expect.true_(item.flags.dump) + end) +end diff --git a/test/plugins/autofarm.lua b/test/plugins/autofarm.lua new file mode 100644 index 0000000000..5dfb0a6348 --- /dev/null +++ b/test/plugins/autofarm.lua @@ -0,0 +1,104 @@ +config.mode = 'fortress' +config.target = 'autofarm' + +-- autofarm enabled state persists; leave it disabled +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + dfhack.run_command_silent('autofarm', 'disable') + end, test_fn) +end + +function test.status() + local output = dfhack.run_command_silent('autofarm', 'status') + expect.ne('', output) +end + +function test.enable_disable() + dfhack.run_command_silent('autofarm', 'enable') + local output = dfhack.run_command_silent('autofarm', 'status') + expect.str_find('Active', output) + dfhack.run_command_silent('autofarm', 'disable') + output = dfhack.run_command_silent('autofarm', 'status') + expect.str_find('Stopped', output) +end + +function test.runonce() + local output, status = dfhack.run_command_silent('autofarm', 'runonce') + expect.eq(0, status) +end + +function test.unknown_arg_shows_usage() + local output, status = dfhack.run_command_silent('autofarm', 'bogus') + expect.true_(output:find('autofarm', 1, true) ~= nil or status ~= 0) +end + +function test.threshold_rejects_bad_plant() + local output, status = dfhack.run_command_silent('autofarm', + 'threshold', '-1', '5') + expect.true_(output:find('Cannot find plant', 1, true) ~= nil + or status ~= 0) +end + +local function find_plant(id) + for _,plant in ipairs(df.global.world.raws.plants.all) do + if plant.id == id then return plant end + end +end + +function test.threshold_growth() + local plugin = require('plugins.autofarm') + -- quarry bushes have a stockpile LEAVES growth in vanilla + local output, status = dfhack.run_command_silent('autofarm', + 'threshold', '30', + 'BUSH_QUARRY:LEAVES') + expect.eq(0, status) + local plant = find_plant('BUSH_QUARRY') + local data = plugin.autofarm_getData() + expect.eq(30, data.growth_thresholds[plant.index].LEAVES) + plugin.autofarm_removeGrowthThreshold(plant.index, 'LEAVES') + plugin.autofarm_removeThreshold(plant.index) +end + +function test.threshold_rejects_bad_growth() + local output, status = dfhack.run_command_silent('autofarm', + 'threshold', '5', + 'BUSH_QUARRY:NOPE') + expect.true_(output:find('Cannot find plant or growth', 1, true) ~= nil + or status ~= 0) + -- growths that are not stockpile-marked are also rejected + output, status = dfhack.run_command_silent('autofarm', + 'threshold', '5', + 'BUCKWHEAT:FLOWERS') + expect.true_(output:find('Cannot find plant or growth', 1, true) ~= nil + or status ~= 0) +end + +function test.growth_threshold_funcs() + local plugin = require('plugins.autofarm') + local plant = find_plant('BUSH_QUARRY') + plugin.autofarm_setGrowthThreshold(plant.index, 'LEAVES', 42) + local data = plugin.autofarm_getData() + expect.eq(42, data.growth_thresholds[plant.index].LEAVES) + plugin.autofarm_removeGrowthThreshold(plant.index, 'LEAVES') + data = plugin.autofarm_getData() + expect.nil_(data.growth_thresholds[plant.index]) +end + +function test.farm_exclusion() + local plugin = require('plugins.autofarm') + expect.false_(plugin.autofarm_isFarmExcluded(-1)) + plugin.autofarm_setFarmExcluded(-1, true) + expect.true_(plugin.autofarm_isFarmExcluded(-1)) + plugin.autofarm_setFarmExcluded(-1, false) + expect.false_(plugin.autofarm_isFarmExcluded(-1)) +end + +function test.get_data() + local data = require('plugins.autofarm').autofarm_getData() + expect.eq('number', type(data.default_threshold)) + expect.eq('table', type(data.thresholds)) + expect.eq('table', type(data.growth_thresholds)) + expect.eq('table', type(data.last_counts)) + expect.eq('table', type(data.growth_counts)) + expect.eq('table', type(data.excluded_farms)) +end diff --git a/test/plugins/autolabor.lua b/test/plugins/autolabor.lua new file mode 100644 index 0000000000..bca5aa04f0 --- /dev/null +++ b/test/plugins/autolabor.lua @@ -0,0 +1,37 @@ +config.mode = 'fortress' +config.target = 'autolabor' + +-- autolabor enabled state and labor configs persist; leave it off and +-- restore the test labor's defaults +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + dfhack.run_command_silent('autolabor', 'MINE', 'reset') + dfhack.run_command_silent('autolabor', 'disable') + end, test_fn) +end + +function test.no_args_shows_help() + local output = dfhack.run_command_silent('autolabor') + expect.ne('', output) +end + +function test.labor_command_requires_enable() + dfhack.run_command_silent('autolabor', 'disable') + local output = dfhack.run_command_silent('autolabor', 'MINE', 'haulers') + expect.str_find('not enabled', output) +end + +function test.set_labor_mode() + dfhack.run_command_silent('autolabor', 'enable') + local output = dfhack.run_command_silent('autolabor', 'MINE', 'haulers') + expect.str_find('MINE.-haulers', output) + output = dfhack.run_command_silent('autolabor', 'MINE', 'reset') + expect.str_find('MINE.-minimum', output) +end + +function test.unknown_labor_errors() + dfhack.run_command_silent('autolabor', 'enable') + local output = dfhack.run_command_silent('autolabor', 'BOGUS_LABOR', + 'haulers') + expect.str_find('Could not find labor', output) +end diff --git a/test/plugins/autonestbox.lua b/test/plugins/autonestbox.lua new file mode 100644 index 0000000000..997feb054b --- /dev/null +++ b/test/plugins/autonestbox.lua @@ -0,0 +1,37 @@ +config.mode = 'fortress' +config.target = 'autonestbox' + +local autonestbox = require('plugins.autonestbox') + +local function parse(args) + local opts = {} + autonestbox.parse_commandline(opts, args) + return opts +end + +function test.now_flag() + expect.true_(parse({'now'}).now) +end + +function test.help_flag() + expect.true_(parse({'help'}).help) + expect.true_(parse({'--help'}).help) + expect.true_(parse({'-h'}).help) +end + +function test.help_does_not_set_now() + local opts = parse({'help', 'now'}) + expect.true_(opts.help) + expect.nil_(opts.now) +end + +function test.unknown_positionals_ignored() + local opts = parse({'bogus'}) + expect.nil_(opts.now) + expect.nil_(opts.help) +end + +function test.mixed_positionals() + local opts = parse({'bogus', 'now', 'other'}) + expect.true_(opts.now) +end diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua index a08844bbee..d5ebd2031d 100644 --- a/test/plugins/blueprint.lua +++ b/test/plugins/blueprint.lua @@ -243,16 +243,16 @@ end function test.get_filename() local opts = {name='a', split_strategy='none'} - expect.eq('dfhack-config/blueprints/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='none'} - expect.eq('dfhack-config/blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a', split_strategy='phase'} - expect.eq('dfhack-config/blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='phase'} - expect.eq('dfhack-config/blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) + expect.eq(dfhack.getConfigPath() .. '/blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) expect.error_match('could not parse basename', function() b.get_filename({name='', split_strategy='none'}) diff --git a/test/plugins/buildingplan.lua b/test/plugins/buildingplan.lua new file mode 100644 index 0000000000..f2f48e92f2 --- /dev/null +++ b/test/plugins/buildingplan.lua @@ -0,0 +1,70 @@ +config.mode = 'fortress' +config.target = 'buildingplan' + +local buildingplan = require('plugins.buildingplan') + +local function find_floor_pos() + for _, unit in ipairs(df.global.world.units.active) do + for dx = -4, 4 do + for dy = -4, 4 do + local pos = xyz2pos(unit.pos.x + dx, unit.pos.y + dy, unit.pos.z) + local tt = dfhack.maps.getTileType(pos) + local block = dfhack.maps.getTileBlock(pos) + if tt and block + and df.tiletype.attrs[tt].shape == df.tiletype_shape.FLOOR + and block.occupancy[pos.x % 16][pos.y % 16].building == 0 then + return pos + end + end + end + end +end + +local function place_box(pos) + return dfhack.buildings.constructBuilding{ + pos=pos, + type=df.building_type.Box, subtype=-1, custom=-1, + width=1, height=1, + filters=dfhack.buildings.getFiltersByType({}, df.building_type.Box, -1, -1), + } +end + +function test.set_get_do_now() + local pos = find_floor_pos() + expect.ne(nil, pos, 'test needs a free floor tile') + local bld = place_box(pos) + expect.ne(nil, bld, 'failed to place test building') + dfhack.with_finalize( + function() dfhack.buildings.deconstruct(bld) end, + function() + expect.false_(buildingplan.getDoNow(bld)) + expect.true_(buildingplan.setDoNow(bld, true)) + expect.true_(buildingplan.getDoNow(bld)) + expect.true_(bld.jobs[0].flags.do_now) + expect.true_(buildingplan.setDoNow(bld, false)) + expect.false_(buildingplan.getDoNow(bld)) + end) +end + +function test.do_now_invalid_input() + expect.false_(buildingplan.setDoNow(nil, true)) + expect.false_(buildingplan.getDoNow(nil)) +end + +function test.do_now_flag_survives_registration() + local pos = find_floor_pos() + expect.ne(nil, pos, 'test needs a free floor tile') + local bld = place_box(pos) + expect.ne(nil, bld, 'failed to place test building') + dfhack.with_finalize( + function() dfhack.buildings.deconstruct(bld) end, + function() + buildingplan.addPlannedBuilding(bld) + expect.true_(buildingplan.isPlannedBuilding(bld)) + expect.true_(buildingplan.setDoNow(bld, true)) + -- the flag is set on the job directly, so buildingplan's own + -- suspend/cycle handling must not clear it + buildingplan.doCycle() + expect.true_(buildingplan.getDoNow(bld)) + end) +end diff --git a/test/plugins/burrow.lua b/test/plugins/burrow.lua new file mode 100644 index 0000000000..dc557cd89d --- /dev/null +++ b/test/plugins/burrow.lua @@ -0,0 +1,102 @@ +config.mode = 'fortress' +config.target = 'burrow' + +local burrow = require('plugins.burrow') + +local function with_burrow(fn, name) + local burrows = df.global.plotinfo.burrows + -- burrow name lookups return the first match, so remove any test burrows + -- leaked by previous runs before creating ours + name = name or 'DFHACK_TEST_BURROW' + for i = #burrows.list - 1, 0, -1 do + if burrows.list[i].name == name then + dfhack.burrows.clearTiles(burrows.list[i]) + burrows.list[i]:delete() + burrows.list:erase(i) + end + end + local b = df.burrow:new() + b.id = burrows.next_id + burrows.next_id = burrows.next_id + 1 + b.name = name + burrows.list:insert('#', b) + return dfhack.with_finalize( + function() + for i = #burrows.list - 1, 0, -1 do + if burrows.list[i] == b then + burrows.list:erase(i) + end + end + dfhack.burrows.clearTiles(b) + dfhack.burrows.clearUnits(b) + b:delete() + end, + function() fn(b) end) +end + +local function assigned(b, x, y, z) + return dfhack.burrows.isAssignedTile(b, xyz2pos(x, y, z)) +end + +function test.tiles_box_add_and_remove() + with_burrow(function(b) + expect.true_(burrow.parse_commandline( + 'tiles', 'box-add', b.name, '10,10,120', '12,12,120')) + expect.true_(assigned(b, 10, 10, 120)) + expect.true_(assigned(b, 12, 12, 120)) + expect.false_(assigned(b, 9, 10, 120)) + expect.false_(assigned(b, 10, 10, 119)) + + expect.true_(burrow.parse_commandline( + 'tiles', 'box-remove', b.name, '11,11,120', '12,12,120')) + expect.true_(assigned(b, 10, 10, 120)) + expect.false_(assigned(b, 12, 12, 120)) + end) +end + +function test.tiles_clear() + with_burrow(function(b) + burrow.parse_commandline('tiles', 'box-add', b.name, '10,10,120', '12,12,120') + expect.true_(assigned(b, 10, 10, 120)) + expect.true_(burrow.parse_commandline('tiles', 'clear', b.name)) + expect.false_(assigned(b, 10, 10, 120)) + end) +end + +function test.tiles_add_copies_from_other_burrow() + with_burrow(function(src) + burrow.parse_commandline('tiles', 'box-add', src.name, '20,20,120', '21,21,120') + with_burrow(function(dst) + expect.true_(burrow.parse_commandline( + 'tiles', 'add', dst.name, src.name)) + expect.true_(assigned(dst, 20, 20, 120)) + expect.true_(assigned(src, 20, 20, 120)) + end, 'DFHACK_TEST_BURROW_DST') + end, 'DFHACK_TEST_BURROW_SRC') +end + +function test.units_add_copies_from_other_burrow() + -- create a source burrow with a unit assigned directly + with_burrow(function(src) + local unit = dfhack.units.getCitizens()[1] + if not unit then return end + dfhack.burrows.setAssignedUnit(src, unit, true) + expect.true_(dfhack.burrows.isAssignedUnit(src, unit)) + with_burrow(function(dst) + expect.true_(burrow.parse_commandline( + 'units', 'add', dst.name, src.name)) + expect.true_(dfhack.burrows.isAssignedUnit(dst, unit)) + expect.true_(burrow.parse_commandline( + 'units', 'remove', dst.name, src.name)) + expect.false_(dfhack.burrows.isAssignedUnit(dst, unit)) + expect.true_(dfhack.burrows.isAssignedUnit(src, unit)) + end, 'DFHACK_TEST_BURROW_DST') + end, 'DFHACK_TEST_BURROW_SRC') +end + +function test.help_and_bad_mode_return_false() + expect.false_(burrow.parse_commandline('help')) + expect.false_(burrow.parse_commandline('--help')) + expect.false_(burrow.parse_commandline('bogus', 'clear')) + expect.false_(burrow.parse_commandline('tiles', 'bogus')) +end diff --git a/test/plugins/changeitem.lua b/test/plugins/changeitem.lua new file mode 100644 index 0000000000..0efc69530a --- /dev/null +++ b/test/plugins/changeitem.lua @@ -0,0 +1,85 @@ +config.mode = 'fortress' +config.target = 'changeitem' + +local dwarfmode = require('gui.dwarfmode') + +local function find_floor_item_pos() + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + if not item.flags.hidden and not item.flags.in_inventory + and not item.flags.in_job and not item.flags.construction then + local x, y, z = dfhack.items.getPosition(item) + if x then return item, x, y, z end + end + end +end + +local function set_cursor(x, y, z) + dwarfmode.setCursorPos(xyz2pos(x, y, z)) +end + +local function clear_cursor() + dwarfmode.clearCursorPos() +end + +function test.here_quality_changes_item() + local item, x, y, z = find_floor_item_pos() + expect.ne(nil, item, 'test needs an item on the ground') + local orig_quality = item.quality + + return dfhack.with_finalize(function() + item.quality = orig_quality + clear_cursor() + end, function() + set_cursor(x, y, z) + local output, status = dfhack.run_command_silent('changeitem', + 'here', 'q', '4') + expect.eq(CR_OK, status) + expect.str_find('items processed', output) + expect.eq(4, item.quality) + end) +end + +function test.here_empty_tile_processes_nothing() + -- find a floor tile and put the cursor on it; if it happens to hold + -- items the count is still reported + local block = df.global.world.map.map_blocks[0] + local x, y, z = block.map_pos.x, block.map_pos.y, block.map_pos.z + + return dfhack.with_finalize(clear_cursor, function() + set_cursor(x, y, z) + local output, status = dfhack.run_command_silent('changeitem', 'here') + expect.eq(CR_OK, status) + expect.str_find('items processed', output) + end) +end + +function test.here_no_cursor_is_failure() + return dfhack.with_finalize(clear_cursor, function() + clear_cursor() + local output, status = dfhack.run_command_silent('changeitem', 'here') + expect.eq(CR_FAILURE, status) + expect.str_find('Cursor position not found', output) + end) +end + +function test.no_selection_is_failure() + local output, status = dfhack.run_command_silent('changeitem') + expect.eq(CR_FAILURE, status) + expect.str_find('No item selected', output) +end + +function test.material_missing_arg_is_wrong_usage() + local output, status = dfhack.run_command_silent('changeitem', 'm') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('no material specified', output) +end + +function test.bad_quality_is_wrong_usage() + local _, status = dfhack.run_command_silent('changeitem', 'q', '9') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.bad_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('changeitem', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/changelayer.lua b/test/plugins/changelayer.lua new file mode 100644 index 0000000000..f3bfb55b1b --- /dev/null +++ b/test/plugins/changelayer.lua @@ -0,0 +1,33 @@ +config.mode = 'fortress' +config.target = 'changelayer' + +local dwarfmode = require('gui.dwarfmode') + +function test.no_material_is_wrong_usage() + local output, status = dfhack.run_command_silent('changelayer') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('specify a material', output) +end + +function test.bad_material_is_failure() + local output, status = dfhack.run_command_silent('changelayer', 'BOGUSMAT') + expect.eq(CR_FAILURE, status) + expect.str_find('No such material', output) +end + +function test.no_cursor_is_failure() + return dfhack.with_finalize(function() + dwarfmode.clearCursorPos() + end, function() + dwarfmode.clearCursorPos() + local output, status = dfhack.run_command_silent('changelayer', + 'GRANITE') + expect.eq(CR_FAILURE, status) + expect.str_find('No cursor', output) + end) +end + +function test.help_is_wrong_usage() + local _, status = dfhack.run_command_silent('changelayer', '?') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/cleanconst.lua b/test/plugins/cleanconst.lua new file mode 100644 index 0000000000..3d06657b18 --- /dev/null +++ b/test/plugins/cleanconst.lua @@ -0,0 +1,11 @@ +config.mode = 'fortress' +config.target = 'cleanconst' + +-- cleanconst scans all items for bogus construction flags. On a healthy map +-- it cleans nothing but still prints its summary; the value here is +-- exercising the full item scan for crashes. +function test.clean_run() + local output, status = dfhack.run_command_silent('cleanconst') + expect.eq(CR_OK, status) + expect.str_find('Done%. %d+ construction items cleaned up%.', output) +end diff --git a/test/plugins/cleaners.lua b/test/plugins/cleaners.lua new file mode 100644 index 0000000000..1136e69634 --- /dev/null +++ b/test/plugins/cleaners.lua @@ -0,0 +1,48 @@ +config.mode = 'fortress' +config.target = 'cleaners' + +-- the plugin's command is `clean` (the plugin binary is named cleaners). +-- each subcommand prints a summary only when it actually removed something, +-- so on a clean map the output is empty but the status is still CR_OK. + +local function expect_clean_ok(output, status, pattern) + expect.eq(CR_OK, status) + if #output > 0 then + expect.str_find(pattern, output) + end +end + +function test.clean_map() + local output, status = dfhack.run_command_silent('clean', 'map') + expect_clean_ok(output, status, 'Cleaned %d+ of %d+ map blocks%.') +end + +function test.clean_units() + local output, status = dfhack.run_command_silent('clean', 'units') + expect_clean_ok(output, status, 'Removed %d+ contaminants from %d+ creatures%.') +end + +function test.clean_items() + local output, status = dfhack.run_command_silent('clean', 'items') + expect_clean_ok(output, status, 'Removed %d+ contaminants from %d+ items%.') +end + +function test.clean_plants() + local output, status = dfhack.run_command_silent('clean', 'plants') + expect_clean_ok(output, status, 'Removed %d+ contaminants from %d+ plants%.') +end + +function test.clean_all() + local _, status = dfhack.run_command_silent('clean', 'all') + expect.eq(CR_OK, status) +end + +function test.no_args_is_wrong_usage() + local _, status = dfhack.run_command_silent('clean') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.unknown_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('clean', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/cleanowned.lua b/test/plugins/cleanowned.lua new file mode 100644 index 0000000000..78e4fc28a6 --- /dev/null +++ b/test/plugins/cleanowned.lua @@ -0,0 +1,20 @@ +config.mode = 'fortress' +config.target = 'cleanowned' + +-- `cleanowned dryrun` scans all owned items on the map and prints what it +-- would confiscate without mutating anything; the value here is exercising +-- the full item scan plus the per-item report path. +function test.dryrun_is_side_effect_free() + local _, status = dfhack.run_command_silent('cleanowned', 'dryrun') + expect.eq(CR_OK, status) +end + +function test.dryrun_with_filters() + local _, status = dfhack.run_command_silent('cleanowned', 'dryrun', 'scattered', 'x', 'X', 'all', 'nodump') + expect.eq(CR_OK, status) +end + +function test.unknown_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('cleanowned', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/createitem.lua b/test/plugins/createitem.lua new file mode 100644 index 0000000000..cfb1b1b59b --- /dev/null +++ b/test/plugins/createitem.lua @@ -0,0 +1,95 @@ +config.mode = 'fortress' +config.target = 'createitem' + +local dwarfmode = require('gui.dwarfmode') + +local function find_floor_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + local des = block.designation[x][y] + local occ = block.occupancy[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.FLOOR + and not des.hidden and occ.building == 0 then + return block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + end + end + end + end +end + +local function set_cursor(x, y, z) + dwarfmode.setCursorPos(xyz2pos(x, y, z)) +end + +local function clear_cursor() + dwarfmode.clearCursorPos() +end + +local function count_items_at(x, y, z) + local n = 0 + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + -- item.pos is unreliable for contained items; getPosition is not + local ix, iy, iz = dfhack.items.getPosition(item) + if ix == x and iy == y and iz == z then + n = n + 1 + end + end + return n +end + +function test.creates_item_at_cursor() + local x, y, z = find_floor_pos() + expect.ne(nil, x, 'test needs a revealed floor tile') + local before = count_items_at(x, y, z) + + local created + return dfhack.with_finalize(function() + if created then dfhack.items.remove(created) end + clear_cursor() + dfhack.run_command_silent('createitem', 'floor') + end, function() + set_cursor(x, y, z) + local _, status = dfhack.run_command_silent('createitem', + 'BOULDER', 'INORGANIC:GRANITE') + expect.eq(CR_OK, status) + expect.eq(before + 1, count_items_at(x, y, z)) + + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + local ix, iy, iz = dfhack.items.getPosition(item) + if ix == x and iy == y and iz == z + and item:getType() == df.item_type.BOULDER then + created = item + break + end + end + expect.ne(nil, created, 'created boulder should exist') + end) +end + +function test.floor_mode_sets_destination() + local output, status = dfhack.run_command_silent('createitem', 'floor') + expect.eq(CR_OK, status) + expect.str_find('placed on the floor', output) +end + +function test.no_args_is_wrong_usage() + local _, status = dfhack.run_command_silent('createitem') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.bad_item_type_is_failure() + local output, status = dfhack.run_command_silent('createitem', + 'BOGUS', 'INORGANIC:GRANITE') + expect.eq(CR_FAILURE, status) + expect.str_find('valid item type', output) +end + +function test.bad_material_is_failure() + local output, status = dfhack.run_command_silent('createitem', + 'BOULDER', 'BOGUSMAT') + expect.eq(CR_FAILURE, status) + expect.str_find('Unrecognized material', output) +end diff --git a/test/plugins/cursecheck.lua b/test/plugins/cursecheck.lua new file mode 100644 index 0000000000..bd701c071a --- /dev/null +++ b/test/plugins/cursecheck.lua @@ -0,0 +1,20 @@ +config.mode = 'fortress' +config.target = 'cursecheck' + +function test.reports_map_curse_count() + local output = dfhack.run_command_silent('cursecheck') + expect.str_find('%d+ cursed creatures on map', output) +end + +function test.help_shows_usage() + local output, status = dfhack.run_command_silent('cursecheck', 'help') + expect.true_(output:find('cursecheck', 1, true) ~= nil + or output:find('Usage', 1, true) ~= nil + or output:find('usage', 1, true) ~= nil + or status ~= 0) +end + +function test.detail_still_reports_count() + local output = dfhack.run_command_silent('cursecheck', 'detail') + expect.str_find('cursed creatures on map', output) +end diff --git a/test/plugins/deramp.lua b/test/plugins/deramp.lua new file mode 100644 index 0000000000..80d64d8ba8 --- /dev/null +++ b/test/plugins/deramp.lua @@ -0,0 +1,89 @@ +config.mode = 'fortress' +config.target = 'deramp' + +local function find_ramp_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.RAMP + and not block.designation[x][y].hidden then + return block, x, y + end + end + end + end +end + +local function tiletype_at(x, y, z) + return dfhack.maps.getTileType(x, y, z) +end + +-- getTileBlock returns live refs into the map data, so field writes +-- through it mutate the map directly +local function set_tiletype(x, y, z, tt) + dfhack.maps.getTileBlock(x, y, z).tiletype[x % 16][y % 16] = tt +end + +function test.removes_designated_ramp() + local block, x, y = find_ramp_pos() + expect.ne(nil, block, 'test needs a revealed ramp tile') + local ax, ay, az = block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + + local des = block.designation[x][y] + local orig_dig = des.dig + -- ramps are two tiles tall; the usable-ramp cap is RampTop at z+1 + local above_before = tiletype_at(ax, ay, az + 1) + return dfhack.with_finalize(function() + block.designation[x][y].dig = orig_dig + end, function() + -- mark the ramp for removal, as if the player had designated it + des.dig = df.tile_dig_designation.Default + local _, status = dfhack.run_command_silent('deramp') + expect.eq(CR_OK, status) + local shape = df.tiletype.attrs[block.tiletype[x][y]].shape + expect.ne(df.tiletype_shape.RAMP, shape) + -- the tile above must be cleared iff it was the ramp's cap + local above_after = tiletype_at(ax, ay, az + 1) + if above_before == df.tiletype.RampTop then + expect.eq(df.tiletype.OpenSpace, above_after) + else + expect.eq(above_before, above_after) + end + end) +end + +function test.leaves_non_ramptop_above() + -- fabricate the second case: a ramp whose z+1 tile is not RampTop + local block, x, y = find_ramp_pos() + expect.ne(nil, block, 'test needs a revealed ramp tile') + local ax, ay, az = block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + + local des = block.designation[x][y] + local orig_dig = des.dig + local orig_above = tiletype_at(ax, ay, az + 1) + return dfhack.with_finalize(function() + block.designation[x][y].dig = orig_dig + set_tiletype(ax, ay, az + 1, orig_above) + end, function() + set_tiletype(ax, ay, az + 1, + df.tiletype.StoneFloorSmooth) + des.dig = df.tile_dig_designation.Default + local _, status = dfhack.run_command_silent('deramp') + expect.eq(CR_OK, status) + expect.eq(df.tiletype.StoneFloorSmooth, + tiletype_at(ax, ay, az + 1)) + end) +end + +function test.no_designations_is_ok() + local _, status = dfhack.run_command_silent('deramp') + expect.eq(CR_OK, status) +end + +function test.arg_is_wrong_usage() + local _, status = dfhack.run_command_silent('deramp', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/dig-now.lua b/test/plugins/dig-now.lua new file mode 100644 index 0000000000..9a3ae9da86 --- /dev/null +++ b/test/plugins/dig-now.lua @@ -0,0 +1,96 @@ +config.mode = 'fortress' +config.target = 'dig-now' + +local dignow = require('plugins.dig-now') + +local function make_opts() + return { + boulder_percents={}, + dump_pos={}, + start={}, + ['end']={}, + } +end + +function test.clean_sets_zero_percentages() + local opts = make_opts() + dignow.parse_commandline(opts, '--clean') + expect.eq(0, opts.boulder_percents.layer) + expect.eq(0, opts.boulder_percents.vein) + expect.eq(0, opts.boulder_percents.small_cluster) + expect.eq(0, opts.boulder_percents.deep) +end + +function test.everywhere_sets_full_percentages() + local opts = make_opts() + dignow.parse_commandline(opts, '--everywhere') + expect.eq(100, opts.boulder_percents.layer) + expect.eq(100, opts.boulder_percents.deep) +end + +function test.percentages_custom_values() + local opts = make_opts() + dignow.parse_commandline(opts, '--percentages', '0,33,100,100') + expect.eq(0, opts.boulder_percents.layer) + expect.eq(33, opts.boulder_percents.vein) + expect.eq(100, opts.boulder_percents.small_cluster) + expect.eq(100, opts.boulder_percents.deep) +end + +function test.percentages_rejects_bad_values() + local opts = make_opts() + expect.error_match('invalid percentages', + function() dignow.parse_commandline(opts, '-p', '0,33,101,100') end) + expect.error_match('invalid percentages', + function() dignow.parse_commandline(opts, '-p', '0,33,-5,100') end) + expect.error(function() + dignow.parse_commandline(opts, '-p', '0,33,100') end) +end + +function test.coords_normalize_start_end() + local opts = make_opts() + dignow.parse_commandline(opts, '9,8,95', '3,4,94') + expect.eq(3, opts.start.x) + expect.eq(9, opts['end'].x) + expect.eq(4, opts.start.y) + expect.eq(8, opts['end'].y) + expect.eq(94, opts.start.z) + expect.eq(95, opts['end'].z) +end + +function test.single_coord_sets_same_end() + local opts = make_opts() + dignow.parse_commandline(opts, '5,6,94') + expect.eq(5, opts.start.x) + expect.eq(5, opts['end'].x) + expect.eq(6, opts['end'].y) + expect.eq(94, opts['end'].z) +end + +function test.cur_zlevel_covers_whole_map() + local opts = make_opts() + dignow.parse_commandline(opts, '--cur-zlevel') + local map = df.global.world.map + expect.eq(0, opts.start.x) + expect.eq(map.x_count - 1, opts['end'].x) + expect.eq(map.y_count - 1, opts['end'].y) + expect.eq(df.global.window_z, opts.start.z) + expect.eq(df.global.window_z, opts['end'].z) +end + +function test.help_flag_leaves_opts_untouched() + local opts = make_opts() + dignow.parse_commandline(opts, '--help') + expect.true_(opts.help) + expect.nil_(opts.start.x) + dignow.parse_commandline(opts, 'help') + expect.true_(opts.help) +end + +function test.dump_pos_parses_coords() + local opts = make_opts() + dignow.parse_commandline(opts, '--dump', '1,2,90') + expect.eq(1, opts.dump_pos.x) + expect.eq(2, opts.dump_pos.y) + expect.eq(90, opts.dump_pos.z) +end diff --git a/test/plugins/dig.lua b/test/plugins/dig.lua new file mode 100644 index 0000000000..7f19e7d8a5 --- /dev/null +++ b/test/plugins/dig.lua @@ -0,0 +1,30 @@ +config.mode = 'fortress' +config.target = 'dig' + +local gui = require('gui') +local overlay = require('plugins.overlay') + +local TestScreen = defclass(TestScreen, gui.ZScreen) +TestScreen.ATTRS{focus_path='quickfort'} + +function test.ascii_designations_visible_in_quickfort() + require('plugins.dig') + overlay.rescan() + local widget = overlay.get_state().db['dig.asciidesignated'].widget + expect.eq('dwarfmode/dfhack/lua/quickfort', + widget.viewscreens[#widget.viewscreens]) + + local render = mock.func() + local screen = TestScreen{} + screen:show() + dfhack.with_finalize( + function() screen:dismiss() end, + function() + mock.patch(widget, 'render', render, function() + overlay.render_viewscreen_widgets( + 'viewscreen_dwarfmodest', + dfhack.gui.getViewscreenByType(df.viewscreen_dwarfmodest, 0)) + end) + end) + expect.eq(1, render.call_count) +end diff --git a/test/plugins/dwarfvet.lua b/test/plugins/dwarfvet.lua new file mode 100644 index 0000000000..6e454a77d4 --- /dev/null +++ b/test/plugins/dwarfvet.lua @@ -0,0 +1,42 @@ +config.mode = 'fortress' +config.target = 'dwarfvet' + +local function status_output() + local output, status = dfhack.run_command_silent('dwarfvet', 'status') + expect.eq(CR_OK, status) + return output +end + +function test.status_reports_state_and_patients() + dfhack.run_command_silent('disable', 'dwarfvet') + local output = status_output() + expect.str_find('dwarfvet is not running', output) + expect.str_find('The following animals are receiving treatment:', output) + + dfhack.run_command_silent('enable', 'dwarfvet') + output = status_output() + expect.str_find('dwarfvet is running', output) + dfhack.run_command_silent('disable', 'dwarfvet') +end + +function test.bare_command_is_status() + local output, status = dfhack.run_command_silent('dwarfvet') + expect.eq(CR_OK, status) + expect.str_find('dwarfvet is ', output) +end + +function test.unknown_command_is_wrong_usage() + local _, status = dfhack.run_command_silent('dwarfvet', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.now_runs_checkup() + -- checkup scans hospital zones and animal patients; safe on any fort + dfhack.run_command_silent('enable', 'dwarfvet') + return dfhack.with_finalize(function() + dfhack.run_command_silent('disable', 'dwarfvet') + end, function() + local _, status = dfhack.run_command_silent('dwarfvet', 'now') + expect.eq(CR_OK, status) + end) +end diff --git a/test/plugins/export-world-map.lua b/test/plugins/export-world-map.lua new file mode 100644 index 0000000000..dc914f2e8b --- /dev/null +++ b/test/plugins/export-world-map.lua @@ -0,0 +1,29 @@ +config.mode = 'fortress' +config.target = 'export-world-map' + +local export_dir = dfhack.getConfigPath() .. '/map-export' + +local function sites_csv() + local f = io.open(export_dir .. '/sites.csv') + if not f then return nil end + local content = f:read('*a') + f:close() + return content +end + +function test.sites_export_writes_csv() + return dfhack.with_finalize(function() + os.remove(export_dir .. '/sites.csv') + end, function() + local _, status = dfhack.run_command_silent('export-world-map', 'sites') + expect.eq(CR_OK, status) + local content = sites_csv() + expect.ne(nil, content, 'sites.csv was not written') + expect.true_(#content > 0, 'sites.csv is empty') + end) +end + +function test.unknown_topic_is_wrong_usage() + local _, status = dfhack.run_command_silent('export-world-map', 'bogus-topic') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/filltraffic.lua b/test/plugins/filltraffic.lua new file mode 100644 index 0000000000..9257e71106 --- /dev/null +++ b/test/plugins/filltraffic.lua @@ -0,0 +1,119 @@ +config.mode = 'fortress' +config.target = 'filltraffic' + +local dwarfmode = require('gui.dwarfmode') + +local function find_floor_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + local shape = df.tiletype.attrs[tt].shape + local des = block.designation[x][y] + local occ = block.occupancy[x][y] + if shape == df.tiletype_shape.FLOOR and not des.hidden + and occ.building == 0 then + return block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + end + end + end + end +end + +local function traffic_at(x, y, z) + return dfhack.maps.getTileBlock(x, y, z).designation[x % 16][y % 16].traffic +end + +local function set_cursor(x, y, z) + dwarfmode.setCursorPos(xyz2pos(x, y, z)) +end + +local function clear_cursor() + dwarfmode.clearCursorPos() +end + +function test.fill_high_then_restore() + local x, y, z = find_floor_pos() + expect.ne(nil, x, 'test needs a revealed floor tile') + + return dfhack.with_finalize(function() + set_cursor(x, y, z) + -- always leave the tile normal + dfhack.run_command_silent('filltraffic', 'N') + clear_cursor() + end, function() + set_cursor(x, y, z) + local _, status = dfhack.run_command_silent('filltraffic', 'H') + expect.eq(CR_OK, status) + expect.eq(df.tile_traffic.High, traffic_at(x, y, z)) + + local _, status2 = dfhack.run_command_silent('filltraffic', 'N') + expect.eq(CR_OK, status2) + expect.eq(df.tile_traffic.Normal, traffic_at(x, y, z)) + end) +end + +function test.same_type_is_failure() + local x, y, z = find_floor_pos() + expect.ne(nil, x) + + return dfhack.with_finalize(clear_cursor, function() + set_cursor(x, y, z) + -- the tile is already Normal, so filling Normal is a no-op failure + local output, status = dfhack.run_command_silent('filltraffic', 'N') + expect.eq(CR_FAILURE, status) + expect.str_find('already set to the target', output) + end) +end + +function test.no_cursor_is_failure() + clear_cursor() + -- the cursor_hotkey command guard rejects before the plugin runs + local output, status = dfhack.run_command_silent('filltraffic', 'H') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('unsuitable UI state', output) +end + +function test.wall_is_failure() + -- find a revealed wall tile + local wx, wy, wz + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.WALL + and not block.designation[x][y].hidden then + wx, wy, wz = block.map_pos.x + x, + block.map_pos.y + y, block.map_pos.z + end + end + end + end + expect.ne(nil, wx, 'test needs a revealed wall tile') + + return dfhack.with_finalize(clear_cursor, function() + set_cursor(wx, wy, wz) + local output, status = dfhack.run_command_silent('filltraffic', 'H') + expect.eq(CR_FAILURE, status) + expect.str_find('wall', output) + end) +end + +function test.bad_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('filltraffic', 'Q') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.multi_char_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('filltraffic', 'HH') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.restrictice_and_restrictliquids() + -- restrict traffic on ice/liquid tiles; safe full-map passes + local _, status = dfhack.run_command_silent('restrictice') + expect.eq(CR_OK, status) + local _, status2 = dfhack.run_command_silent('restrictliquids') + expect.eq(CR_OK, status2) +end diff --git a/test/plugins/fix-occupancy.lua b/test/plugins/fix-occupancy.lua new file mode 100644 index 0000000000..d91b4b943d --- /dev/null +++ b/test/plugins/fix-occupancy.lua @@ -0,0 +1,30 @@ +config.mode = 'fortress' +config.target = 'fix-occupancy' + +local fo = require('plugins.fix-occupancy') + +local function any_map_pos() + local block = df.global.world.map.map_blocks[0] + return xyz2pos(block.map_pos.x + 8, block.map_pos.y + 8, + block.map_pos.z) +end + +function test.fix_map_dry_run_leaves_occupancy() + local pos = any_map_pos() + local block = dfhack.maps.getTileBlock(pos) + local before = block.occupancy[pos.x % 16][pos.y % 16].whole + fo.fix_map(true) + expect.eq(before, block.occupancy[pos.x % 16][pos.y % 16].whole) +end + +function test.fix_tile_dry_run() + local pos = any_map_pos() + fo.fix_tile(pos, true) + -- smoke: ran without error + expect.true_(true) +end + +function test.fix_map_run() + fo.fix_map(false) + expect.true_(true) +end diff --git a/test/plugins/flows.lua b/test/plugins/flows.lua new file mode 100644 index 0000000000..7973f905f9 --- /dev/null +++ b/test/plugins/flows.lua @@ -0,0 +1,10 @@ +config.mode = 'fortress' +config.target = 'flows' + +function test.counts_liquids() + local output, status = dfhack.run_command_silent('flows') + expect.eq(CR_OK, status) + expect.str_find('Blocks with liquid_1', output) + expect.str_find('Water tiles:', output) + expect.str_find('Magma tiles:', output) +end diff --git a/test/plugins/getplants.lua b/test/plugins/getplants.lua new file mode 100644 index 0000000000..95012e7769 --- /dev/null +++ b/test/plugins/getplants.lua @@ -0,0 +1,123 @@ +config.target = 'getplants' +config.mode = 'fortress' + +local function find_raw(id) + for _, raw in ipairs(df.global.world.raws.plants.all) do + if raw.id == id then return raw end + end +end + +local function run(...) + return dfhack.run_command_silent('getplants', ...) +end + +-- count shrubs carrying a gather designation (designation tile == plant tile +-- for shrubs, unlike trees) +local function count_marked(mat_idx) + local n = 0 + for _, p in ipairs(df.global.world.plants.all) do + if (mat_idx == nil or p.material == mat_idx) and + df.plant_type.attrs[p.type].is_shrub + then + local blk = dfhack.maps.getTileBlock(p.pos) + if blk and blk.designation[p.pos.x % 16][p.pos.y % 16].dig == + df.tile_dig_designation.Default + then + n = n + 1 + end + end + end + return n +end + +local function mat_has_product(mat, product) + if not mat then return false end + for _, id in ipairs(mat.reaction_product.id) do + if id.value == product then return true end + end + return false +end + +-- independent reimplementation of the brewable check used by --brewable +local function raw_is_brewable(raw) + local mi = dfhack.matinfo.decode( + raw.material_defs.type[df.plant_material_def.basic_mat], + raw.material_defs.idx[df.plant_material_def.basic_mat]) + if mat_has_product(mi.material, 'DRINK_MAT') then return true end + for _, g in ipairs(raw.growths) do + local gm = dfhack.matinfo.decode(g.mat_type, g.mat_index) + if mat_has_product(gm.material, 'DRINK_MAT') then return true end + end + return false +end + +function test.dry_run() + local before = count_marked() + local out = run('-s', '-a', '-n', '2', '-d') + local n_dry = tonumber(out:match('Would update (%d+) plant designations')) + expect.ne(nil, n_dry) + -- nothing may have been designated + expect.eq(before, count_marked()) + + -- a real run must update exactly the number the dry run reported + local out_real = run('-s', '-a', '-n', '2') + local n_real = tonumber(out_real:match('Updated (%d+) plant designations')) + expect.eq(n_dry, n_real) + expect.eq(before + n_real, count_marked()) + + -- dry-run clear reports the currently marked count without clearing + local out_cdry = run('-s', '-a', '-c', '-d') + local n_cdry = tonumber(out_cdry:match('Would update (%d+) plant designations')) + expect.eq(count_marked(), n_cdry) + expect.eq(before + n_real, count_marked()) + + local out_clear = run('-s', '-a', '-c') + local n_clear = tonumber(out_clear:match('Updated (%d+) plant designations')) + expect.eq(n_cdry, n_clear) + expect.eq(0, count_marked()) +end + +function test.trait_filters() + -- find a brewable and a non-brewable non-tree non-grass species + local brewable_id, other_id + for _, raw in ipairs(df.global.world.raws.plants.all) do + if not raw.flags.TREE and not raw.flags.GRASS then + if raw_is_brewable(raw) then + brewable_id = brewable_id or raw.id + else + other_id = other_id or raw.id + end + end + end + expect.ne(nil, brewable_id) + expect.ne(nil, other_id) + + -- a matching species is accepted by the filter + local out = run(brewable_id, '--brewable', '-d') + expect.str_find('Would update', out) + + -- a non-matching species is rejected with a specific message + local out2 = run(other_id, '--brewable', '-d') + expect.str_find('does not match the specified trait filters', out2) + + -- every species in the filtered ID listing must be brewable + local listed = run('--brewable') + local listed_count = 0 + for id in listed:gmatch('%* %(%a+%) ([%w_%-]+)') do + listed_count = listed_count + 1 + local raw = find_raw(id) + expect.true_(raw and raw_is_brewable(raw), id) + end + expect.true_(listed_count > 0) +end + +function test.cloth_listing() + local listed = run('--cloth') + local listed_count = 0 + for id in listed:gmatch('%* %(%a+%) ([%w_%-]+)') do + listed_count = listed_count + 1 + local raw = find_raw(id) + expect.true_(raw and raw.flags.THREAD, id) + end + expect.true_(listed_count > 0) +end diff --git a/test/plugins/infinite-sky.lua b/test/plugins/infinite-sky.lua new file mode 100644 index 0000000000..a799f79ebd --- /dev/null +++ b/test/plugins/infinite-sky.lua @@ -0,0 +1,29 @@ +config.mode = 'fortress' +config.target = 'infinite-sky' + +function test.status() + local output = dfhack.run_command_silent('infinite-sky') + expect.str_find('Construction monitoring is', output) +end + +function test.help_shows_usage() + local output, status = dfhack.run_command_silent('infinite-sky', 'help') + expect.true_(output:find('infinite%-sky') ~= nil + or output:find('z%-level', 1, true) ~= nil + or output:find('Usage', 1, true) ~= nil + or output:find('usage', 1, true) ~= nil) +end + +function test.rejects_non_numeric() + local output, status = dfhack.run_command_silent('infinite-sky', 'bogus') + expect.true_(output:find('positive integer', 1, true) ~= nil + or output:find('invalid', 1, true) ~= nil + or status ~= 0) +end + +function test.rejects_non_positive() + local output, status = dfhack.run_command_silent('infinite-sky', '-3') + expect.true_(output:find('positive integer', 1, true) ~= nil + or output:find('invalid', 1, true) ~= nil + or status ~= 0) +end diff --git a/test/plugins/kittens.lua b/test/plugins/kittens.lua new file mode 100644 index 0000000000..9c3cbd2fac --- /dev/null +++ b/test/plugins/kittens.lua @@ -0,0 +1,31 @@ +config.mode = 'fortress' +config.target = 'kittens' + +-- kittens is a developer plugin and is not built unless BUILD_DEV_PLUGINS is +-- on, so these tests are skipped on builds where throwtest is unavailable +local function has_throwtest() + for _, plugin in ipairs(dfhack.internal.listPlugins()) do + if plugin == 'kittens' then + for _, command in ipairs(dfhack.internal.listCommands(plugin)) do + if command == 'throwtest' then + return true + end + end + end + end + return false +end + +function test.exception_returns_failure() + if not has_throwtest() then return end + local output, status = dfhack.run_command_silent('throwtest') + expect.eq(CR_FAILURE, status) + expect.str_find('test exception', output) +end + +function test.nonstd_exception_returns_failure() + if not has_throwtest() then return end + local output, status = dfhack.run_command_silent('throwtest nonstd') + expect.eq(CR_FAILURE, status) + expect.str_find("Exception in kittens command", output) +end diff --git a/test/plugins/liquids.lua b/test/plugins/liquids.lua new file mode 100644 index 0000000000..3b121a06c2 --- /dev/null +++ b/test/plugins/liquids.lua @@ -0,0 +1,341 @@ +config.mode = 'fortress' +config.target = 'liquids' + +local dwarfmode = require('gui.dwarfmode') +local liquids = require('plugins.liquids') + +local function find_floor_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + local des = block.designation[x][y] + local occ = block.occupancy[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.FLOOR + and not des.hidden and des.flow_size == 0 + and occ.building == 0 then + return block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + end + end + end + end +end + +local function set_cursor(x, y, z) + dwarfmode.setCursorPos(xyz2pos(x, y, z)) +end + +local function clear_cursor() + dwarfmode.clearCursorPos() +end + +-- wait for any screen above the map to go away (e.g. a lingering load +-- screen, or the prerelease warning shown by dev builds on map load) +-- so that commands guarded by the cursor_hotkey UI check can run +local function ensure_dwarfmode_view() + delay_until(function() + local top = dfhack.gui.getCurViewscreen() + if df.viewscreen_dwarfmodest:is_instance(top) then return true end + if type(top.dismiss) == 'function' then + top:dismiss() + end + return false + end) +end + +local function des_at(x, y, z) + return select(1, dfhack.maps.getTileFlags(x, y, z)) +end + +function test.liquids_here_spawns_magma_at_cursor() + local x, y, z = find_floor_pos() + expect.ne(nil, x, 'test needs a revealed dry floor tile') + + local was_paused = dfhack.world.ReadPauseState() + return dfhack.with_finalize(function() + local des = des_at(x, y, z) + des.flow_size = 0 + dfhack.world.SetPauseState(was_paused) + clear_cursor() + end, function() + -- pause so the magma cannot spread before we clean it up + dfhack.world.SetPauseState(true) + ensure_dwarfmode_view() + set_cursor(x, y, z) + + local _, status = dfhack.run_command_silent('liquids-here') + expect.eq(CR_OK, status) + local des = des_at(x, y, z) + expect.eq(7, des.flow_size) + -- liquid_type is a 1-bit field exposed to Lua as a boolean + expect.eq(true, des.liquid_type) + end) +end + +local OPEN_SHAPES = { + [df.tiletype_shape.EMPTY] = true, + [df.tiletype_shape.RAMP_TOP] = true, +} + +-- find an open tile directly above another open, sky-lit tile +local function find_stacked_open_tiles() + for _, block in ipairs(df.global.world.map.map_blocks) do + local below = block.map_pos.z >= 1 and + dfhack.maps.getTileBlock(block.map_pos.x, block.map_pos.y, + block.map_pos.z - 1) or nil + if below then + for x = 0, 15 do + for y = 0, 15 do + local des_below = below.designation[x][y] + if des_below.light and des_below.outside + and OPEN_SHAPES[df.tiletype.attrs[ + block.tiletype[x][y]].shape] + and OPEN_SHAPES[df.tiletype.attrs[ + below.tiletype[x][y]].shape] then + return block.map_pos.x + x, block.map_pos.y + y, + block.map_pos.z + end + end + end + end + end +end + +-- find an open, sky-lit tile in the topmost allocated block of a map +-- column whose column to the east also reaches that z (needed by the +-- range test) +local function find_sky_top_pos() + for _, block in ipairs(df.global.world.map.map_blocks) do + local bx, by, bz = block.map_pos.x, block.map_pos.y, block.map_pos.z + if not dfhack.maps.getTileBlock(bx, by, bz + 1) + and dfhack.maps.getTileBlock(bx + 16, by, bz) then + for x = 0, 15 do + for y = 0, 15 do + local tt = block.tiletype[x][y] + local des = block.designation[x][y] + if df.tiletype.attrs[tt].shape == df.tiletype_shape.EMPTY + and des.light and des.outside then + return bx + x, by + y, bz + end + end + end + end + end +end + +-- detach a block from block_index so its position reads as unallocated; +-- ensureTileBlock can then materialize it from the block below in the +-- same column. returns a function that restores the original block +local function unallocate_block(x, y, z) + local column = df.global.world.map.block_index + [math.floor(x / 16)][math.floor(y / 16)] + local block = column[z] + column[z] = nil + expect.ne(nil, block, 'test needs an allocated block to detach') + expect.eq(nil, dfhack.maps.getTileBlock(x, y, z), + 'test setup could not detach the block') + return function() column[z] = block end +end + +-- snapshot the light/outside/subterranean flags of the open column below +-- pos; paint may propagate coverage down through all of it +local function snapshot_column_flags(x, y, z) + local column = {} + for dz = z, 0, -1 do + local b = dfhack.maps.getTileBlock(x, y, dz) + if not b or not OPEN_SHAPES[ + df.tiletype.attrs[b.tiletype[x % 16][y % 16]].shape] then + break + end + local d = b.designation[x % 16][y % 16] + column[dz] = {block=b, light=d.light, outside=d.outside, + subterranean=d.subterranean} + end + return column +end + +local function restore_column_flags(x, y, column) + for dz, orig in pairs(column) do + local d = orig.block.designation[x % 16][y % 16] + d.light = orig.light + d.outside = orig.outside + d.subterranean = orig.subterranean + end +end + +function test.paint_obsidian_covers_tiles_below() + local x, y, z = find_stacked_open_tiles() + expect.ne(nil, x, 'test needs vertically adjacent open, lit tiles') + + local block = dfhack.maps.getTileBlock(x, y, z) + local orig_tt = block.tiletype[x % 16][y % 16] + local orig_des_whole = block.designation[x % 16][y % 16].whole + local column = snapshot_column_flags(x, y, z - 1) + + return dfhack.with_finalize(function() + block.tiletype[x % 16][y % 16] = orig_tt + block.designation[x % 16][y % 16].whole = orig_des_whole + restore_column_flags(x, y, column) + end, function() + liquids.paint(xyz2pos(x, y, z), 'point', 'obsidian') + + expect.eq(df.tiletype.LavaWall, dfhack.maps.getTileType(x, y, z)) + local des = des_at(x, y, z - 1) + expect.false_(des.light) + expect.false_(des.outside) + end) +end + +function test.paint_obsidian_floor_covers_tiles_below() + local x, y, z = find_stacked_open_tiles() + expect.ne(nil, x, 'test needs vertically adjacent open, lit tiles') + + local block = dfhack.maps.getTileBlock(x, y, z) + local orig_tt = block.tiletype[x % 16][y % 16] + local orig_des_whole = block.designation[x % 16][y % 16].whole + local column = snapshot_column_flags(x, y, z - 1) + + return dfhack.with_finalize(function() + block.tiletype[x % 16][y % 16] = orig_tt + block.designation[x % 16][y % 16].whole = orig_des_whole + restore_column_flags(x, y, column) + end, function() + liquids.paint(xyz2pos(x, y, z), 'point', 'obsidian_floor') + + local tt = dfhack.maps.getTileType(x, y, z) + expect.eq(df.tiletype_shape.FLOOR, df.tiletype.attrs[tt].shape) + expect.eq(df.tiletype_material.LAVA_STONE, + df.tiletype.attrs[tt].material) + local des = des_at(x, y, z - 1) + expect.false_(des.light) + expect.false_(des.outside) + end) +end + +function test.paint_river_source_covers_tiles_below() + local x, y, z = find_stacked_open_tiles() + expect.ne(nil, x, 'test needs vertically adjacent open, lit tiles') + + local block = dfhack.maps.getTileBlock(x, y, z) + local orig_tt = block.tiletype[x % 16][y % 16] + local orig_des_whole = block.designation[x % 16][y % 16].whole + local column = snapshot_column_flags(x, y, z - 1) + + local was_paused = dfhack.world.ReadPauseState() + return dfhack.with_finalize(function() + block.tiletype[x % 16][y % 16] = orig_tt + block.designation[x % 16][y % 16].whole = orig_des_whole + restore_column_flags(x, y, column) + dfhack.world.SetPauseState(was_paused) + end, function() + -- pause so the spawned water cannot spread before we clean it up + dfhack.world.SetPauseState(true) + liquids.paint(xyz2pos(x, y, z), 'point', 'riversource') + + expect.eq(df.tiletype.RiverSource, dfhack.maps.getTileType(x, y, z)) + local des = des_at(x, y, z - 1) + expect.false_(des.light) + expect.false_(des.outside) + end) +end + +function test.paint_water_allocates_sky_block() + local x, y, z = find_sky_top_pos() + expect.ne(nil, x, 'test needs a top-of-column sky block') + + -- detach the block so its position is unallocated; the paint should + -- recreate it via ensureTileBlock + local restore_block = unallocate_block(x, y, z) + + local was_paused = dfhack.world.ReadPauseState() + return dfhack.with_finalize(function() + -- swap the original block back in; the block allocated by the + -- paint remains in map_blocks but is no longer referenced + restore_block() + dfhack.world.SetPauseState(was_paused) + end, function() + -- pause so the spawned water cannot spread before we clean it up + dfhack.world.SetPauseState(true) + liquids.paint(xyz2pos(x, y, z), 'point', 'water', 7) + + local block = dfhack.maps.getTileBlock(x, y, z) + expect.ne(nil, block, 'paint did not allocate the block') + local des = des_at(x, y, z) + expect.eq(7, des.flow_size) + -- liquid_type is a 1-bit field exposed to Lua as a boolean + expect.eq(false, des.liquid_type) + end) +end + +function test.paint_obsidian_allocates_sky_block() + local x, y, z = find_sky_top_pos() + expect.ne(nil, x, 'test needs a top-of-column sky block') + + local restore_block = unallocate_block(x, y, z) + + -- the painted wall also covers open tiles in the block below + local column = snapshot_column_flags(x, y, z - 1) + return dfhack.with_finalize(function() + restore_block() + restore_column_flags(x, y, column) + end, function() + liquids.paint(xyz2pos(x, y, z), 'point', 'obsidian') + + local block = dfhack.maps.getTileBlock(x, y, z) + expect.ne(nil, block, 'paint did not allocate the block') + expect.eq(df.tiletype.LavaWall, dfhack.maps.getTileType(x, y, z)) + end) +end + +function test.paint_water_range_into_unallocated_sky() + local x, y, z = find_sky_top_pos() + expect.ne(nil, x, 'test needs adjacent top-of-column sky blocks') + + -- the range brush paints forward from the cursor, so a 4x1x1 range + -- starting near the block edge paints two tiles in each block + local bx = x - (x % 16) + local x0, y0 = bx + 14, y + local restore_west = unallocate_block(bx, y0, z) + local restore_east = unallocate_block(bx + 16, y0, z) + + local was_paused = dfhack.world.ReadPauseState() + return dfhack.with_finalize(function() + restore_east() + restore_west() + dfhack.world.SetPauseState(was_paused) + end, function() + -- pause so the spawned water cannot spread before we clean it up + dfhack.world.SetPauseState(true) + liquids.paint(xyz2pos(x0, y0, z), 'range', 'water', 5, + xyz2pos(4, 1, 1)) + + for dx = 0, 3 do + local des = des_at(x0 + dx, y0, z) + expect.eq(5, des.flow_size, + ('tile (%d,%d,%d)'):format(x0 + dx, y0, z)) + end + end) +end + +function test.liquids_needs_console() + -- the interactive liquid spawner requires a console; the dispatcher + -- rejects non-console invocations before the plugin runs + local _, status = dfhack.run_command_silent('liquids') + expect.eq(CR_NEEDS_CONSOLE, status) +end + +function test.liquids_here_help_is_wrong_usage() + local _, status = dfhack.run_command_silent('liquids-here', '?') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.liquids_here_no_cursor_is_failure() + return dfhack.with_finalize(clear_cursor, function() + clear_cursor() + -- the cursor_hotkey command guard rejects before the plugin runs + local output, status = dfhack.run_command_silent('liquids-here') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('unsuitable UI state', output) + end) +end diff --git a/test/plugins/logistics.lua b/test/plugins/logistics.lua new file mode 100644 index 0000000000..cb942ed10e --- /dev/null +++ b/test/plugins/logistics.lua @@ -0,0 +1,125 @@ +config.mode = 'fortress' +config.target = 'logistics' + +local logistics = require('plugins.logistics') + +local saved_autoretrain = logistics.logistics_getFeature('autoretrain') +local saved_configs = {} + +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + logistics.logistics_setFeature(saved_autoretrain, 'autoretrain') + for sp_number, configs in pairs(saved_configs) do + for _,c in ipairs(configs) do + logistics.logistics_setStockpileConfig( + c.stockpile_number, c.melt == 1, c.trade == 1, + c.dump == 1, c.train == 1, c.forbid, + c.melt_masterworks == 1) + end + end + end, test_fn) +end + +local function snapshot_config(sp_number) + if saved_configs[sp_number] then return end + saved_configs[sp_number] = + logistics.logistics_getStockpileConfigs(sp_number) or {} +end + +local function get_first_stockpile() + local stockpiles = df.global.world.buildings.other.STOCKPILE + if #stockpiles == 0 then return nil end + return stockpiles[0].stockpile_number +end + +local function get_stats_for(sp_number) + for _,sp in ipairs(logistics.getStockpileData().sp_stats) do + if sp.sp_number == sp_number then return sp end + end +end + +function test.help() + expect.false_(logistics.parse_commandline{'help'}) + expect.false_(logistics.parse_commandline{'-h'}) +end + +function test.status() + expect.true_(logistics.parse_commandline{}) + expect.true_(logistics.parse_commandline{'status'}) +end + +function test.unrecognized_command() + expect.false_(logistics.parse_commandline{'bogus'}) +end + +function test.autoretrain_toggle() + expect.true_(logistics.parse_commandline{'enable', 'autoretrain'}) + expect.true_(logistics.logistics_getFeature('autoretrain')) + expect.true_(logistics.parse_commandline{'disable', 'autoretrain'}) + expect.false_(logistics.logistics_getFeature('autoretrain')) +end + +function test.unknown_feature() + expect.error_match('unknown feature', function() + logistics.parse_commandline{'enable', 'bogus_feature'} end) +end + +function test.missing_feature() + expect.error(function() + logistics.parse_commandline{'enable'} end) +end + +function test.getStockpileData() + local data = logistics.getStockpileData() + expect.eq(#df.global.world.buildings.other.STOCKPILE, + #data.sp_stats) + -- totals are the sum of the per-stockpile stats + for _,desig in ipairs{'melt', 'trade', 'dump', 'train', 'forbid'} do + local designated, designatable = 0, 0 + for _,sp in ipairs(data.sp_stats) do + designated = designated + sp[desig].designated + designatable = designatable + sp[desig].designatable + end + expect.eq(designated, data.sp_totals[desig].designated) + expect.eq(designatable, data.sp_totals[desig].designatable) + end +end + +function test.add_and_clear_config() + local sp_number = get_first_stockpile() + if not sp_number then return end + snapshot_config(sp_number) + expect.true_(logistics.parse_commandline{ + 'add', 'melt', '-s', tostring(sp_number)}) + local sp = get_stats_for(sp_number) + expect.true_(sp.melt.enabled) + expect.false_(sp.trade.enabled) + expect.true_(logistics.parse_commandline{ + 'add', 'trade', 'dump', '-s', tostring(sp_number)}) + sp = get_stats_for(sp_number) + expect.true_(sp.melt.enabled) + expect.true_(sp.trade.enabled) + expect.true_(sp.dump.enabled) + expect.true_(logistics.parse_commandline{ + 'clear', '-s', tostring(sp_number)}) + sp = get_stats_for(sp_number) + expect.false_(sp.melt.enabled) + expect.false_(sp.trade.enabled) + expect.false_(sp.dump.enabled) +end + +function test.add_invalid_stockpile() + expect.printerr_match('invalid stockpile', function() + logistics.parse_commandline{'add', 'melt', '-s', '999999'} end) +end + +function test.cycle() + -- a cycle can designate items if any stockpile configs are enabled; + -- only run it when the fort has none so no state is mutated + for _,desig in ipairs{'melt', 'trade', 'dump', 'train', 'forbid', 'claim'} do + if logistics.getStockpileData().sp_totals[desig].enabled_count > 0 then + return + end + end + expect.true_(logistics.parse_commandline{'now'}) +end diff --git a/test/plugins/misery.lua b/test/plugins/misery.lua new file mode 100644 index 0000000000..82b38d3621 --- /dev/null +++ b/test/plugins/misery.lua @@ -0,0 +1,60 @@ +config.mode = 'fortress' +config.target = 'misery' + +local misery = require('plugins.misery') + +local saved_factor = misery.misery_getFactor() + +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + if saved_factor >= 2 then + misery.misery_setFactor(saved_factor) + end + end, test_fn) +end + +function test.status() + expect.true_(misery.parse_commandline('status')) + expect.true_(misery.parse_commandline()) +end + +function test.help() + expect.false_(misery.parse_commandline('help')) + expect.false_(misery.parse_commandline('-h')) + expect.false_(misery.parse_commandline('--help')) +end + +function test.set_factor() + expect.true_(misery.parse_commandline('5')) + expect.eq(5, misery.misery_getFactor()) + expect.true_(misery.parse_commandline('2')) + expect.eq(2, misery.misery_getFactor()) +end + +function test.set_factor_below_minimum() + misery.misery_setFactor(5) + -- the C++ layer rejects the value with a console error message rather + -- than failing the command + misery.misery_setFactor(1) + expect.eq(5, misery.misery_getFactor()) + expect.true_(misery.parse_commandline('1')) + expect.eq(5, misery.misery_getFactor()) +end + +function test.clear() + expect.true_(misery.parse_commandline('clear')) +end + +function test.unrecognized_command() + expect.false_(misery.parse_commandline('bogus')) +end + +function test.status_output() + misery.misery_setFactor(5) + local lines = {} + mock.patch({{misery, 'print', function(line) + table.insert(lines, line) end}}, function() + misery.status() + end) + expect.str_find('misery factor is: 5', table.concat(lines, '\n')) +end diff --git a/test/plugins/nestboxes.lua b/test/plugins/nestboxes.lua new file mode 100644 index 0000000000..86a3f42265 --- /dev/null +++ b/test/plugins/nestboxes.lua @@ -0,0 +1,43 @@ +config.mode = 'fortress' +config.target = 'nestboxes' + +-- burrow config is persisted to the site; restore whatever was set +local saved_burrow_name + +config.wrapper = function(test_fn) + local output = dfhack.run_command_silent('nestboxes') + saved_burrow_name = output:match('inside burrow: ([^\n]+)') + return dfhack.with_finalize(function() + if saved_burrow_name then + dfhack.run_command_silent('nestboxes', 'burrow', + saved_burrow_name) + else + dfhack.run_command_silent('nestboxes', 'all') + end + end, test_fn) +end + +function test.status_reports_protection_scope() + local output = dfhack.run_command_silent('nestboxes') + expect.true_(output:find('nestboxes is', 1, true) ~= nil) + expect.true_(output:find('protecting', 1, true) ~= nil + or output:find('disabled', 1, true) ~= nil) +end + +function test.burrow_unknown_name_clears_scope() + dfhack.run_command_silent('nestboxes', 'burrow', + 'dfhack-test-nonexistent-burrow') + local output = dfhack.run_command_silent('nestboxes') + expect.true_(output:find('inside burrow', 1, true) == nil) +end + +function test.all_clears_burrow_scope() + dfhack.run_command_silent('nestboxes', 'all') + local output = dfhack.run_command_silent('nestboxes') + expect.true_(output:find('inside burrow', 1, true) == nil) +end + +function test.unknown_command_shows_usage() + local output, status = dfhack.run_command_silent('nestboxes', 'bogus') + expect.true_(output:find('nestboxes', 1, true) ~= nil or status ~= 0) +end diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index ab2ad3235e..dab396ba96 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -1,7 +1,7 @@ config.mode = 'fortress' config.target = 'orders' -local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json' +local FILE_PATH_PATTERN = dfhack.getConfigPath() .. '/orders/%s.json' local BACKUP_FILE_NAME = 'tmp-backup' local BACKUP_FILE_PATH = FILE_PATH_PATTERN:format(BACKUP_FILE_NAME) @@ -190,6 +190,7 @@ function test.import_export_reaction_condition() } ], "job" : "CustomReaction", + "name" : "Make soap from tallow", "reaction" : "MAKE_SOAP_FROM_TALLOW" } ] diff --git a/test/plugins/overlay.lua b/test/plugins/overlay.lua new file mode 100644 index 0000000000..0f7e5d1a48 --- /dev/null +++ b/test/plugins/overlay.lua @@ -0,0 +1,47 @@ +config.mode = 'fortress' +config.target = 'overlay' + +local overlay = require('plugins.overlay') + +-- overlay enable state persists via save_config; restore originals +local saved_enabled + +config.wrapper = function(test_fn) + saved_enabled = {} + return dfhack.with_finalize(function() + for name, enabled in pairs(saved_enabled) do + dfhack.run_command_silent('overlay', + enabled and 'enable' or 'disable', + name) + end + end, test_fn) +end + +local function save_widget_state(name) + local cfg = overlay.get_state().config[name] + saved_enabled[name] = cfg and cfg.enabled or false +end + +-- overlay_command suppresses prints under run_command_silent (quiet +-- flag), so verify state changes directly instead of output +function test.list_succeeds() + local output, status = dfhack.run_command_silent('overlay', 'list') + expect.eq(0, status) +end + +function test.enable_disable_widget() + overlay.rescan() + local db = overlay.get_state().db + local name = next(db) + expect.ne(nil, name) + save_widget_state(name) + dfhack.run_command_silent('overlay', 'disable', name) + expect.false_(overlay.get_state().config[name].enabled) + dfhack.run_command_silent('overlay', 'enable', name) + expect.true_(overlay.get_state().config[name].enabled) +end + +function test.unknown_command_shows_usage() + local output, status = dfhack.run_command_silent('overlay', 'bogus') + expect.true_(output:find('overlay', 1, true) ~= nil or status ~= 0) +end diff --git a/test/plugins/plant.lua b/test/plugins/plant.lua new file mode 100644 index 0000000000..2079713505 --- /dev/null +++ b/test/plugins/plant.lua @@ -0,0 +1,118 @@ +config.mode = 'fortress' +config.target = 'plant' + +local plant = require('plugins.plant') + +-- parse_commandline takes C++-allocated outputs: an options table, two +-- coords, and a vector filter. stl vectors can't be created from +-- Lua, so borrow an int32 vector field from a temporary burrow. +local function parse(args) + local opts = {} + local pos1, pos2 = xyz2pos(0, 0, -30000), xyz2pos(0, 0, -30000) + local b = df.burrow:new() + local ok, err = pcall(plant.parse_commandline, opts, pos1, pos2, b.block_x, args) + -- copy out before deleting the burrow; the vector dangles with it + local filter = {} + for i = 0, #b.block_x - 1 do filter[i + 1] = b.block_x[i] end + local result = {opts=opts, pos1=pos1, pos2=pos2, filter=filter} + b:delete() + if not ok then error(err, 0) end + return result +end + +local function find_valid_plant() + for _,p in ipairs(df.global.world.raws.plants.bushes) do + if p.id and #p.id > 0 then return p end + end +end + +function test.modes_set_flags() + expect.eq(-2, parse({'list'}).opts.plant_idx) + expect.true_(parse({'grow'}).opts.grow) + expect.true_(parse({'remove'}).opts.del) +end + +function test.create_requires_plant_id() + expect.error_match('Must specify plant_id', + function() parse({'create'}) end) +end + +function test.create_resolves_plant_by_name_and_index() + local p = find_valid_plant() + if not p then return end + local by_name = parse({'create', p.id}) + expect.true_(by_name.opts.create) + expect.eq(p.index, by_name.opts.plant_idx) + expect.eq(p.index, parse({'create', tostring(p.index)}).opts.plant_idx) +end + +function test.create_unknown_plant_errors() + expect.error_match('Plant raw not found', + function() parse({'create', 'DFHACK_NO_SUCH_PLANT'}) end) +end + +function test.invalid_and_missing_modes_error() + expect.error_match('Specify mode', function() parse({}) end) + expect.error_match('Invalid mode', function() parse({'bogus'}) end) +end + +function test.too_many_positionals_error() + expect.error_match('Too many positionals', + function() parse({'grow', '1,1,100', '2,2,100', 'extra'}) end) +end + +function test.age_option_parsing() + expect.eq(40320*3-1, parse({'grow', '--age', 'tree'}).opts.age) + expect.eq(40320*5-1, parse({'grow', '--age', '5'}).opts.age) + expect.eq(40320*1250-1, parse({'grow', '--age', '9999'}).opts.age) + expect.error_match('Invalid age', + function() parse({'grow', '--age', 'bogus'}) end) +end + +function test.filter_and_exclude_populate_vector() + local p = find_valid_plant() + if not p then return end + local res = parse({'remove', '--filter', p.id}) + expect.eq(1, #res.filter) + expect.eq(p.index, res.filter[1]) + res = parse({'remove', '--exclude', p.id}) + expect.true_(res.opts.filter_ex) +end + +function test.double_filter_errors() + expect.error_match('Filter already defined', + function() + local p = find_valid_plant() + if not p then error('skip') end + parse({'remove', '--filter', p.id, '--exclude', p.id}) + end) +end + +function test.option_flags() + local opts = parse({'remove', '-s', '-p', '-t', '-d', '-z', '-n', '-c'}).opts + expect.true_(opts.shrubs) + expect.true_(opts.saplings) + expect.true_(opts.trees) + expect.true_(opts.dead) + expect.true_(opts.zlevel) + expect.true_(opts.dry_run) + expect.true_(opts.force) +end + +function test.coords_assigned_to_positions() + local res = parse({'grow', '3,4,100', '7,8,101'}) + expect.eq(3, res.pos1.x) + expect.eq(4, res.pos1.y) + expect.eq(100, res.pos1.z) + expect.eq(7, res.pos2.x) + expect.eq(8, res.pos2.y) + expect.eq(101, res.pos2.z) +end + +function test.create_uses_third_positional_as_pos1() + local p = find_valid_plant() + if not p then return end + local res = parse({'create', p.id, '9,10,102'}) + expect.eq(9, res.pos1.x) + expect.eq(102, res.pos1.z) +end diff --git a/test/plugins/preserve-rooms.lua b/test/plugins/preserve-rooms.lua new file mode 100644 index 0000000000..47a350d91f --- /dev/null +++ b/test/plugins/preserve-rooms.lua @@ -0,0 +1,49 @@ +config.mode = 'fortress' +config.target = 'preserve-rooms' + +local pr = require('plugins.preserve-rooms') + +-- feature enabled state persists across sessions; restore it +local saved_features + +config.wrapper = function(test_fn) + local features = pr.preserve_rooms_getState() + saved_features = {} + for k, v in pairs(features) do + saved_features[k] = v + end + return dfhack.with_finalize(function() + for k, v in pairs(saved_features) do + pr.preserve_rooms_setFeature(v, k) + end + end, test_fn) +end + +function test.status_lists_features() + local output = dfhack.run_command_silent('preserve-rooms', 'status') + expect.str_find('track%-missions', output) + expect.str_find('track%-roles', output) +end + +function test.enable_disable_feature() + pr.preserve_rooms_setFeature(false, 'track-missions') + dfhack.run_command_silent('preserve-rooms', 'enable', 'track-missions') + local features = pr.preserve_rooms_getState() + expect.true_(features['track-missions']) + dfhack.run_command_silent('preserve-rooms', 'disable', 'track-missions') + features = pr.preserve_rooms_getState() + expect.false_(features['track-missions']) +end + +function test.enable_unknown_feature_errors() + local output, status = dfhack.run_command_silent('preserve-rooms', + 'enable', 'bogus-feature') + expect.true_(output:find('unknown feature', 1, true) ~= nil) +end + +function test.reset_feature() + dfhack.run_command_silent('preserve-rooms', 'reset', 'track-roles') + local output, status = dfhack.run_command_silent('preserve-rooms', + 'reset', 'bogus-feature') + expect.true_(output:find('unknown feature', 1, true) ~= nil) +end diff --git a/test/plugins/preserve-tombs.lua b/test/plugins/preserve-tombs.lua new file mode 100644 index 0000000000..d5dfbb4dbc --- /dev/null +++ b/test/plugins/preserve-tombs.lua @@ -0,0 +1,34 @@ +config.mode = 'fortress' +config.target = 'preserve-tombs' + +-- enabled state persists; capture it and restore afterwards +local was_enabled + +config.wrapper = function(test_fn) + local output = dfhack.run_command_silent('preserve-tombs') + was_enabled = output:find('currently enabled', 1, true) ~= nil + return dfhack.with_finalize(function() + dfhack.run_command_silent(was_enabled and 'enable' or 'disable', + 'preserve-tombs') + end, test_fn) +end + +function test.status_reports_state() + dfhack.run_command_silent('disable', 'preserve-tombs') + local output = dfhack.run_command_silent('preserve-tombs') + expect.str_find('preserve%-tombs is currently disabled', output) +end + +function test.now_requires_enable() + dfhack.run_command_silent('disable', 'preserve-tombs') + local output = dfhack.run_command_silent('preserve-tombs', 'now') + expect.str_find('Cannot update', output) +end + +function test.enable_then_now() + dfhack.run_command_silent('enable', 'preserve-tombs') + local output = dfhack.run_command_silent('preserve-tombs') + expect.str_find('preserve%-tombs is currently enabled', output) + output = dfhack.run_command_silent('preserve-tombs', 'now') + expect.str_find('Updated tomb assignments', output) +end diff --git a/test/plugins/probe.lua b/test/plugins/probe.lua new file mode 100644 index 0000000000..078cef0098 --- /dev/null +++ b/test/plugins/probe.lua @@ -0,0 +1,66 @@ +config.mode = 'fortress' +config.target = 'probe' + +local saved_cursor + +config.wrapper = function(test_fn) + saved_cursor = xyz2pos(df.global.cursor.x, df.global.cursor.y, + df.global.cursor.z) + return dfhack.with_finalize(function() + df.global.cursor.x = saved_cursor.x + df.global.cursor.y = saved_cursor.y + df.global.cursor.z = saved_cursor.z + end, test_fn) +end + +local function set_cursor(pos) + df.global.cursor.x = pos.x + df.global.cursor.y = pos.y + df.global.cursor.z = pos.z +end + +local function first_map_block() + return df.global.world.map.map_blocks[0] +end + +local function any_map_pos() + local block = first_map_block() + return xyz2pos(block.map_pos.x + 8, block.map_pos.y + 8, + block.map_pos.z) +end + +function test.probe_requires_cursor() + set_cursor(xyz2pos(-30000, -30000, -30000)) + local output = dfhack.run_command_silent('probe') + expect.str_find('No cursor', output) +end + +function test.probe_reports_tiletype() + set_cursor(any_map_pos()) + local output = dfhack.run_command_silent('probe') + expect.str_find('tiletype', output) + expect.str_find('dig', output) +end + +-- regression: probe must describe the block actually under the cursor, not a +-- block at a different z-level +function test.probe_reports_correct_block() + local block = first_map_block() + set_cursor(any_map_pos()) + local output = dfhack.run_command_silent('probe') + + local tiletype = tonumber(output:match('tiletype: (%-?%d+)')) + expect.eq(block.tiletype[8][8], tiletype) + + local local_idx = tonumber(output:match('local feature idx: (%-?%d+)')) + local global_idx = tonumber(output:match('global feature idx: (%-?%d+)')) + expect.eq(block.local_feature, local_idx) + expect.eq(block.global_feature, global_idx) +end + +function test.cprobe_requires_unit() + set_cursor(xyz2pos(-30000, -30000, -30000)) + local output = dfhack.run_command_silent('cprobe') + -- fails without a selected creature + expect.ne('', output) +end diff --git a/test/plugins/prospector.lua b/test/plugins/prospector.lua new file mode 100644 index 0000000000..42b0016f60 --- /dev/null +++ b/test/plugins/prospector.lua @@ -0,0 +1,55 @@ +config.mode = 'fortress' +config.target = 'prospector' + +local prospector = require('plugins.prospector') + +local function parse(args) + local opts = {} + prospector.parse_commandline(opts, args) + return opts +end + +function test.help_flag() + expect.true_(parse({'--help'}).help) + expect.true_(parse({'-h'}).help) +end + +function test.all_and_hell_set_hidden() + expect.true_(parse({'all'}).hidden) + expect.nil_(parse({'all'}).tube) + local opts = parse({'hell'}) + expect.true_(opts.hidden) + expect.true_(opts.tube) +end + +function test.unknown_keyword_errors() + expect.error_match('unknown keyword', + function() prospector.parse_commandline({}, {'bogus'}) end) +end + +function test.show_sections_set_flags() + local opts = parse({'-s', 'gems,ores'}) + expect.true_(opts.gems) + expect.true_(opts.ores) + -- selecting any section disables the rest + expect.false_(opts.summary) + expect.false_(opts.layers) +end + +function test.show_all_sections() + local opts = parse({'-s', 'summary,liquids,layers,features,ores,gems,veins,shrubs,trees'}) + for _,s in ipairs{'summary', 'liquids', 'layers', 'features', 'ores', + 'gems', 'veins', 'shrubs', 'trees'} do + expect.true_(opts[s], 'expected section flag set: '..s) + end +end + +function test.unknown_show_section_errors() + expect.error_match('unknown report section', + function() prospector.parse_commandline({}, {'-s', 'bogus'}) end) +end + +function test.values_flag() + expect.true_(parse({'-v'}).value) + expect.true_(parse({'--values'}).value) +end diff --git a/test/plugins/regrass.lua b/test/plugins/regrass.lua new file mode 100644 index 0000000000..73953564f3 --- /dev/null +++ b/test/plugins/regrass.lua @@ -0,0 +1,71 @@ +config.mode = 'fortress' +config.target = 'regrass' + +local regrass = require('plugins.regrass') + +local function parse(args) + local opts, pos1, pos2 = {}, {}, {} + return regrass.parse_commandline(opts, pos1, pos2, args), opts, pos1, pos2 +end + +function test.plant_requires_force() + expect.error_match('without %-%-force', + function() parse({'--plant', 'GRASS'}) end) +end + +function test.plant_with_force_resolves_grass_by_name() + local grasses = df.global.world.raws.plants.grasses + if #grasses == 0 then return end + local grass = grasses[0] + local _, opts = parse({'--force', '--plant', grass.id}) + expect.eq(grass.index, opts.forced_plant) +end + +function test.plant_with_force_resolves_grass_by_index() + local _, opts = parse({'--force', '--plant', '0'}) + expect.eq(0, opts.forced_plant) +end + +function test.unknown_plant_errors() + expect.error_match('Plant raw not found', + function() parse({'--force', '--plant', 'NO_SUCH_PLANT_XYZ'}) end) +end + +function test.list_flag_forces_plant_listing() + local _, opts = parse({'--list'}) + expect.eq(-2, opts.forced_plant) +end + +function test.force_without_plant_picks_random_grass() + if #df.global.world.raws.plants.grasses == 0 then return end + local _, opts = parse({'--force'}) + expect.true_(type(opts.forced_plant) == 'number') + expect.true_(opts.forced_plant >= 0) +end + +function test.option_flags_set() + local _, opts = parse({'--max', '--new', '--ashes', '--buildings', + '--mud', '--block', '--zlevel'}) + expect.true_(opts.max_grass) + expect.true_(opts.new_grass) + expect.true_(opts.ashes) + expect.true_(opts.buildings) + expect.true_(opts.mud) + expect.true_(opts.block) + expect.true_(opts.zlevel) +end + +function test.too_many_positionals_error() + expect.error_match('Too many positionals', + function() parse({'1,1,1', '2,2,2', '3,3,3'}) end) +end + +function test.positionals_assign_coords() + local _, _, pos1, pos2 = parse({'10,11,95', '20,21,96'}) + expect.eq(10, pos1.x) + expect.eq(11, pos1.y) + expect.eq(95, pos1.z) + expect.eq(20, pos2.x) + expect.eq(21, pos2.y) + expect.eq(96, pos2.z) +end diff --git a/test/plugins/reveal.lua b/test/plugins/reveal.lua new file mode 100644 index 0000000000..7c9fe6a45f --- /dev/null +++ b/test/plugins/reveal.lua @@ -0,0 +1,78 @@ +config.mode = 'fortress' +config.target = 'reveal' + +-- reveal/unreveal mutate every map block's hidden flag, and the plugin +-- force-pauses the game while the map is revealed. Every test that reveals +-- must unreveal in a finalize so a failed check can't leave the game +-- revealed (and stuck paused) for the rest of the suite. +local function ensure_unrevealed() + dfhack.run_command_silent('unreveal') +end + +local function count_hidden() + local count = 0 + for _, block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + if block.designation[x][y].hidden then + count = count + 1 + end + end + end + end + return count +end + +function test.reveal_and_unreveal() + local before = count_hidden() + expect.true_(before > 0, 'test needs at least one hidden tile on the map') + + return dfhack.with_finalize(ensure_unrevealed, function() + local output, status = dfhack.run_command_silent('reveal') + expect.eq(CR_OK, status) + expect.str_find('Map revealed%.', output) + -- blocks containing trigger events (encased horrors, treasure veins) + -- stay hidden by design, so compare counts rather than spot tiles + expect.lt(count_hidden(), before) + + output, status = dfhack.run_command_silent('unreveal') + expect.eq(CR_OK, status) + expect.eq(before, count_hidden()) + end) +end + +function test.reveal_twice_fails() + return dfhack.with_finalize(ensure_unrevealed, function() + dfhack.run_command_silent('reveal') + local output, status = dfhack.run_command_silent('reveal') + expect.eq(CR_FAILURE, status) + expect.str_find('already revealed', output) + end) +end + +function test.unreveal_nothing_fails() + local output, status = dfhack.run_command_silent('unreveal') + expect.eq(CR_FAILURE, status) + expect.str_find('nothing to revert', output) +end + +function test.demon_is_disabled() + local output, status = dfhack.run_command_silent('reveal', 'demon') + expect.eq(CR_FAILURE, status) + expect.str_find('currently disabled', output) +end + +function test.revtoggle_toggles() + local before = count_hidden() + expect.true_(before > 0, 'test needs at least one hidden tile on the map') + + return dfhack.with_finalize(ensure_unrevealed, function() + local _, status = dfhack.run_command_silent('revtoggle') + expect.eq(CR_OK, status) + expect.lt(count_hidden(), before) + + _, status = dfhack.run_command_silent('revtoggle') + expect.eq(CR_OK, status) + expect.eq(before, count_hidden()) + end) +end diff --git a/test/plugins/seedwatch.lua b/test/plugins/seedwatch.lua new file mode 100644 index 0000000000..8af3e7ebab --- /dev/null +++ b/test/plugins/seedwatch.lua @@ -0,0 +1,111 @@ +config.mode = 'fortress' +config.target = 'seedwatch' + +local seedwatch = require('plugins.seedwatch') + +local saved_targets = {} +do + local watch_map = seedwatch.seedwatch_getData() + for k, v in pairs(watch_map) do + saved_targets[k] = v + end +end + +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + -- restore original targets and zero out any watch entries that the + -- test created for previously unwatched plants (there is no API to + -- remove a watch entry entirely) + local watch_map = seedwatch.seedwatch_getData() + for k in pairs(watch_map) do + if saved_targets[k] == nil then saved_targets[k] = 0 end + end + for k, v in pairs(saved_targets) do + local plant = df.global.world.raws.plants.all[k] + if plant then + seedwatch.seedwatch_setTarget(plant.id, v) + end + end + end, test_fn) +end + +-- finds a seed-bearing non-tree plant, matching the set of plant ids that +-- seedwatch manages (see plugin_load_world_data in seedwatch.cpp) +local function get_seed_plant() + local plants = df.global.world.raws.plants.all + for i = 0, #plants - 1 do + local plant = plants[i] + if plant.material_defs.type[df.plant_material_def.seed] ~= -1 and + not plant.flags.TREE then + return i, plant.id + end + end +end + +local function all_targets() + local watch_map = seedwatch.seedwatch_getData() + return watch_map +end + +function test.status() + expect.true_(seedwatch.parse_commandline('status')) + expect.true_(seedwatch.parse_commandline()) +end + +function test.help() + expect.false_(seedwatch.parse_commandline('help')) + expect.false_(seedwatch.parse_commandline('-h')) +end + +function test.set_all_targets() + expect.true_(seedwatch.parse_commandline('all', '50')) + local targets = all_targets() + local count = 0 + for _ in pairs(targets) do count = count + 1 end + expect.gt(count, 0) + for _, v in pairs(targets) do + expect.eq(50, v) + end +end + +function test.clear() + expect.true_(seedwatch.parse_commandline('clear')) + for _, v in pairs(all_targets()) do + expect.eq(0, v) + end +end + +function test.set_single_plant() + local idx, id = get_seed_plant() + if not idx then return end + expect.true_(seedwatch.parse_commandline(id, '17')) + expect.eq(17, all_targets()[idx]) +end + +function test.target_floored() + local idx, id = get_seed_plant() + if not idx then return end + expect.true_(seedwatch.parse_commandline(id, '9.9')) + expect.eq(9, all_targets()[idx]) +end + +function test.unknown_plant() + local idx = get_seed_plant() + -- the C++ layer prints an error message but the command does not fail + expect.true_(seedwatch.parse_commandline( + 'THIS_PLANT_DOES_NOT_EXIST', '5')) + if idx then + expect.ne(5, all_targets()[idx]) + end +end + +function test.negative_target() + local _, id = get_seed_plant() + if not id then return end + expect.error_match('non%-negative integer', function() + seedwatch.parse_commandline(id, '-5') end) +end + +function test.missing_target() + expect.false_(seedwatch.parse_commandline('all')) +end diff --git a/test/plugins/showmood.lua b/test/plugins/showmood.lua new file mode 100644 index 0000000000..12478db3be --- /dev/null +++ b/test/plugins/showmood.lua @@ -0,0 +1,14 @@ +config.mode = 'fortress' +config.target = 'showmood' + +function test.reports_no_active_mood() + -- the test fort has no strange mood in progress + local output, status = dfhack.run_command_silent('showmood') + expect.eq(CR_OK, status) + expect.str_find('No strange moods currently active', output) +end + +function test.arg_is_wrong_usage() + local _, status = dfhack.run_command_silent('showmood', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end diff --git a/test/plugins/sort.lua b/test/plugins/sort.lua new file mode 100644 index 0000000000..d417d31960 --- /dev/null +++ b/test/plugins/sort.lua @@ -0,0 +1,163 @@ +config.mode = 'fortress' +config.target = 'sort' + +local gui = require('gui') +local overlay = require('plugins.overlay') + +local function get_widget() + return overlay.get_state().db['sort.stress_icons'].widget +end + +local function get_citizen_counts() + local counts = {} + for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do + local cat = dfhack.units.getStressCategory(unit) + counts[cat] = (counts[cat] or 0) + 1 + end + return counts +end + +-- rects are stored happiest-first, so category c is at index 7-c +local function rect_for_category(w, cat) + if not w.icon_rects then qerror('icon_rects not populated') end + return w.icon_rects[7 - cat] +end + +local function click_at(x, y) + local handled + mock.patch(dfhack.screen, 'getMousePos', mock.func(x, y), function() + handled = get_widget():onInput{_MOUSE_L=true} + end) + return handled +end + +local function dismiss_top() + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'LEAVESCREEN') +end + +local function synth_rects() + local rects = {} + for i = 0, 6 do + local x1 = 42 + i * 3 + table.insert(rects, {x1=x1, y1=0, x2=x1+2, y2=2, category=6-i}) + end + return rects +end + +config.wrapper = function(test_fn) + overlay.rescan() + local w = get_widget() + -- give the top bar a few frames to render so the anchor scan finds 'Pop'; + -- environments where the vanilla screen is never rendered (e.g. headless + -- Windows CI) fall back to synthetic rects so the click/list behavior is + -- still exercised + for _ = 1, 20 do + w:overlay_onupdate() + if w.icon_rects then break end + delay() + end + w.scan_unavailable = not w.icon_rects + if w.scan_unavailable then + print('NOTE: top bar not rendered; testing with synthetic icon rects') + w.icon_rects = synth_rects() + end + return dfhack.with_finalize(function() + -- dismiss any screens the test left open + while (dfhack.gui.getCurFocus(true)[1] or ''):match('^dfhack/') do + dismiss_top() + end + end, test_fn) +end + +function test.icon_rects_detected() + local w = get_widget() + if w.scan_unavailable then + -- the vanilla top bar is never rendered in this environment, so the + -- screen scan cannot be verified here (covered by other platforms) + return + end + expect.eq(7, #w.icon_rects) + expect.eq(6, w.icon_rects[1].category) + expect.eq(0, w.icon_rects[7].category) + -- each rect covers a 3-wide, 3-tall cell in the top bar + for _,rect in ipairs(w.icon_rects) do + expect.eq(0, rect.y1) + expect.eq(2, rect.y2) + expect.eq(2, rect.x2 - rect.x1) + end +end + +function test.click_outside_icons_does_nothing() + local w = get_widget() + local before = dfhack.gui.getCurFocus(true)[1] + expect.false_(click_at(0, 30)) + expect.eq(before, dfhack.gui.getCurFocus(true)[1]) +end + +function test.click_icon_opens_filtered_list() + local w = get_widget() + local counts = get_citizen_counts() + local cat + for c = 0, 6 do + if (counts[c] or 0) > 0 then cat = c break end + end + expect.ne(nil, cat, 'no citizens with a stress category') + + local rect = rect_for_category(w, cat) + expect.true_(click_at(rect.x1 + 1, 1)) + expect.eq('dfhack/lua/ListBox', dfhack.gui.getCurFocus(true)[1]) + dismiss_top() +end + +function test.empty_category_shows_message() + local w = get_widget() + local counts = get_citizen_counts() + local cat + for c = 0, 6 do + if not counts[c] or counts[c] == 0 then cat = c break end + end + if not cat then + -- all categories populated; nothing to test + return + end + local rect = rect_for_category(w, cat) + expect.true_(click_at(rect.x1 + 1, 1)) + expect.true_((dfhack.gui.getCurFocus(true)[1] or ''):match('^dfhack/lua/') ~= nil) + dismiss_top() +end + +function test.selecting_unit_centers_map() + local w = get_widget() + local counts = get_citizen_counts() + local cat + for c = 0, 6 do + if (counts[c] or 0) > 0 then cat = c break end + end + expect.ne(nil, cat, 'no citizens with a stress category') + + local rect = rect_for_category(w, cat) + expect.true_(click_at(rect.x1 + 1, 1)) + expect.eq('dfhack/lua/ListBox', dfhack.gui.getCurFocus(true)[1]) + + local reveal_spy = mock.observe_func(function() end) + mock.patch(dfhack.gui, 'revealInDwarfmodeMap', reveal_spy, + function() + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'SELECT') + end) + expect.eq(1, reveal_spy.call_count) + local args = reveal_spy.call_args[1] + local called_with = {x=args[1], y=args[2], z=args[3]} + + expect.ne(nil, called_with) + -- the revealed position must be a real citizen's position in that bucket + local matched = false + for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do + if dfhack.units.getStressCategory(unit) == cat and + unit.pos.x == called_with.x and unit.pos.y == called_with.y and + unit.pos.z == called_with.z then + matched = true + break + end + end + expect.true_(matched) +end diff --git a/test/plugins/spectate.lua b/test/plugins/spectate.lua new file mode 100644 index 0000000000..e6887d1f6f --- /dev/null +++ b/test/plugins/spectate.lua @@ -0,0 +1,51 @@ +config.mode = 'fortress' +config.target = 'spectate' + +require('plugins.spectate') + +function test.status_shows_settings() + local output = dfhack.run_command_silent('spectate') + expect.true_(output:find('auto%-unpause') ~= nil + or output:find('follow%-seconds') ~= nil) +end + +function test.set_boolean() + dfhack.run_command_silent('spectate', 'set', 'auto-unpause', 'true') + local output = dfhack.run_command_silent('spectate') + expect.str_find('auto%-unpause.-true', output) + dfhack.run_command_silent('spectate', 'set', 'auto-unpause', 'false') + output = dfhack.run_command_silent('spectate') + expect.str_find('auto%-unpause.-false', output) +end + +function test.set_number() + dfhack.run_command_silent('spectate', 'set', 'follow-seconds', '42') + local output = dfhack.run_command_silent('spectate') + expect.str_find('follow%-seconds.-42', output) + dfhack.run_command_silent('spectate', 'set', 'follow-seconds', '10') +end + +function test.set_toggle_value() + dfhack.run_command_silent('spectate', 'set', 'auto-unpause', 'false') + dfhack.run_command_silent('spectate', 'set', 'auto-unpause', 'toggle') + local output = dfhack.run_command_silent('spectate') + expect.str_find('auto%-unpause.-true', output) +end + +function test.set_unknown_option() + local output, status = dfhack.run_command_silent('spectate', 'set', + 'bogus-key', '1') + expect.true_(output:find('unknown option', 1, true) ~= nil) +end + +function test.set_missing_key() + local output, status = dfhack.run_command_silent('spectate', 'set') + expect.true_(output:find('missing key', 1, true) ~= nil) +end + +function test.unknown_command_shows_usage() + local output, status = dfhack.run_command_silent('spectate', 'boguscmd') + expect.true_(output:find('status', 1, true) ~= nil + or output:find('Usage', 1, true) ~= nil + or output:find('usage', 1, true) ~= nil) +end diff --git a/test/plugins/stocks.lua b/test/plugins/stocks.lua new file mode 100644 index 0000000000..0dba35c00a --- /dev/null +++ b/test/plugins/stocks.lua @@ -0,0 +1,87 @@ +config.mode = 'fortress' +config.target = 'overlay' + +local gui = require('gui') +local overlay = require('plugins.overlay') + +local stocks = df.global.game.main_interface.stocks + +-- feed a key to the stocks overlay widget's input handler. This is the same +-- code path the viewscreen's interposed feed() takes; we invoke it directly +-- because the focus string cache only refreshes on game frames, which tests +-- cannot wait for. +local function send_key(key) + overlay.get_state().db['stocks.overlay'].widget:onInput({[key]=true}) +end + +local function set_stocks_open(open) + if stocks.open ~= open then + gui.simulateInput(dfhack.gui.getDFViewscreen(true), 'D_STOCKS') + end +end + +local function expanded_count() + local n = 0 + for i=0,#stocks.current_type_a_expanded-1 do + if stocks.current_type_a_expanded[i] then n = n + 1 end + end + return n +end + +local saved_open + +config.wrapper = function(test_fn) + saved_open = stocks.open + -- the widget db can be empty if a rescan is still in progress + overlay.rescan() + return dfhack.with_finalize(function() + -- close and reopen so the game repopulates any lists the test mutated + set_stocks_open(false) + set_stocks_open(saved_open) + end, test_fn) +end + +function test.toggle_collapses_then_expands() + set_stocks_open(true) + local num_sections = #stocks.current_type_a_expanded + expect.true_(num_sections > 0) + for i=0,num_sections-1 do + stocks.current_type_a_expanded[i] = true + end + stocks.scroll_position_item = 5 + send_key('CUSTOM_CTRL_X') + expect.eq(0, expanded_count()) + -- collapsing resets the item scroll position so it stays in bounds + expect.eq(0, stocks.scroll_position_item) + -- with everything collapsed, the same hotkey expands + send_key('CUSTOM_CTRL_X') + expect.eq(num_sections, expanded_count()) +end + +function test.expand_hotkey_expands_all() + set_stocks_open(true) + for i=0,#stocks.current_type_a_expanded-1 do + stocks.current_type_a_expanded[i] = false + end + send_key('CUSTOM_CTRL_Z') + expect.eq(#stocks.current_type_a_expanded, expanded_count()) +end + +function test.remove_empty_keeps_type_scroll_in_bounds() + set_stocks_open(true) + stocks.scroll_position_type = #stocks.filtered_type_list + 10 + send_key('CUSTOM_CTRL_E') + expect.true_(stocks.scroll_position_type >= 0) + expect.true_(stocks.scroll_position_type < #stocks.filtered_type_list) +end + +function test.closed_stocks_ignores_overlay_keys() + set_stocks_open(false) + -- the stocks.open guard must make every hotkey a no-op while the page is + -- closed, even if an activation slips through as the page closes + local n = expanded_count() + send_key('CUSTOM_CTRL_X') + send_key('CUSTOM_CTRL_Z') + send_key('CUSTOM_CTRL_E') + expect.eq(n, expanded_count()) +end diff --git a/test/plugins/strangemood.lua b/test/plugins/strangemood.lua new file mode 100644 index 0000000000..3349826fbb --- /dev/null +++ b/test/plugins/strangemood.lua @@ -0,0 +1,180 @@ +config.mode = 'fortress' +config.target = 'strangemood' + +-- note: the --force/--type/--skill happy path is deliberately not tested +-- here: inducing a real strange mood mutates the fort (dwarf claims a +-- workshop and can go berserk if unmet), which is too disruptive mid-suite. + +function test.help_is_wrong_usage() + local _, status = dfhack.run_command_silent('strangemood', 'help') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.unknown_option_is_wrong_usage() + local output, status = dfhack.run_command_silent('strangemood', '--bogus') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('Unrecognized parameter', output) +end + +function test.id_missing_value() + local output, status = dfhack.run_command_silent('strangemood', '--id') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('No unit id specified', output) +end + +function test.id_not_a_number() + -- regression: the raw std::stoi call threw an uncaught exception on + -- non-numeric input, crashing the game + local output, status = dfhack.run_command_silent('strangemood', '--id', 'abc') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('Invalid unit id', output) +end + +function test.id_nonexistent_unit() + local _, status = dfhack.run_command_silent('strangemood', '--id', '99999999') + expect.eq(CR_FAILURE, status) +end + +function test.type_missing_value() + local _, status = dfhack.run_command_silent('strangemood', '--type') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.type_bad_value() + local output, status = dfhack.run_command_silent('strangemood', '--type', 'bogus') + expect.eq(CR_WRONG_USAGE, status) + expect.str_find('not recognized', output) +end + +-- the following tests trigger real strange moods and validate the generated +-- job_items against the vanilla request tables. they restore all unit and +-- plotinfo state they touch. the mooded dwarf cannot act on the job because +-- the whole test runs atomically on the main thread. + +-- skill name -> expected item_type of the mood's base item +local BASE_ITEMS = { + miner='BOULDER', engraver='BOULDER', mason='BOULDER', + stonecutter='BOULDER', stonecarver='BOULDER', mechanic='BOULDER', + carpenter='WOOD', woodcrafter='WOOD', bowyer='WOOD', + tanner='SKIN_TANNED', leatherworker='SKIN_TANNED', + weaver='CLOTH', clothier='CLOTH', + weaponsmith='BAR', armorsmith='BAR', metalsmith='BAR', + metalcrafter='BAR', + gemcutter='ROUGH', gemsetter='ROUGH', glassmaker='ROUGH', + bonecarver='NONE', +} + +-- every item_type a fey/secretive/possessed mood may ever request +-- (bars/wafers, cloth, and thread are the only dimensional demands) +local ALLOWED_ITEM_TYPES = { + BOULDER=true, WOOD=true, SKIN_TANNED=true, CLOTH=true, BAR=true, + ROUGH=true, SMALLGEM=true, BLOCKS=true, NONE=true, THREAD=true, +} + +-- mirrors isUnitMoodable in strangemood.cpp, plus "not already mid-job" +local function pick_moodable_unit() + for _, u in ipairs(dfhack.units.getCitizens()) do + if not u.flags1.had_mood and not u.flags1.has_mood and + u.mood == df.mood_type.None and not u.job.current_job and + u.status2.limbs_grasp_count > 0 and + df.profession.attrs[u.profession].moodable and + dfhack.units.casteFlagSet(u.race, u.caste, + df.caste_raw_flags.STRANGE_MOODS) then + return u + end + end +end + +local function clear_mood(unit) + local job = unit.job.current_job + unit.job.current_job = nil + if job then + dfhack.job.removeJob(job) + end + unit.mood = df.mood_type.None + unit.mood_copy = df.mood_type.None + unit.flags1.has_mood = false + unit.flags1.had_mood = false + unit.moodstage = df.mood_stage_type.INITIAL + unit.status.artifact_name.type = -1 +end + +local function trigger_mood(unit, ...) + local _, status = dfhack.run_command_silent('strangemood', + '--force', '--id', tostring(unit.id), ...) + return status == CR_OK and unit.job.current_job or nil +end + +local function check_dimension(item, expected_type) + -- strangemood pre-multiplies quantity by the item's dimension and sets + -- min_dimension, working around a vanilla bug + if item.item_type == df.item_type.BAR then + expect.eq(150, item.min_dimension) + expect.eq(0, item.quantity % 150) + elseif item.item_type == df.item_type.CLOTH then + expect.eq(10000, item.min_dimension) + expect.eq(0, item.quantity % 10000) + elseif item.item_type == df.item_type.THREAD then + expect.eq(15000, item.min_dimension) + expect.eq(0, item.quantity % 15000) + else + expect.eq(-1, item.min_dimension) + end +end + +function test.base_items_match_skill() + local unit = pick_moodable_unit() + expect.true_(unit, 'need a moodable citizen without a job') + local saved_cooldown = df.global.plotinfo.mood_cooldown + return dfhack.with_finalize(function() + clear_mood(unit) + df.global.plotinfo.mood_cooldown = saved_cooldown + end, function() + for skill, expected in pairs(BASE_ITEMS) do + local job = trigger_mood(unit, '--skill', skill) + expect.true_(job, 'mood job for ' .. skill) + local base = job.job_items.elements[0] + expect.eq(expected, df.item_type[base.item_type], + 'base item for ' .. skill) + check_dimension(base) + for _, item in ipairs(job.job_items.elements) do + expect.true_(ALLOWED_ITEM_TYPES[df.item_type[item.item_type]], + ('unexpected %s item for %s'):format( + df.item_type[item.item_type], skill)) + if item.item_type == df.item_type.NONE then + expect.true_(item.flags2.body_part) + end + check_dimension(item) + end + clear_mood(unit) + end + end) +end + +function test.fell_and_macabre_base_items() + local unit = pick_moodable_unit() + expect.true_(unit, 'need a moodable citizen without a job') + local saved_cooldown = df.global.plotinfo.mood_cooldown + return dfhack.with_finalize(function() + clear_mood(unit) + df.global.plotinfo.mood_cooldown = saved_cooldown + end, function() + local job = trigger_mood(unit, '--type', 'fell') + expect.true_(job, 'fell mood job') + expect.eq(df.job_type.StrangeMoodFell, job.job_type) + expect.eq(df.item_type.CORPSE, job.job_items.elements[0].item_type) + expect.true_(job.job_items.elements[0].flags1.murdered) + clear_mood(unit) + + job = trigger_mood(unit, '--type', 'macabre') + expect.true_(job, 'macabre mood job') + expect.eq(df.job_type.StrangeMoodBrooding, job.job_type) + local base = job.job_items.elements[0] + if base.item_type == df.item_type.NONE then + expect.true_(base.flags2.body_part) + expect.true_(base.flags2.bone or base.flags2.totemable) + else + expect.eq(df.item_type.REMAINS, base.item_type) + end + end) +end diff --git a/test/plugins/suspendmanager.lua b/test/plugins/suspendmanager.lua new file mode 100644 index 0000000000..911dae4e73 --- /dev/null +++ b/test/plugins/suspendmanager.lua @@ -0,0 +1,419 @@ +config.mode = 'fortress' +config.target = 'suspendmanager' + +local function is_enabled() + local output = dfhack.run_command_silent('suspendmanager') + return output:find('is enabled') ~= nil +end + +local function prevents_blocking() + local output = dfhack.run_command_silent('suspendmanager') + return output:find('but not suspending') == nil +end + +function test.status_reflects_enable_state() + local was_enabled = is_enabled() + + return dfhack.with_finalize(function() + dfhack.run_command_silent('suspendmanager', + was_enabled and 'enable' or 'disable') + end, function() + local _, status = dfhack.run_command_silent('suspendmanager', 'enable') + expect.eq(CR_OK, status) + expect.true_(is_enabled()) + + local _, status2 = dfhack.run_command_silent('suspendmanager', 'disable') + expect.eq(CR_OK, status2) + expect.false_(is_enabled()) + end) +end + +function test.now_runs_cycle() + local _, status = dfhack.run_command_silent('suspendmanager', 'now') + expect.eq(CR_OK, status) +end + +function test.set_preventblocking() + local was_preventing = prevents_blocking() + + return dfhack.with_finalize(function() + dfhack.run_command_silent('suspendmanager', 'set', + 'preventblocking', was_preventing and 'true' or 'false') + end, function() + local _, status = dfhack.run_command_silent('suspendmanager', 'set', + 'preventblocking', 'false') + expect.eq(CR_OK, status) + local _, status2 = dfhack.run_command_silent('suspendmanager', 'set', + 'preventblocking', 'true') + expect.eq(CR_OK, status2) + end) +end + +function test.set_missing_args_is_wrong_usage() + local _, status = dfhack.run_command_silent('suspendmanager', 'set') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.bad_option_is_wrong_usage() + local _, status = dfhack.run_command_silent('suspendmanager', 'bogus') + expect.eq(CR_WRONG_USAGE, status) +end + +function test.unsuspend_runs() + local _, status = dfhack.run_command_silent('unsuspend') + expect.eq(CR_OK, status) +end + +-- machine collapse protection (#5777) + +local function tile_shape(x, y, z) + local block = dfhack.maps.getTileBlock(x, y, z) + if not block then return nil end + local tt = block.tiletype[x % 16][y % 16] + return df.tiletype.attrs[tt].shape +end + +local function free_tile(x, y, z) + local block = dfhack.maps.getTileBlock(x, y, z) + if not block then return false end + local lx, ly = x % 16, y % 16 + local des = block.designation[lx][ly] + if des.dig ~= df.tile_dig_designation.No or des.flow_size ~= 0 then + return false + end + if block.occupancy[lx][ly].building ~= df.tile_building_occ.None then + return false + end + return not dfhack.buildings.findAtTile(xyz2pos(x, y, z)) +end + +-- find a floor tile with two open tiles to its east; if none exists, +-- channel open tiles out of a wall face so the site is floor + two open tiles +local function make_pit_site() + local function clear_run(x, y, z, shape) + for i = 1, 2 do + if tile_shape(x + i, y, z) ~= shape or not free_tile(x + i, y, z) then + return false + end + end + return true + end + -- stay clear of the unbuildable map border + local margin = 20 + local map = df.global.world.map + local wall_site = nil + for _, block in ipairs(df.global.world.map.map_blocks) do + local bx, by, bz = block.map_pos.x, block.map_pos.y, block.map_pos.z + if bx < margin or by < margin or bx > map.x_count - margin - 16 + or by > map.y_count - margin - 16 then + goto next_block + end + for x = 0, 13 do for y = 0, 15 do + local wx, wy = bx + x, by + y + if tile_shape(wx, wy, bz) == df.tiletype_shape.FLOOR + and free_tile(wx, wy, bz) then + if clear_run(wx, wy, bz, df.tiletype_shape.EMPTY) then + return wx, wy, bz + end + if not wall_site + and clear_run(wx, wy, bz, df.tiletype_shape.WALL) then + wall_site = {wx, wy, bz} + end + end + end end + ::next_block:: + end + if not wall_site then return nil end + local x, y, z = wall_site[1], wall_site[2], wall_site[3] + for i = 1, 2 do + local db = dfhack.maps.getTileBlock(x + i, y, z) + db.designation[(x + i) % 16][y % 16].dig = df.tile_dig_designation.Channel + end + dfhack.run_command_silent('dig-now') + if not clear_run(x, y, z, df.tiletype_shape.EMPTY) then return nil end + return x, y, z +end + +-- complete a machine building without dwarf labor: remove its construction +-- job, mark it built, and join it to the machine graph +local function build_machine(btype, x, y, z, width) + local bld = dfhack.buildings.constructBuilding{ + type=btype, pos=xyz2pos(x, y, z), width=width, height=1, + } + if not bld then return nil end + for i = #bld.jobs - 1, 0, -1 do + dfhack.job.removeJob(bld.jobs[i]) + end + bld.flags.exists = true + bld.construction_stage = 1 + df.global.world.machines:add_to_machine(bld) + return bld +end + +local function destroy_job_of(bld) + bld:queueDestroy() + for _, job in ipairs(bld.jobs) do + if job.job_type == df.job_type.DestroyBuilding then + return job + end + end +end + +-- fully remove a completed building: deconstruct() only queues a destroy job +-- for those, so reset the build stage to reach its immediate-deletion path +local function delete_building(bld) + if not bld then return end + local id = bld.id + for i = #bld.jobs - 1, 0, -1 do + dfhack.job.removeJob(bld.jobs[i]) + end + bld.construction_stage = 0 + dfhack.buildings.deconstruct(bld) + -- the object is deleted but its pointer lingers in the global vector + local all = df.global.world.buildings.all + for i = #all - 1, 0, -1 do + if all[i].id == id then + all:erase(i) + end + end +end + +-- drop machine graph nodes left pointing at deleted buildings +local function prune_dead_machines() + local machines = df.global.world.machines.all + for i = #machines - 1, 0, -1 do + local machine = machines[i] + for j = #machine.components - 1, 0, -1 do + if not df.building.find(machine.components[j].building_id) then + machine.components:erase(j) + end + end + if #machine.components == 0 then + machines:erase(i) + end + end +end + +function test.destroying_machine_anchor_suspends_job() + local x, y, z = make_pit_site() + expect.ne(nil, x, 'no diggable site for the machine rig') + if not x then return end + + local was_enabled = is_enabled() + if not was_enabled then + dfhack.run_command_silent('suspendmanager', 'enable') + end + + local anchor = build_machine(df.building_type.GearAssembly, x, y, z, 1) + local axle = build_machine(df.building_type.AxleHorizontal, x + 1, y, z, 1) + local hanging = build_machine(df.building_type.GearAssembly, x + 2, y, z, 1) + + return dfhack.with_finalize(function() + if not was_enabled then + dfhack.run_command_silent('suspendmanager', 'disable') + end + for _, bld in pairs{anchor, axle, hanging} do + delete_building(bld) + end + prune_dead_machines() + end, function() + expect.ne(nil, anchor, 'anchor gear not built') + expect.ne(nil, axle, 'axle not built') + expect.ne(nil, hanging, 'hanging gear not built') + if not anchor or not axle or not hanging then return end + + -- all three components must be in the same machine + local machine_id = anchor.machine.machine_id + expect.ne(-1, machine_id) + expect.eq(machine_id, axle.machine.machine_id) + expect.eq(machine_id, hanging.machine.machine_id) + + -- removing the endpoint leaves an anchored component: safe + local end_job = destroy_job_of(hanging) + expect.ne(nil, end_job, 'no destroy job on endpoint') + -- removing the anchor orphans the axle and the endpoint: unsafe + local anchor_job = destroy_job_of(anchor) + expect.ne(nil, anchor_job, 'no destroy job on anchor') + if not end_job or not anchor_job then return end + + dfhack.run_command_silent('suspendmanager', 'now') + expect.true_(anchor_job.flags.suspend, + 'anchor destroy job was not suspended') + expect.false_(end_job.flags.suspend, + 'endpoint destroy job was suspended') + end) +end + + +local suspendmanager = require('plugins.suspendmanager') + +local function save_suspend_flags() + local flags = {} + suspendmanager.foreach_construction_job(function(job) + flags[job.id] = job.flags.suspend + end) + return flags +end + +local created_buildings = {} + +config.wrapper = function(test_fn) + local flags = save_suspend_flags() + local saved_onstatechange = dfhack.onStateChange.unsuspend + return dfhack.with_finalize(function() + dfhack.onStateChange.unsuspend = saved_onstatechange + for _,bld in ipairs(created_buildings) do + dfhack.buildings.deconstruct(bld) + end + created_buildings = {} + suspendmanager.foreach_construction_job(function(job) + if flags[job.id] ~= nil then + job.flags.suspend = flags[job.id] + end + end) + end, test_fn) +end + +-- scans the map for a tile where a 1x1 building can be placed +local function construct_test_building() + for _,block in ipairs(df.global.world.map.map_blocks) do + for x = 0, 15 do + for y = 0, 15 do + local attrs = df.tiletype.attrs[block.tiletype[x][y]] + if attrs.shape == df.tiletype_shape.FLOOR and + block.occupancy[x][y].building == 0 then + local bld = dfhack.buildings.constructBuilding{ + type=df.building_type.Chair, + pos={x=block.map_pos.x+x, y=block.map_pos.y+y, + z=block.map_pos.z}} + if bld then + table.insert(created_buildings, bld) + return bld + end + end + end + end + end +end + +local function find_job_for(bld) + local found = nil + suspendmanager.foreach_construction_job(function(job) + if dfhack.job.getHolder(job) == bld then found = job end + end) + return found +end + +function test.foreach_construction_job() + local seen = {} + suspendmanager.foreach_construction_job(function(job) + expect.eq(df.job_type.ConstructBuilding, job.job_type) + seen[job.id] = true + end) + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + expect.ne(nil, job) + if not job then return end + expect.nil_(seen[job.id]) + expect.eq(df.job_type.ConstructBuilding, job.job_type) +end + +function test.run_once_unsuspends_everything() + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + expect.ne(nil, job) + if not job then return end + job.flags.suspend = true + suspendmanager.runOnce(false, true, true) + expect.false_(job.flags.suspend) +end + +function test.unsuspend_command_args() + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + expect.ne(nil, job) + if not job then return end + job.flags.suspend = true + -- -q is quiet, -f forces unsuspension of everything + suspendmanager.unsuspend_command('-q', '-f') + expect.false_(job.flags.suspend) +end + +function test.is_buildingplan_job() + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + if not job then return end + -- buildings created without a material selection have mat_type == -1, + -- which is how suspendmanager identifies buildingplan-placed jobs + expect.true_(suspendmanager.isBuildingPlanJob(job)) +end + +function test.is_kept_suspended() + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + if not job then return end + -- clearing all suspension reasons must synchronously unmark the job; + -- this is only deterministic within a single frame since an enabled + -- suspendmanager instance re-populates reasons on its own cycle + suspendmanager.runOnce(false, true, true) + expect.false_(suspendmanager.isKeptSuspended(job)) +end + +function test.overlay_update_building() + local overlay = suspendmanager.SuspendOverlay{} + local job = {flags={suspend=true}} + overlay:update_building(42, job) + local data = overlay.in_progress_buildings[42] + expect.true_(data.suspended) + expect.eq(1, data.suspend_count) + -- re-suspending while already suspended doesn't bump the count + overlay:update_building(42, job) + expect.eq(1, overlay.in_progress_buildings[42].suspend_count) + -- unsuspend then suspend again -> repeat offender + job.flags.suspend = false + overlay:update_building(42, job) + job.flags.suspend = true + overlay:update_building(42, job) + expect.eq(2, overlay.in_progress_buildings[42].suspend_count) +end + +function test.overlay_update_viewport() + local overlay = suspendmanager.SuspendOverlay{} + local viewport = {z=1, x1=0, y1=0, x2=10, y2=10} + expect.true_(overlay:update_viewport(viewport)) + expect.false_(overlay:update_viewport(viewport)) + expect.true_(overlay:update_viewport( + {z=2, x1=0, y1=0, x2=10, y2=10})) +end + +function test.overlay_onupdate() + local bld = construct_test_building() + if not bld then return end + local job = find_job_for(bld) + if not job then return end + job.flags.suspend = true + local overlay = suspendmanager.SuspendOverlay{} + overlay:overlay_onupdate() + local data = overlay.in_progress_buildings[bld.id] + expect.ne(nil, data) + expect.true_(data.suspended) + -- a second update clears entries whose version wasn't refreshed + overlay.in_progress_buildings[999999] = {version=0} + overlay:overlay_onupdate() + expect.nil_(overlay.in_progress_buildings[999999]) +end + +function test.status_overlay_no_selection() + local overlay = suspendmanager.StatusOverlay{} + expect.eq('Not suspended.', overlay:get_status_string()) +end + +function test.toggle_overlay_no_selection() + local overlay = suspendmanager.ToggleOverlay{} + expect.false_(overlay:shouldRender()) +end diff --git a/test/plugins/tiletypes.lua b/test/plugins/tiletypes.lua new file mode 100644 index 0000000000..88c8aa6494 --- /dev/null +++ b/test/plugins/tiletypes.lua @@ -0,0 +1,165 @@ +config.mode = 'fortress' +config.target = 'tiletypes' + +local tiletypes = require('plugins.tiletypes') + +local function set_cursor(pos) + df.global.cursor.x = pos.x + df.global.cursor.y = pos.y + df.global.cursor.z = pos.z +end + +local function reset_cursor() + df.global.cursor.x = -30000 + df.global.cursor.y = -30000 + df.global.cursor.z = -30000 +end + +-- options is a list of "