diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3bc931305a7..c2d946e300c 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 9257a210816..1dec2111175 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,49 +116,82 @@ 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 + 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: | + Get-ChildItem -Recurse -File -Path "build" -Filter *.pdb | + Copy-Item -Destination "pdb" - name: Prep artifact - if: inputs.artifact-name run: | - cd build/win64-cross/output - tar cjf ../../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + cd output + 7z a -ttar -so -an . | + 7z a -si -tbzip2 ../${{ steps.artifactname.outputs.name }}.tar.bz2 - name: Upload artifact if: inputs.artifact-name uses: actions/upload-artifact@v4 @@ -162,4 +203,4 @@ jobs: 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 85a0067c854..f78da19e748 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73bb3f82d5..175a1adf48c 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 d86f9934304..50950140d1f 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 60a0e1b6b2b..8ee4eb5ff19 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01094ed98db..91a34bdee25 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.37.2 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 f102c355549..dff1b7fac32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0074 NEW) # set up versioning. -set(DF_VERSION "51.06") -set(DFHACK_RELEASE "r1") +set(DF_VERSION "53.14") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") @@ -46,8 +46,8 @@ 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() endmacro() @@ -66,7 +66,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 +122,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 +226,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 +264,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 +344,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 790b9389ae99c4ddebdd2736a8602eca1fec684e # 12.1.0 + bugfix for MSVC warning + build time improvements +) +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 +436,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 ${FMTLIB} gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() @@ -523,7 +534,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 f25d10ca7b9..11f87d8c3eb 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -7,3 +7,4 @@ _CPack_Packages *.tar.* .cmake win64-cross +dest diff --git a/ci/test.lua b/ci/test.lua index e57facf14bb..a4846520ec7 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 diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 355182bfb76..75146071b88 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 787b9962a1b..38fed7e0885 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 00000000000..935a332f00b 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 133dc5aaf8c..c34407b372c 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 00000000000..d1b09ac93bb 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 fe82cc1aed2..a29fa9b9004 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 b6675cee287..dc79e523d44 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 72013fd487d..c1266edb6e4 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 f4068a5cd10..018c66ccde9 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/orders/basic.json b/data/orders/basic.json index 688bdaa7ab0..e1cd5ac8fff 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 00000000000..599bbf47fbc --- /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 5b70cd886f6..8a578206fb9 160000 --- a/depends/dfhooks +++ b/depends/dfhooks @@ -1 +1 @@ -Subproject commit 5b70cd886f619b503aa770008fb79aa39a3be6fa +Subproject commit 8a578206fb9b1dd32b04c8c7c35217e2b83e369e diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index 2908cae9d67..a154d314b63 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -43,6 +43,7 @@ Cameron Ewell Ozzatron Carter Bray Qartar Chris Dombroski cdombroski Chris Parsons chrismdp +Christian Doczkal chdoc cjhammel cjhammel Clayton Hughes Clément Vuchener cvuchener @@ -63,6 +64,7 @@ DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash doomchild doomchild Droseran Droseran +dvantwisk dvantwisk DwarvenM DwarvenM Eamon Bode eamondo2 Baron Von Munchhausen EarthPulseAcademy EarthPulseAcademy @@ -158,8 +160,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 +246,7 @@ thurin thurin Tim Siegel softmoth Tim Walberg twalberg Timothy Collett danaris +Timothy Torres timothymtorres Timur Kelman TymurGubayev Tom Jobbins TheBloke Tom Prince diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 5f73b86be38..96eaa3c1423 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -258,6 +258,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 +285,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 bfb6780b23c..bf0dd9e4883 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 3e6970b6fc2..e8f206848d3 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 774b93e67d0..70e55aff58b 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 ]]] ================================================================================ @@ -68,6 +70,784 @@ Template for new versions: ## 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 + +## API + +## Lua + +## 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 a5665761689..d59b1f84820 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/Documentation.rst b/docs/dev/Documentation.rst index 13eaf42da04..0ba61c404d8 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 ----- @@ -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 000840c76b3..0fbcb419714 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: @@ -1073,7 +1080,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 +1320,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 +1349,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 +1413,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 +1430,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 ------------ @@ -1627,7 +1696,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. @@ -1772,17 +1841,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 +1962,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. @@ -1986,6 +2073,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 +2212,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])`` @@ -2377,9 +2490,30 @@ 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. + 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 +2572,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 +2606,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)`` @@ -3158,12 +3297,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 +3312,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 +3455,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 +3563,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)`` @@ -3752,6 +3902,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 ===== @@ -5936,6 +6109,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 +6118,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} @@ -6444,10 +6621,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 +7540,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/compile/Compile.rst b/docs/dev/compile/Compile.rst index 3eea95d880f..2ad2c52efb4 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 b314e9db279..462fd1f1ca0 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 855a6db26ce..ae7076941d1 100644 --- a/docs/dev/data-identity.rst +++ b/docs/dev/data-identity.rst @@ -123,18 +123,12 @@ 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/github-workflows.rst b/docs/dev/github-workflows.rst new file mode 100644 index 00000000000..ea71e1a2b6e --- /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 4992015d863..931b9bb006e 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -18,6 +18,8 @@ These are pages relevant to people developing for DFHack. /docs/dev/overlay-dev-guide /docs/dev/Structures-intro /docs/dev/data-identity + /docs/dev/github-workflows + /docs/dev/release-process /docs/dev/Memory-research /docs/dev/Binpatches /docs/dev/Remote diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 7b8198c78aa..d87178bfff9 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/release-process.rst b/docs/dev/release-process.rst new file mode 100644 index 00000000000..bef09be412e --- /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 6660dfae51d..f47f9565ce6 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 03b26d56a54..dae03c3aba5 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 00000000000..c9e966dba3b --- /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 00000000000..dc749a21965 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 00000000000..35dcbefd551 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 00000000000..401a8549bf7 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 00000000000..83ae6ce5077 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 b429f575425..f7bea4d6d9f 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 1ea1a9094f1..02d13c83a73 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/autofarm.rst b/docs/plugins/autofarm.rst index 8896c3117c9..0fe98f2b8b8 100644 --- a/docs/plugins/autofarm.rst +++ b/docs/plugins/autofarm.rst @@ -24,9 +24,7 @@ Usage Sets thresholds of individual plant types. You can find the identifiers for the crop types in your world by running the -following command:: - - lua "for _,plant in ipairs(df.global.world.raws.plants.all) do if plant.flags.SEED then print(plant.id) end end" +following command: ``getplants -f`` Examples -------- diff --git a/docs/plugins/blueprint.rst b/docs/plugins/blueprint.rst index 1a5071b6be1..2d84a7ca69c 100644 --- a/docs/plugins/blueprint.rst +++ b/docs/plugins/blueprint.rst @@ -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/changeitem.rst b/docs/plugins/changeitem.rst index f452f6a7425..cf3fb83d941 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 f986a1e3d19..44fa1a3279e 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 0a6007f598e..d8d9c4ff400 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 00000000000..4522ff36bfc --- /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/nestboxes.rst b/docs/plugins/nestboxes.rst index 3c07cc30cab..a8c67dda333 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 9ac3009af81..d89475a3280 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 51eedaf9999..2a2d6327e0f 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/spectate.rst b/docs/plugins/spectate.rst index 1e400a06e42..dd0174f3206 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/stockpiles.rst b/docs/plugins/stockpiles.rst index 739cac5b8d4..506b9d783cd 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -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 5d8dbb69640..7127dc71b4c 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 b7d87196e23..e9f00337831 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/tailor.rst b/docs/plugins/tailor.rst index c1732a741dd..9e59957810f 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 87361ef670e..c3699603443 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/library/CMakeLists.txt b/library/CMakeLists.txt index 37f5b12d5ec..d87e20531e0 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -48,6 +48,7 @@ set(MAIN_HEADERS include/DFHackVersion.h include/BitArray.h include/ColorText.h + include/Commands.h include/Console.h include/Core.h include/CoreDefs.h @@ -58,15 +59,15 @@ set(MAIN_HEADERS include/DebugManager.h include/Error.h include/Export.h + include/Format.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 +89,7 @@ set(MAIN_HEADERS_WINDOWS set(MAIN_SOURCES Core.cpp ColorText.cpp + Commands.cpp CompilerWorkAround.cpp DataDefs.cpp DataIdentity.cpp @@ -100,11 +102,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 +129,6 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} - Hooks.cpp - Process-windows.cpp ) if(WIN32) @@ -136,13 +138,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 +155,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 +186,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 +313,6 @@ endif() # Compilation -add_definitions(-DBUILD_DFHACK_LIB) - if(UNIX) if(CONSOLE_NO_CATCH) add_definitions(-DCONSOLE_NO_CATCH) @@ -321,12 +320,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) @@ -367,6 +366,7 @@ if(EXISTS ${dfhack_SOURCE_DIR}/.git/index AND EXISTS ${dfhack_SOURCE_DIR}/.git/m endif() 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 +375,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 +386,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 ${FMTLIB}) + # effectively disables debug builds... set_target_properties(dfhack PROPERTIES DEBUG_POSTFIX "-debug" ) @@ -421,7 +422,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 +445,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 +461,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 8dc5cc5dcfe..a101d795a98 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 00000000000..b19ed68bb4a --- /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 85934b2f371..058cedebe09 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -36,6 +36,7 @@ POSSIBILITY OF SUCH DAMAGE. */ +#define NOMINMAX #include #include #include @@ -232,17 +233,17 @@ namespace DFHack size_t len = raw_buffer.size(); int cooked_cursor = raw_cursor; - while ((plen + cooked_cursor) >= cols) + if (plen + cooked_cursor > cols) { - buf++; - len--; - cooked_cursor--; - } - while (plen + len > cols) - { - len--; + int adj = std::min(plen + cooked_cursor - cols, 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); @@ -473,6 +474,10 @@ bool Console::init(bool) HMENU hm = GetSystemMenu(d->ConsoleWindow,false); DeleteMenu(hm, SC_CLOSE, MF_BYCOMMAND); + // force console code pages to utf-8 + 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; diff --git a/library/Core.cpp b/library/Core.cpp index 9dc2d2e68a4..00419bd7aef 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -22,38 +22,44 @@ must not be misrepresented as being the original software. distribution. */ +#include "Core.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 +68,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 +122,28 @@ 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::filesystem::path getConfigPath() + { + return Filesystem::getInstallDir() / "dfhack-config"; + }; -static const std::string CONFIG_PATH = "dfhack-config/"; -static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/"; + static const std::filesystem::path getConfigDefaultsPath() + { + return Core::getInstance().getHackPath() / "data" / "dfhack-config-defaults"; + }; class MainThread { public: @@ -187,7 +224,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 +234,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 +265,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 +297,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 +311,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 +380,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 +442,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 +451,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 +470,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 +495,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) { - 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 +553,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()); + 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 +604,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 +614,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 +624,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 +672,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 +724,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 +750,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 +762,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 +794,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 +825,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 +864,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) +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; } - std::ifstream script(fname.c_str()); - if ( !script.good() ) + + 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,11 +935,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, 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, getConfigPath() / "init"); // show the terminal if requested auto L = DFHack::Core::getInstance().getLuaState(); @@ -1395,9 +951,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 +960,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"; + static const std::filesystem::path HISTORY_FILE = getConfigPath() / "dfhack.history"; - IODATA * iod = ((IODATA*) iodata); Core * core = iod->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + 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 +980,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 +991,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 +1009,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 +1033,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 +1066,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 +1084,27 @@ 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) { // 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(); + #ifdef LINUX_BUILD + extern void dfhack_crashlog_init(); + 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. @@ -1572,6 +1124,7 @@ bool Core::InitMainThread() { 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 +1133,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 +1158,7 @@ bool Core::InitMainThread() { { if (!Version::git_xml_match()) { - const char *msg = ( + constexpr auto msg = ( "*******************************************************\n" "* BIG, UGLY ERROR MESSAGE *\n" "*******************************************************\n" @@ -1680,6 +1229,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 +1266,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 +1310,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 +1377,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 +1413,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 +1423,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 +1448,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 +1473,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 +1485,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,73 +1505,6 @@ 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; @@ -2023,7 +1521,7 @@ void Core::doUpdate(color_ostream &out) 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 +1553,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 +1683,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 +1799,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 +1850,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 +1915,11 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { + #ifdef LINUX_BUILD + extern void dfhack_crashlog_shutdown(); + dfhack_crashlog_shutdown(); + #endif + if(errorstate) return true; errorstate = 1; @@ -2433,28 +1932,23 @@ 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; + } + 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)); d.reset(); return -1; } @@ -2507,27 +2001,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 +2038,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 +2070,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 +2119,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 +2144,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 +2178,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.cpp b/library/Crashlog.cpp new file mode 100644 index 00000000000..3860e0106e2 --- /dev/null +++ b/library/Crashlog.cpp @@ -0,0 +1,190 @@ +#include "DFHackVersion.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +const int BT_ENTRY_MAX = 25; +struct CrashInfo { + int backtrace_entries = 0; + void* backtrace[BT_ENTRY_MAX]; + int signal = 0; +}; + +CrashInfo crash_info; + +std::atomic crashed = false; +std::atomic crashlog_ready = false; +std::atomic shutdown = false; + +// 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) { + 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; + 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) { + handle_signal_internal(sig); +} + +void dfhack_crashlog_handle_terminate() { + handle_signal_internal(0); +} + +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 SIGTERM: + return "SIGTERM"; + } + 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; +} + +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 << "\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"; + } + } 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[3] = {SIGSEGV,SIGILL,SIGABRT}; +namespace DFHack { + void dfhack_crashlog_init() { + // Initialize eventfd flag + crashlog_complete = eventfd(0, EFD_CLOEXEC); + + crashlog_thread = std::thread(dfhack_crashlog_thread); + + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); + + // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 + // 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); + } + + 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 5d2386282e5..880a8b93a58 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 @@ -124,10 +141,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 +193,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()) @@ -228,23 +262,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 +305,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 +317,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 +398,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 +419,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 +482,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 e6b4889cf88..0a797c20694 100644 --- a/library/DataIdentity.cpp +++ b/library/DataIdentity.cpp @@ -1,14 +1,21 @@ -#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 @@ -19,7 +26,7 @@ namespace df { #define INTEGER_IDENTITY_TRAITS(type, name) NUMBER_IDENTITY_TRAITS(integer, type, name) #define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type, #type) #define OPAQUE_IDENTITY_TRAITS_NAME(name, ...) \ - const opaque_identity identity_traits<__VA_ARGS__ >::identity(sizeof(__VA_ARGS__), allocator_noassign_fn<__VA_ARGS__ >, name) + const opaque_identity 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"); @@ -33,11 +40,13 @@ namespace df { INTEGER_IDENTITY_TRAITS(unsigned long, "unsigned long"); INTEGER_IDENTITY_TRAITS(long long, "int64_t"); INTEGER_IDENTITY_TRAITS(unsigned long long, "uint64_t"); + INTEGER_IDENTITY_TRAITS(wchar_t, "wchar_t"); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); const bool_identity identity_traits::identity; const stl_string_identity identity_traits::identity; + const path_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const ptr_string_identity identity_traits::identity; const pointer_identity identity_traits::identity; @@ -56,6 +65,7 @@ namespace df { OPAQUE_IDENTITY_TRAITS(std::optional >); OPAQUE_IDENTITY_TRAITS(std::variant >); OPAQUE_IDENTITY_TRAITS(std::weak_ptr); + OPAQUE_IDENTITY_TRAITS(wchar_t*); const buffer_container_identity buffer_container_identity::base_instance; diff --git a/library/Error.cpp b/library/Error.cpp index 3cd0eb31d6c..9abb9ea7c9e 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 951472eba66..42e9f859af4 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -3,10 +3,43 @@ #include "df/gamest.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::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() { @@ -17,7 +50,7 @@ DFhackCExport void dfhooks_init() { } // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread() || !df::global::game) { + if (!DFHack::Core::getInstance().InitMainThread(std::filesystem::canonical(basepath)) || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } @@ -68,6 +101,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged + DFHack::Core::getInstance().DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 4b82dddc17f..d95b6c1a2a7 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,6 +71,7 @@ distribution. #include "df/building_civzonest.h" #include "df/building_stockpilest.h" #include "df/building_tradedepotst.h" +#include "df/building_workshopst.h" #include "df/burrow.h" #include "df/caravan_state.h" #include "df/construction.h" @@ -89,6 +92,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 +103,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" @@ -114,6 +119,8 @@ distribution. #include #include #include +#include +#include namespace DFHack { DBG_DECLARE(core, luaapi, DebugCategory::LINFO); @@ -627,7 +634,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 +1133,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 +1217,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 +1295,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 }); } /************************ @@ -1350,8 +1357,8 @@ 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 bool isWorldLoaded() { return Core::getInstance().isWorldLoaded(); } static bool isMapLoaded() { return Core::getInstance().isMapLoaded(); } @@ -1862,6 +1869,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 +2019,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 } @@ -2126,6 +2253,8 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, setGroupActionTimers), WRAPM(Units, getUnitByNobleRole), WRAPM(Units, unassignTrainer), + WRAPM(Units, hasUnbailableSocialActivity), + WRAPM(Units, isJobAvailable), { NULL, NULL } }; @@ -2289,10 +2418,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 +2448,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 +2481,7 @@ static const luaL_Reg dfhack_units_funcs[] = { { "getReadableName", units_getReadablename }, { "getVisibleName", units_getVisibleName }, { "getProfessionName", units_getProfessionName }, + { "getFocusPenalty", units_getFocusPenalty }, { NULL, NULL } }; @@ -2341,6 +2491,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 +2594,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 +2608,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; } @@ -2603,6 +2758,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,6 +2783,40 @@ 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; +} + static const luaL_Reg dfhack_maps_funcs[] = { { "isValidTilePos", maps_isValidTilePos }, { "isTileVisible", maps_isTileVisible }, @@ -2642,6 +2832,8 @@ static const luaL_Reg dfhack_maps_funcs[] = { { "isTileHeavyAquifer", maps_isTileHeavyAquifer }, { "setTileAquifer", maps_setTileAquifer }, { "removeTileAquifer", maps_removeTileAquifer }, + { "addMaterialSpatter", maps_addMaterialSpatter }, + { "addItemSpatter", maps_addItemSpatter }, { NULL, NULL } }; @@ -2662,20 +2854,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 +2921,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 +3005,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 @@ -3151,9 +3341,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 +3352,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 +3365,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 +3396,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 +3681,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 +3765,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 +3975,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 +4121,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 +4146,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 +4160,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 +4465,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 c0f8bd1d2ec..69242ede5f6 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -244,12 +244,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 +261,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 +282,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 +366,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 +378,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 +426,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 +446,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; } @@ -563,7 +563,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); @@ -842,7 +842,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 +850,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; } @@ -1077,7 +1077,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 +1100,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 +1109,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 +1124,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 +1193,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 +1211,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 +1238,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 +1267,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,21 +1281,13 @@ 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, const char* prompt, const char* 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_pushstring(state, prompt); + lua_pushstring(state, hfile); return true; } @@ -1312,11 +1302,10 @@ bool DFHack::Lua::InterpreterLoop(color_ostream &out, lua_State *state, if (!prompt) prompt = "lua"; - InterpreterArgs args; - args.prompt = prompt; - args.hfile = hfile; + using namespace std::placeholders; + auto init_fn = std::bind(init_interpreter, _1, _2, 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) @@ -2145,7 +2134,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/LuaTypes.cpp b/library/LuaTypes.cpp index 1aa7711e689..0f990a6b844 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 @@ -188,6 +189,24 @@ void df::stl_string_identity::lua_write(lua_State *state, int fname_idx, void *p *(std::string*)ptr = std::string(bytes, size); } +void df::path_identity::lua_read(lua_State* state, int fname_idx, void* ptr) const +{ + auto ppath = (std::filesystem::path*)ptr; + auto str = DFHack::Filesystem::as_string(*ppath); + lua_pushlstring(state, (char*)str.data(), str.size()); +} + +void df::path_identity::lua_write(lua_State* state, int fname_idx, void* ptr, int val_index) const +{ + size_t size; + const char* bytes = lua_tolstring(state, val_index, &size); + if (!bytes) + field_error(state, fname_idx, "path expected", "write"); + + 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) { push_object_internal(state, target, *(void**)ptr); @@ -1234,7 +1253,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"); } diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 608ef956837..9f0ecab7381 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -49,7 +49,7 @@ using namespace DFHack::LuaWrapper; /** * 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 +59,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 +132,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 +168,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 +221,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; } @@ -1005,7 +1009,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,10 +1034,12 @@ 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; } @@ -1641,7 +1648,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"); diff --git a/library/MemoryPatcher.cpp b/library/MemoryPatcher.cpp new file mode 100644 index 00000000000..44aa865c420 --- /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 082657ea34a..d91471803b0 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 059e6aef953..d3b75607739 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 458c0377c24..34095898db9 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; } @@ -489,7 +500,7 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s 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 { @@ -771,6 +782,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 +881,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 +904,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 +918,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 +937,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 +948,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 +962,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 +976,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 +987,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 +1002,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 +1012,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 +1023,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 +1072,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 +1089,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 +1106,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 +1122,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 +1133,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 f8c0833a782..00000000000 --- 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 85e83f54f61..00000000000 --- 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 2450eb87bd2..00000000000 --- 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 00000000000..1ff63622f01 --- /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 20990428dca..c917a3232ae 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 4e26b064973..aa3deb201d7 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 fbace1ae2ec..5dab76e8cb0 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); @@ -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 7636d5bab2c..9f8a72ade60 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 ffed86eb591..21437723f1f 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 797161240e8..2ade207cbaa 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 b1f0472c617..94e2560e37d 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 df10bf1485b..171032a39c7 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/include/BitArray.h b/library/include/BitArray.h index 3679323eb8f..7658e331020 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); diff --git a/library/include/ColorText.h b/library/include/ColorText.h index f484e61d4b6..98bba1447e5 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 00000000000..163d2e164f4 --- /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 a4ca8c5452c..cbf7d585119 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/Core.h b/library/include/Core.h index 556e5e1b03d..8b78e580970 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -29,44 +29,45 @@ distribution. #include "Export.h" #include "Hooks.h" -#include "modules/Graphic.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,6 +151,7 @@ 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. @@ -159,38 +161,24 @@ namespace DFHack /// 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 +189,10 @@ namespace DFHack std::map> ListAliases(); std::string GetAliasCommand(const std::string &name, bool ignore_params = false); - std::string getHackPath(); + 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 +201,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)...); + } - PluginManager *getPluginManager() { return plug_mgr; } + template + static void printerr(fmt::format_string format, Args&& ... args) + { + color_ostream_proxy proxy(getInstance().con); + proxy.printerr(format, std::forward(args)...); + } + + PluginManager* getPluginManager() const { return plug_mgr; } + HotkeyManager* getHotkeyManager() { return hotkey_mgr; } static void cheap_tokenise(std::string const& input, std::vector &output); @@ -228,6 +228,29 @@ namespace DFHack return State; } + 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; + } + private: DFHack::Console con; @@ -237,11 +260,12 @@ namespace DFHack 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 +275,11 @@ 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 + 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 +288,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 +318,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 +338,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 +510,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/DataDefs.h b/library/include/DataDefs.h index c6a7eb8272c..37892391a48 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -24,18 +24,21 @@ distribution. #pragma once +#include +#include #include -#include #include +#include #include -#include #include +#include #include #include "BitArray.h" #include "Export.h" +#include "Format.h" -typedef struct lua_State lua_State; +struct lua_State; /* * Definitions of DFHack namespace structs used by generated headers. @@ -71,7 +74,7 @@ namespace DFHack PTRFLAG_HAS_BAD_POINTERS = 2, }; - typedef void *(*TAllocateFn)(void*,const void*); + using TAllocateFn = void *(*)(void*, const void*); class DFHACK_EXPORT type_identity { const size_t size; @@ -121,10 +124,10 @@ 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 can_allocate() const { return (allocator != nullptr); } + virtual void *do_allocate() const { return allocator(nullptr,nullptr); } 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 do_destroy(void *obj) const { return allocator(nullptr,obj) == obj; } public: virtual bool isPrimitive() const { return false; } virtual bool isConstructed() const { return true; } @@ -134,28 +137,30 @@ namespace DFHack }; 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; + const compound_identity *const scope_parent; + + static void ensure_compound_identity_init(); protected: 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; - 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); }; @@ -201,7 +206,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 { @@ -257,8 +262,8 @@ namespace DFHack }; 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,14 +291,15 @@ 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(); + protected: - virtual void doInit(Core *core); + virtual void doInit(Core *core) const override; public: struct_identity(size_t size, TAllocateFn alloc, @@ -302,9 +308,9 @@ namespace DFHack virtual identity_type type() const { 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; } @@ -326,8 +332,8 @@ namespace DFHack 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; } @@ -351,37 +357,89 @@ namespace DFHack virtual void build_metatable(lua_State *state) const; }; + namespace + { + 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 + >; + } + #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 { 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, @@ -394,16 +452,16 @@ namespace DFHack 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 +469,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 @@ -494,56 +552,78 @@ 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 {}; template - requires requires () { { &T::_identity } -> std::convertible_to; } + requires 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 +648,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 +699,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 +735,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 +846,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; @@ -877,7 +969,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); } @@ -910,7 +1002,7 @@ namespace DFHack { #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) \ @@ -938,3 +1030,25 @@ namespace std { } }; } + +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); + } +}; + +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/DataFuncs.h b/library/include/DataFuncs.h index e8bfc7e2848..ab9bb78a261 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" @@ -54,6 +50,7 @@ namespace df { 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); @@ -69,6 +66,7 @@ namespace df { template T get_from_lua_state(lua_State* L, int idx) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; T val{}; df::identity_traits::get()->lua_write(L, UPVAL_METHOD_NAME, &val, idx); return val; @@ -78,6 +76,7 @@ namespace df { requires std::is_invocable_r_v void call_and_push_impl(lua_State* L, int base, std::index_sequence, FT fun, ET... extra) { + using DFHack::LuaWrapper::UPVAL_METHOD_NAME; if constexpr (std::is_same_v) { std::invoke(fun, extra..., (get_from_lua_state(L, base+I))...); lua_pushnil(L); @@ -119,6 +118,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,11 +128,49 @@ 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; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 02bc8b32018..e58247fdaae 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -26,16 +26,17 @@ distribution. #include #include -#include #include -#include #include +#include #include +#include #include #include -#include +#include #include "DataDefs.h" +#include "LuaWrapper.h" namespace std { class condition_variable; @@ -112,6 +113,7 @@ namespace DFHack }; class DFHACK_EXPORT container_identity : public constructed_identity { + protected: const type_identity *item; const enum_identity *ienum; @@ -298,9 +300,27 @@ namespace df virtual void lua_write(lua_State *state, int fname_idx, void *ptr, int val_index) const; }; + class DFHACK_EXPORT path_identity : public DFHack::constructed_identity { + public: + path_identity() + : constructed_identity(sizeof(std::filesystem::path), &allocator_fn) + { + }; + + const std::string getFullName() const { return "path"; } + + 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; + using container = std::vector; /* * This class assumes that std::vector is equivalent @@ -399,6 +419,25 @@ namespace df ct.insert(ct.begin()+idx, *(typename T::value_type*)item); return true; } + virtual bool lua_insert2(lua_State* state, int fname_idx, void* ptr, int idx, int val_index) const + { + 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 != item || !pitem) + DFHack::LuaWrapper::field_error(state, fname_idx, "incompatible object type", "insert"); + + return insert(ptr, idx, pitem); + } protected: virtual int item_count(void *ptr, CountMode) const { return (int)((T*)ptr)->size(); } @@ -467,7 +506,7 @@ namespace df * in layout and behavior to BitArray for any T. */ - typedef BitArray container; + using container = BitArray; bit_array_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &allocator_fn, ienum) @@ -484,7 +523,7 @@ namespace df protected: virtual int item_count(void *ptr, CountMode cnt) const { - return cnt == COUNT_LEN ? ((container*)ptr)->size * 8 : -1; + return cnt == COUNT_LEN ? ((container*)ptr)->size() * 8 : -1; } virtual bool get_item(void *ptr, int idx) const { return ((container*)ptr)->is_set(idx); @@ -497,7 +536,7 @@ namespace df class DFHACK_EXPORT stl_bit_vector_identity : public bit_container_identity { public: - typedef std::vector container; + using container = std::vector; stl_bit_vector_identity(const enum_identity *ienum = NULL) : bit_container_identity(sizeof(container), &df::allocator_fn, ienum) @@ -528,7 +567,7 @@ namespace df template class enum_list_attr_identity : public container_identity { public: - typedef enum_list_attr container; + using container = enum_list_attr; enum_list_attr_identity(const type_identity *item) : container_identity(sizeof(container), NULL, item, NULL) @@ -578,8 +617,10 @@ namespace df INTEGER_IDENTITY_TRAITS(unsigned long); INTEGER_IDENTITY_TRAITS(long long); INTEGER_IDENTITY_TRAITS(unsigned long long); + INTEGER_IDENTITY_TRAITS(wchar_t); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); + OPAQUE_IDENTITY_TRAITS(wchar_t*); OPAQUE_IDENTITY_TRAITS(std::condition_variable); OPAQUE_IDENTITY_TRAITS(std::fstream); OPAQUE_IDENTITY_TRAITS(std::mutex); @@ -596,9 +637,9 @@ namespace df 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; } }; @@ -616,6 +657,11 @@ namespace df static const stl_string_identity *get() { return &identity; } }; + template<> struct DFHACK_EXPORT identity_traits { + static const bool is_primitive = true; + static const path_identity identity; + static const path_identity* get() { return &identity; } + }; template<> struct DFHACK_EXPORT identity_traits { static const bool is_primitive = true; static const ptr_string_identity identity; @@ -705,6 +751,11 @@ 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 bit_container_identity *get() { return &identity; } @@ -748,7 +799,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef std::vector container; + using container = std::vector; static const stl_container_identity identity("vector", identity_traits::get()); return &identity; } @@ -776,28 +827,36 @@ namespace df #ifdef BUILD_DFHACK_LIB template inline const container_identity *identity_traits >::get() { - typedef std::deque container; + using container = std::deque; static const stl_container_identity identity("deque", identity_traits::get()); return &identity; } template inline const container_identity *identity_traits >::get() { - typedef std::set container; + using container = std::set; static const ro_stl_container_identity identity("set", identity_traits::get()); return &identity; } + template + inline const container_identity* identity_traits >::get() + { + using container = std::unordered_set; + static const ro_stl_container_identity identity("unordered_set", identity_traits::get()); + return &identity; + } + template inline const container_identity *identity_traits>::get() { - typedef std::map container; + using container = std::map; static const ro_stl_assoc_container_identity identity("map", identity_traits::get(), identity_traits::get()); return &identity; } template inline const container_identity *identity_traits>::get() { - typedef std::unordered_map container; + using container = std::unordered_map; static const ro_stl_assoc_container_identity identity("unordered_map", identity_traits::get(), identity_traits::get()); return &identity; } @@ -810,7 +869,7 @@ namespace df template inline const container_identity *identity_traits >::get() { - typedef DfArray container; + using container = DfArray; static const stl_container_identity identity("DfArray", identity_traits::get()); return &identity; } diff --git a/library/include/Error.h b/library/include/Error.h index a4624d5f727..550e1b01983 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 00000000000..52d4e0e3c6a --- /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/Hooks.h b/library/include/Hooks.h index 6945de2ea68..5f49d8dabaa 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 53c17e1b21a..93853468e4e 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -26,28 +26,23 @@ distribution. #include #include -#include #include +#include #include #include #include #include +#include #include "Core.h" #include "ColorText.h" #include "DataDefs.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 +55,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 +139,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 +165,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 +173,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 +181,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 +192,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); } @@ -280,9 +294,9 @@ namespace DFHack {namespace Lua { * 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 +312,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 +477,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 +514,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 +560,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 +573,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 347945e8966..70f36d3f957 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,7 +139,7 @@ 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]] 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. @@ -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,7 +228,8 @@ 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); diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h index 083d94434ca..5115348ba27 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 00000000000..8577a762eff --- /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 8568a55c0a2..e8d904e81bd 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; } @@ -456,6 +474,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 7770e13970b..00000000000 --- 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 c99e7b32898..00000000000 --- 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/PluginManager.h b/library/include/PluginManager.h index 3749ff95489..1eccc7a79a8 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 8a5bee86f74..bade25b5219 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 38c234c5fbc..8731ad3474b 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 53b6aee18ae..f367946e561 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -26,14 +26,14 @@ distribution. #pragma once #include +#include +#include #include "Export.h" - #include "DataDefs.h" #include "df/general_ref_type.h" #include "df/specific_ref_type.h" -#include "df/language_name_type.h" namespace df { struct building; @@ -118,7 +118,7 @@ namespace DFHack return rect.second - rect.first + df::coord2d(1,1); } - DFHACK_EXPORT int getdir(std::string dir, std::vector &files); + 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); diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h index 7575ee18c28..9dff74d7e59 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 060d622ecd9..92a5f94e103 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 00000000000..96e79c167db --- /dev/null +++ b/library/include/df/custom/.gitignore @@ -0,0 +1 @@ +!*.h diff --git a/library/include/df/custom/creature_handler.methods.inc b/library/include/df/custom/creature_handler.methods.inc index 811128d058d..14639bd8891 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 00000000000..5f2d0d2b339 --- /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 00000000000..a1e4606e33d --- /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 00000000000..a1de2fec13d --- /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/modules/Buildings.h b/library/include/modules/Buildings.h index ee7fcdf6982..1a37cb8304a 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 b2e3e56b4ee..dbd2e51dd6b 100644 --- a/library/include/modules/Burrows.h +++ b/library/include/modules/Burrows.h @@ -45,6 +45,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 0fcd17b3226..7d53242ad03 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -1,26 +1,20 @@ #pragma once -#include "Error.h" -#include "Export.h" #include "ColorText.h" +#include "Export.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 +22,61 @@ 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(); + 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/EventManager.h b/library/include/modules/EventManager.h index e0a74bed929..a2c546a7555 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 8e7bab9b200..68da9ec7f61 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 9fa498a7cf8..00000000000 --- 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 222779a8eee..761e948bb0b 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 00000000000..25129b2ff5a --- /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 79aed92c36e..52af19e680c 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 f0d3e89b970..acb37169c3a 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 04fddd38bd7..0d636273894 100644 --- a/library/include/modules/MapCache.h +++ b/library/include/modules/MapCache.h @@ -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; diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c6..1e7eac89e16 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -31,8 +31,6 @@ distribution. #define CL_MOD_MAPS #include "Export.h" -#include "Module.h" -#include "BitArray.h" #include "modules/Materials.h" @@ -40,6 +38,7 @@ distribution. #include "df/block_flags.h" #include "df/feature_type.h" #include "df/flow_type.h" +#include "df/matter_state.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" @@ -229,9 +228,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 +251,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); } /** @@ -371,6 +373,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 diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 90907b8a414..1f87f548ffc 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -30,14 +30,12 @@ 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" -namespace df { +namespace df +{ struct item; struct plant_raw; struct creature_raw; @@ -56,28 +54,32 @@ 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 { + 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; + static const int PLANT_BASE = NUM_BUILTIN + GROUP_SIZE * 2; + static const int END_BASE = NUM_BUILTIN + GROUP_SIZE * 3; int16_t type; int32_t index; - df::material *material; + df::material* material; - enum Mode { + enum Mode + { None, Builtin, Inorganic, @@ -87,16 +89,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 +112,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 +141,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 +354,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 013f8044151..3b51a921e2a 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 38fbdd0f894..b8e5de26bff 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 3989460bb6f..1a6ab569ada 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" diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index b820c3332d7..c2038c20b33 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 2709dbf6fae..2860c3db879 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 2e0f188b5cb..b13a75d6a3b 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; @@ -281,9 +283,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 +341,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 +363,7 @@ 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); } } diff --git a/library/include/modules/World.h b/library/include/modules/World.h index b03170b5fa4..a82ceef9a40 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -32,12 +32,10 @@ distribution. */ #include "Export.h" -#include "Module.h" #include "modules/Persistence.h" -#include - #include "DataDefs.h" + namespace df { struct tile_bitmask; diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index d9ee57d139c..e5337afa11d 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 6c19deb72aa..7975ba5e394 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 ef08fb7f4b8..0dadc4fa9c6 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -845,16 +845,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 +1004,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 7fff00dcf2b..ae4d6243606 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 a09849a57df..f43429642f3 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 793acbd5202..a0e50532e62 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,35 @@ end ---@return df.coord|nil function getCursorPos() - if g_cursor.x >= 0 then + if dfhack.world.isAdventureMode() 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 dfhack.world.isAdventureMode() then + a_look.cursor = copyall(cursor) + else + df.global.cursor = copyall(cursor) + end end function clearCursorPos() - df.global.cursor = xyz2pos(nil) + if dfhack.world.isAdventureMode() then + if not a_look.open then + return + end + local u = dfhack.world.getAdventurer() + if u and u.pos:isValid() then + a_look.cursor = copyall(u.pos) + end + else + df.global.cursor = xyz2pos(nil) + end end function getSelection() @@ -249,7 +268,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 d3a9408a511..7429a78237a 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -116,7 +116,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 @@ -378,14 +378,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 44d4f0de30f..b97a10e439a 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 8a5daab7b23..e7870f88e86 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -27,6 +27,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/range_slider.lua b/library/lua/gui/widgets/range_slider.lua index bd1f9cb3de4..9b44e0fd54d 100644 --- a/library/lua/gui/widgets/range_slider.lua +++ b/library/lua/gui/widgets/range_slider.lua @@ -1,17 +1,15 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse +local core = 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 @@ -22,28 +20,19 @@ local to_pen = dfhack.pen.parse ---@field super widgets.Widget ---@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, slide_core) 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,7 +40,7 @@ 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 @@ -71,94 +60,4 @@ function RangeSlider:onInput(keys) 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 00000000000..450954b9c12 --- /dev/null +++ b/library/lua/gui/widgets/slide_core.lua @@ -0,0 +1,163 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- slide_core +-------------------------------- + +---@class widgets.slide_core.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field is_single boolean +---@field w integer + +---@class widgets.slide_core.attrs.partial: widgets.slide_core.attrs + +---@class widgets.slide_core.initTable: widgets.slide_core.attrs +---@field num_stops integer + +---@class widgets.slide_core: widgets.Widget, widgets.slide_core.attrs +---@field super widgets.Widget +---@field ATTRS widgets.slide_core.attrs|fun(attributes: widgets.slide_core.attrs.partial) +---@overload fun(init_table: widgets.slide_core.initTable): self +slide_core = defclass(slide_core, Widget) +slide_core.ATTRS{ + num_stops=DEFAULT_NIL, + is_single=DEFAULT_NIL, + w=DEFAULT_NIL +} + +function slide_core:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function slide_core: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 slide_core:get_min_stops() + return self.is_single and 1 or 2 +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 + 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 slide_core: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 slide_core diff --git a/library/lua/gui/widgets/slider.lua b/library/lua/gui/widgets/slider.lua new file mode 100644 index 00000000000..65ce28a5c71 --- /dev/null +++ b/library/lua/gui/widgets/slider.lua @@ -0,0 +1,60 @@ +local core = 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.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, slide_core) +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.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.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 cfa790a355a..83099fac765 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -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 0bd64d29ccb..5af55f5697c 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/script-manager.lua b/library/lua/script-manager.lua index c2647fbe2d5..80774c53e85 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 diff --git a/library/lua/syndrome-util.lua b/library/lua/syndrome-util.lua index efbaa5deaae..c0af9f8a726 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/modules/Buildings.cpp b/library/modules/Buildings.cpp index a560a6fc994..ddedbbc1b66 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 3c641e79b74..17ab0389d4b 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); diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 832ffeb0be7..f23cde55113 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 @@ -58,6 +62,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 +113,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 +194,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 +304,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 b5892bbb16a..31cacfea632 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 { @@ -100,9 +119,6 @@ void DFSteam::cleanup(color_ostream& out) { #ifdef WIN32 -#include -#include -#include static bool is_running_on_wine() { typedef const char* (CDECL wine_get_version)(void); @@ -157,13 +173,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 +195,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 +220,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 +265,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 5a24f1174a2..3c926645a26 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" @@ -70,7 +71,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 +90,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 +118,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 +129,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; @@ -407,7 +417,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; @@ -598,13 +608,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 +622,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 +640,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, @@ -1228,7 +1238,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 +1361,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 +1376,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 +1403,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 2c275ceba84..6d49c6a3b74 100644 --- a/library/modules/Filesystem.cpp +++ b/library/modules/Filesystem.cpp @@ -47,13 +47,19 @@ SOFTWARE. #include #include +#include +#include +#include +#include "modules/DFSDL.h" #include "modules/Filesystem.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 +70,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 +94,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 b55ee83ed4e..00000000000 --- 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 3e9dd0dda99..e7c8233ba17 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" @@ -105,10 +104,9 @@ 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 +133,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 +150,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 +188,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 +774,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 +823,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 +855,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 +968,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); @@ -1830,7 +1840,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 +1886,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 +1926,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 +1943,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 +2018,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 +2048,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 +2069,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 +2083,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 +2093,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 +2103,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 +2111,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 +2149,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 +2176,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 +2188,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 +2246,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 +2459,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 +2514,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 +2652,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 +2686,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 +2723,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 +2807,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 +2897,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 +2905,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 +2913,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 +2967,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 00000000000..6433879d859 --- /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 47d68b16548..6b5b3ee1d28 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 >= 419 && mat <= 618 && 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; @@ -1754,39 +1827,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 +1845,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 +1927,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 +1965,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 +1980,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 86809ba5ba7..cbcfebcc23f 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" @@ -323,11 +327,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); @@ -515,6 +519,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 +600,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 +610,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 +678,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 d6c2b657f50..c9d4c7d5c67 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] >= 419 && plotinfo->kitchen.mat_types[i] <= 618) ? 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 c01f5bf5769..603d1d0da29 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" diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac48..2376936054b 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -31,7 +31,6 @@ distribution. #include "Error.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "VersionInfo.h" #include "modules/Buildings.h" @@ -42,14 +41,20 @@ distribution. #include "df/block_burrow.h" #include "df/block_burrow_link.h" #include "df/block_square_event_grassst.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.h" #include "df/building_type.h" #include "df/builtin_mats.h" #include "df/burrow.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/map_block_column.h" +#include "df/material.h" #include "df/plant.h" #include "df/plant_root_tile.h" #include "df/plant_tree_info.h" @@ -195,10 +200,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 +233,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 @@ -515,7 +529,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 +654,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; + memset(spatter->amount, 0, sizeof(spatter->amount)); + 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; + memset(spatter->amount, 0, sizeof(spatter->amount)); + memset(spatter->flag, 0, sizeof(spatter->flag)); + 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); @@ -947,13 +1206,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 +1252,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 +1261,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; diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 341ddeabc46..dba8a781324 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,14 +93,15 @@ 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) @@ -123,7 +123,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < FIGURE_BASE) { mode = Creature; - subtype = type-CREATURE_BASE; + subtype = type - CREATURE_BASE; creature = df::creature_raw::find(index); if (!creature || size_t(subtype) >= creature->material.size()) return false; @@ -132,7 +132,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < PLANT_BASE) { mode = Creature; - subtype = type-FIGURE_BASE; + subtype = type - FIGURE_BASE; figure = df::historical_figure::find(index); if (!figure) return false; @@ -144,7 +144,7 @@ bool MaterialInfo::decode(int16_t type, int32_t index) else if (type < END_BASE) { mode = Plant; - subtype = type-PLANT_BASE; + subtype = type - PLANT_BASE; 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,17 +207,18 @@ 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; + auto& raws = world->raws; for (int i = 0; i < NUM_BUILTIN; i++) { auto obj = raws.mat_table.builtin[i]; @@ -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(PLANT_BASE + 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(CREATURE_BASE + 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 0477c366b02..d3fb4368265 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 0a6f1c3772d..b9c3df577b3 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 f0d2054c9e3..d6d682d9216 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/Screen.cpp b/library/modules/Screen.cpp index 1a988377399..b16a3e9b0e3 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" @@ -621,7 +620,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 +779,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 8b485a3a625..4a1e29aeab5 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -54,7 +54,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 +195,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 +308,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 +322,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 +347,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 +372,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 +397,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 +453,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 38ae2c51ba3..097de85cc9f 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 933ea537547..00821c11c0c 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -27,7 +27,6 @@ distribution. #include "Internal.h" #include "MemAccess.h" #include "MiscUtils.h" -#include "ModuleFactory.h" #include "Types.h" #include "VersionInfo.h" @@ -61,16 +60,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,10 +86,12 @@ 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 +#include #include #include #include @@ -102,8 +111,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 = ( @@ -202,7 +211,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 +334,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; @@ -600,7 +609,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 +669,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 +680,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; } @@ -766,6 +777,18 @@ bool Units::teleport(df::unit *unit, df::coord target_pos) else old_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) unit->flags1.bits.on_ground = true; @@ -994,7 +1017,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 +1036,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 +1156,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 +1190,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 +1212,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 +1288,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 +1301,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 +1649,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 +2022,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 +2174,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 +2308,22 @@ 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; +} diff --git a/library/modules/World.cpp b/library/modules/World.cpp index b41c625f174..d19f61fcee3 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -223,16 +223,16 @@ int32_t World::GetCurrentSiteId() { 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 9d207f7b70c..01aae95cacd 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 9d207f7b70c7ecf3835b6ca7506e598622d29d04 +Subproject commit 01aae95cacd98850e4f477c45a4b75f800bacecc diff --git a/package/launchdf.cpp b/package/launchdf.cpp index 5a850c6cf94..3b07abf99a4 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 3ed5455d0f4..07c1ca795d6 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -433,13 +433,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 +471,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 +568,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 +590,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; } @@ -801,7 +801,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 +852,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 +895,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 +936,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 +946,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 +956,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 +1095,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 +1285,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 +1306,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 +1362,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 +1396,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 +1546,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 +1555,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 +1568,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 +1591,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/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5ea..355cc0ac4c0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -32,10 +32,15 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # 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) @@ -70,6 +75,7 @@ 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(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) @@ -111,7 +117,7 @@ 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(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) diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 3726b86c7c4..192662bccc4 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 ${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 0ece73d982d..a2685983fb2 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 124fa29838a..afb8bbe54fb 100644 --- a/plugins/aquifer.cpp +++ b/plugins/aquifer.cpp @@ -35,7 +35,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,7 +142,7 @@ 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; @@ -151,12 +151,12 @@ static void get_z_range(color_ostream &out, int & minz, int & maxz, const df::co 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,14 +173,14 @@ 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 " : ""); } } @@ -189,22 +189,21 @@ static void aquifer_list(color_ostream &out, df::coord pos1, df::coord pos2, int static int aquifer_drain(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - 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={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); 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; } @@ -212,21 +211,20 @@ static int aquifer_drain(color_ostream &out, string aq_type, static int aquifer_convert(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - 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={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); 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; } @@ -234,20 +232,19 @@ static int aquifer_convert(color_ostream &out, string aq_type, static int aquifer_add(color_ostream &out, string aq_type, df::coord pos1, df::coord pos2, int skip_top, int levels, bool leaky) { - 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={}\n", + aq_type, pos1, pos2, skip_top, levels, leaky); 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 00000000000..1924cfc9e96 --- /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 9e93018cc93..8f7bbb16faf 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 7c2cfc9a5ed..811a3d1cb08 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; } @@ -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 fac721a5c5b..076892f9bd6 100644 --- a/plugins/autoclothing.cpp +++ b/plugins/autoclothing.cpp @@ -129,6 +129,19 @@ struct ClothingRequirement { bool SetFromParameters(color_ostream &out, vector ¶meters) { + if (parameters[0] == "clear") { // handle the clear case + if (!set_bitfield_field(&material_category, parameters[1], 1)) + out << "Unrecognized material type: " << parameters[1] << endl; + if (!setItem(parameters[2], this)) { + out << "Unrecognized item name or token: " << parameters[2] << endl; + return false; + } + else if (!validateMaterialCategory(this)) { + out << parameters[1] << " is not a valid material category for " << parameters[2] << endl; + return false; + } + return true; + } if (!set_bitfield_field(&material_category, parameters[0], 1)) out << "Unrecognized material type: " << parameters[0] << endl; if (!setItem(parameters[1], this)) { @@ -224,7 +237,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 +282,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"); } @@ -397,7 +410,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; } @@ -442,14 +455,20 @@ command_result autoclothing(color_ostream &out, vector ¶meters) 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;) { @@ -459,7 +478,10 @@ command_result autoclothing(color_ostream &out, vector ¶meters) if (settingSize) { if (newRequirement.needed_per_citizen == 0) { clothingOrders.erase(clothingOrders.begin() + i); - out << "Unset " << parameters[0] << " " << parameters[1] << endl; + if (parameters[0] == "clear") + out << "Unset " << parameters[1] << " " << parameters[2] << endl; + else + out << "Unset " << parameters[0] << " " << parameters[1] << endl; } else { clothingOrders[i] = newRequirement; @@ -505,7 +527,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 +613,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 +684,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) diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index fbf3ff8a82e..4376a888d0c 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -170,7 +170,7 @@ 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 @@ -184,7 +184,7 @@ static command_result autodump_main(color_ostream &out, vector ¶mete 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; } diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 571cd136085..65a08123d9d 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -22,6 +22,7 @@ #include "df/unit.h" #include "df/world.h" +#include #include using namespace DFHack; @@ -199,9 +200,9 @@ class AutoFarm { 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)); } } @@ -389,11 +390,11 @@ class AutoFarm { if (plant != std::end(allPlants)) { 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()); + WARN(control, out).print("threshold for unknown plant {} in saved configuration ignored\n", id); } } } @@ -453,7 +454,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; } @@ -512,7 +513,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; } diff --git a/plugins/autogems.cpp b/plugins/autogems.cpp index 593e64670a2..249552a0069 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/autolabor.cpp b/plugins/autolabor/autolabor.cpp index d755202de63..d0cec796fba 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -697,8 +697,8 @@ static void assign_labor(unit_labor::unit_labor labor, 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], + TRACE(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)" : ""); @@ -766,7 +766,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { df::building_tradedepotst* depot = (df::building_tradedepotst*) build; trader_requested = trader_requested || depot->trade_flags.bits.trader_requested; - TRACE(cycle,out).print(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" ); @@ -846,8 +846,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) 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()); + DEBUG(cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", + dwarf, dwarfs[dwarf]->name.first_name); break; } } @@ -919,15 +919,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) 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); + WARN(cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", dwarf, dwarfs[dwarf]->name.first_name, job); dwarf_info[dwarf].state = OTHER; } } state_count[dwarf_info[dwarf].state]++; - 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]); + TRACE(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; @@ -1034,8 +1034,8 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) 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()); + TRACE(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++) @@ -1076,7 +1076,7 @@ void print_labor (df::unit_labor labor, 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; } @@ -1095,7 +1095,7 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) command_result autolabor (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; } @@ -1137,7 +1137,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (labor == unit_labor::NONE) { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); + out.printerr("Could not find labor {}.\n", parameters[0]); return CR_WRONG_USAGE; } @@ -1171,7 +1171,7 @@ command_result autolabor (color_ostream &out, std::vector & parame if (maximum < minimum || maximum < 0 || minimum < 0) { - out.printerr("Syntax: autolabor [] [], %d > %d\n", maximum, minimum); + out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); return CR_WRONG_USAGE; } @@ -1235,7 +1235,7 @@ command_result autolabor (color_ostream &out, std::vector & parame { out.print("Automatically assigns labors to dwarves.\n" "Activate with 'enable autolabor', deactivate with 'disable autolabor'.\n" - "Current state: %d.\n", enable_autolabor); + "Current state: {}.\n", enable_autolabor); return CR_OK; } diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index a9472bcc2ea..3757011d639 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -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/autonestbox.cpp b/plugins/autonestbox.cpp index f7f1bdd84b5..cc45507b2b4 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 f8f93ac7476..e8c06d575d9 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 c7ce4114e7b..f21198c9d57 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -5,9 +5,6 @@ * Written by cdombroski. */ -#include -#include - #include "Console.h" #include "DataDefs.h" #include "DataFuncs.h" @@ -19,8 +16,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 +35,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; @@ -106,9 +112,8 @@ 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; }; @@ -132,9 +137,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 +167,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 +185,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 +222,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 +238,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 +342,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 +381,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 +405,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 +437,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 +486,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 +537,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 +613,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 +636,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 +655,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 +664,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 +687,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 +743,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 +1048,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,240 +1073,300 @@ 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); } - 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.take_from_workshop) { + if (target->name.empty()) + continue; + take_from.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_pile) { + if (target->name.empty()) + continue; + give_to.push_back(target->name); + } + 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) { @@ -1302,8 +1376,8 @@ static bool create_output_dir(color_ostream &out, // 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 +1577,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 +1598,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 +1667,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 +1695,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 +1720,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 +1758,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 fe317e4705b..6f5a13a8c7c 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -64,7 +64,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 +97,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 +125,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 +166,12 @@ static void load_organic_material_cache(df::organic_mat_category cat) { } static void load_material_cache() { - df::world_raws &raws = world->raws; + auto &raws = world->raws; for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++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 +201,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 +214,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 +354,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 +428,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 +460,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 +489,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 +520,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 +560,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 +571,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 +583,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 +654,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 +700,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 +713,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 +731,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 +756,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 +816,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 +846,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 +875,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 +906,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 +920,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 +963,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 +1004,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 +1020,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 +1045,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 +1069,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 +1100,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 d55b5f5da46..7e8187dfce4 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 710878d643c..e8136b88960 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 81bb043c556..5b21b6cc2c2 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 eaa11335d09..0e1cab8548a 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 0e4569f156a..09a4a15c8e7 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 8a1f67d3cf9..95a216c302c 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 5888d169e76..e0f5a8984e9 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 d06e95efb64..172a7d37d20 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 33387aa1417..e07db445706 100644 --- a/plugins/changeitem.cpp +++ b/plugins/changeitem.cpp @@ -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 a73d9584e90..0463b794ca8 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 c1a5b5953f1..71cfdddb8d7 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 67f8742b381..6c85fda951b 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 4aa16484535..ac69ffdc8e4 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 362fd927a34..16db1ff7d26 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 b72806cd9b5..9e05ecae465 100644 --- a/plugins/cleanconst.cpp +++ b/plugins/cleanconst.cpp @@ -38,13 +38,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 +62,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 6706bc92d82..74bc40e6ff2 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 9e6ddb98492..c8d67d92ad0 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 1a720c3f4c5..d813d8e44be 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -30,7 +30,6 @@ using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("createitem"); -REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gametype); REQUIRE_GLOBAL(cur_year_tick); @@ -51,7 +50,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 +66,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 +83,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 +105,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 +144,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 +198,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 +226,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 +268,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 +280,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 +295,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 +313,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 +402,13 @@ command_result df_createitem (color_ostream &out, vector ¶meters) { auto unit = Gui::getSelectedUnit(out, true); if (!unit) { + auto pos = Gui::getCursorPos(); if (*gametype == game_type::ADVENTURE_ARENA || World::isAdventureMode()) { // 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 +440,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 9c41d739125..f0651889a63 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 3f6a4055985..a471cda6be5 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 c3f336afc05..51e4a8300b7 100644 --- a/plugins/debug.cpp +++ b/plugins/debug.cpp @@ -48,30 +48,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 +254,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_; }; @@ -804,9 +804,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 +1062,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 +1096,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 726693b416b..6b41701225d 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 210669a75de..c044fa10447 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 333f1d3bc4e..3b4df00e28c 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 0c0b3453b4a..162ea7bbd45 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -118,7 +118,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) @@ -886,7 +886,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 5bc7c09d6db..6110afe1c07 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 647fa6141ec..4c8a8f6ae26 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) { } diff --git a/plugins/devel/check-structures-sanity/validate.cpp b/plugins/devel/check-structures-sanity/validate.cpp index 24d5327f92c..d6137eff1f3 100644 --- a/plugins/devel/check-structures-sanity/validate.cpp +++ b/plugins/devel/check-structures-sanity/validate.cpp @@ -35,10 +35,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 a19a475bb3b..a17ad9d0725 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 d3672d8d487..05c2024acc3 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 b910b6546be..91b70a26767 100644 --- a/plugins/devel/dumpmats.cpp +++ b/plugins/devel/dumpmats.cpp @@ -32,7 +32,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 +52,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 +82,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 +104,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 +123,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 +134,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 ceaec299219..3b0bd860d4f 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -13,6 +13,7 @@ #include "df/job.h" #include "df/unit.h" #include "df/unit_wound.h" +#include "df/unit_wound_layerst.h" #include "df/world.h" #include @@ -108,7 +109,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 +120,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 +160,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 +187,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 6c4264783e6..a6d862a5a50 100644 --- a/plugins/devel/frozen.cpp +++ b/plugins/devel/frozen.cpp @@ -55,7 +55,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 +72,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 69430e69daf..d9dbf27015f 100644 --- a/plugins/devel/kittens.cpp +++ b/plugins/devel/kittens.cpp @@ -115,14 +115,14 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) uint64_t time2 = GetTimeMs64(); uint64_t delta = time2-timeLast; timeLast = time2; - out.print("Time delta = %d ms\n", int(delta)); + out.print("Time delta = {} ms\n", int(delta)); } if(trackmenu_flg) { if (last_menu != plotinfo->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 +134,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 +158,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 +182,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 +203,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 +300,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()); } }; diff --git a/plugins/devel/memutils.cpp b/plugins/devel/memutils.cpp index c2a7387fdd7..b7ce98ea730 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 a9fba897e13..01397ef7d21 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 ebc03e32b7b..73361c53e78 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 0eca4ea8c9e..9acbce904b5 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/stripcaged.cpp b/plugins/devel/stripcaged.cpp index 14189bdc309..bad22f3e587 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/tilesieve.cpp b/plugins/devel/tilesieve.cpp index 6b36b8dd0d9..9f21d98368b 100644 --- a/plugins/devel/tilesieve.cpp +++ b/plugins/devel/tilesieve.cpp @@ -73,7 +73,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 279fe8a0d5e..ac72de30740 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 18e8d62e32b..d37ed3418a1 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 c3c79122fcc..259978f9bff 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{ @@ -391,7 +390,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 +402,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 +447,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 +483,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)) { @@ -503,7 +500,7 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, 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; } @@ -550,8 +547,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 +556,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 +734,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 +873,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 +895,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 +906,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 +951,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 bc9089a654a..5fbf23b251f 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); @@ -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 045e6c61d63..c1d0eccbc5a 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 eb74433b863..1bd5c97e4dd 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 d883ef4dba8..fff7f033eaf 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 d4fddb48467..35f02381236 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 6f3e32d00f0..7847dfa8e8e 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 00000000000..cdc0c349d22 --- /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 ee418363e7d..5b9d11f78ad 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 b4c55dd8235..8e695bd7487 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 b540ae80b4d..61da64959b5 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 d5b50294a97..37130c6897b 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 84994b28691..ddfc6d8d498 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 0ed06d9284e..8dd3bbc85dd 100644 --- a/plugins/eventful.cpp +++ b/plugins/eventful.cpp @@ -22,6 +22,7 @@ #include "df/unit_wound.h" #include "df/world.h" +#include #include #include #include diff --git a/plugins/examples/persistent_per_save_example.cpp b/plugins/examples/persistent_per_save_example.cpp index 3ca26fc1d3a..4067e1d161e 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 ea45b18d475..d708bc222fe 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 fd77ab691a5..a6b9e234a3c 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 bbd3af3deb7..95bcd164558 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/fastdwarf.cpp b/plugins/fastdwarf.cpp index 0d4808db8c0..d1a6750a5d1 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 c8b0a209e37..23952d8ac25 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 @@ -157,7 +158,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 7b5a416dbb0..5ed962e4d47 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 f87701e0bc4..1fc003972ec 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 8ebf968a0bf..c13e355b6ce 100644 --- a/plugins/flows.cpp +++ b/plugins/flows.cpp @@ -46,11 +46,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 5649d6bdfbe..109c37823b4 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 df716831db8..290772e87ba 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 b0a00b3f212..7c330e00561 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 9bd3f6f0d44..69eba4b1d1c 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -5,6 +5,7 @@ #include "modules/Designations.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Random.h" #include "df/map_block.h" #include "df/map_block_column.h" @@ -20,6 +21,8 @@ #include "df/world_object_data.h" #include "df/world_site.h" +#include + using std::string; using std::vector; using std::set; @@ -76,13 +79,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 +94,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 +106,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 +122,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 +135,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 +157,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 +170,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 +204,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; } } @@ -244,7 +247,7 @@ bool picked(const df::plant* plant, int32_t growth_subtype) { } 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); + 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); if (!farming) { bool istree = (tileMaterial(Maps::getTileBlock(plant->pos)->tiletype[plant->pos.x % 16][plant->pos.y % 16]) == tiletype_material::TREE); @@ -278,14 +281,14 @@ bool designate(color_ostream& out, const df::plant* plant, bool farming) { } 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,8 +312,8 @@ 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)) + 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 Designations::markPlant(plant); } @@ -399,20 +402,20 @@ command_result df_getplants(color_ostream& out, vector & parameters) { plantSelections[i] = selectablePlant(out, plant, farming); switch (plantSelections[i]) { case selectability::Grass: - out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); + 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 +429,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; } @@ -451,7 +454,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 +465,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; } @@ -479,7 +482,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { const df::plant* plant = world->plants.all[i]; df::map_block* cur = Maps::getTileBlock(plant->pos); - TRACE(log, out).print("Examining %s at (%i, %i, %i) [index=%d]\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + TRACE(log, out).print("Examining {} at ({}, {}, {}) [index={}]\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); int x = plant->pos.x % 16; int y = plant->pos.y % 16; @@ -508,7 +511,7 @@ command_result df_getplants(color_ostream& out, vector & parameters) { ++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); + DEBUG(log, out).print("Designated {} at ({}, {}, {}), {}\n", world->raws.plants.all[plant->material]->id, plant->pos.x, plant->pos.y, plant->pos.z, (int)i); collectionCount[plant->material]++; ++count; } @@ -516,11 +519,11 @@ command_result df_getplants(color_ostream& out, vector & parameters) { 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("Updated {} {} designations.\n", collectionCount[i], world->raws.plants.all[i]->id); } out.print("\n"); } - out.print("Updated %d plant designations.\n", (int)count); + out.print("Updated {} plant designations.\n", count); return CR_OK; } diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 13988a21f14..e6920438681 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 9b41c2e2ca2..402a4a7a7b7 100644 --- a/plugins/infinite-sky.cpp +++ b/plugins/infinite-sky.cpp @@ -8,9 +8,15 @@ #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/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 +33,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 +68,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 +86,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 +109,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 +119,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,7 +136,7 @@ static void constructionEventHandler(color_ostream &out, void *ptr) { doInfiniteSky(out, 1); } -void doInfiniteSky(color_ostream& out, int32_t howMany) { +void addBlockColumns(color_ostream& out, int32_t quantity) { int32_t z_count_block = world->map.z_count_block; df::map_block ****block_index = world->map.block_index; @@ -140,14 +147,14 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { 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]; + new df::map_block *[z_count_block + quantity]; 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++) { + 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), @@ -186,12 +193,10 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { 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); + .print("{}, line {}: column is null ({}).\n", __FILE__, __LINE__, bpos); continue; } - df::map_block_column::T_unmined_glyphs *glyphs = - new df::map_block_column::T_unmined_glyphs; + df::block_column_print_infost *glyphs = new df::block_column_print_infost; glyphs->x[0] = 0; glyphs->x[1] = 1; glyphs->x[2] = 2; @@ -210,19 +215,55 @@ void doInfiniteSky(color_ostream& out, int32_t howMany) { }); // Update global z level flags - df::z_level_flags *flags = new df::z_level_flags[z_count_block + howMany]; + 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 < howMany; count++) { + 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 += howMany; - world->map.z_count += howMany; + 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; } +void updateInvasionMap(color_ostream &out, int32_t new_height, df::plot_invasion_mapst& map) { + 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](); + 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; +} + +void doInfiniteSky(color_ostream &out, int32_t quantity) { + addBlockColumns(out, quantity); + + for (auto& invasion : plotinfo->invasions.list) { + updateInvasionMap(out, world->map.z_count, invasion->map); + } + for (auto& entity : world->entities.all) { + for (auto& map : entity->plot_invasion_map) { + if (map->site_id != plotinfo->site_id) + continue; + updateInvasionMap(out, world->map.z_count, map->map); + } + } +} + struct infinitesky_options { // whether to display help bool help = false; @@ -242,7 +283,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 +295,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 37912d8429d..c32b9adf22c 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/logistics.cpp b/plugins/logistics.cpp index 52b9bf43a44..23585356219 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/blueprint.lua b/plugins/lua/blueprint.lua index dfe8f4eae69..8c765d563f8 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) @@ -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/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 62e1a6c49c5..f0fbe17de45 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -119,6 +119,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 @@ -235,6 +239,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() @@ -1307,10 +1312,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 +1359,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 diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua index b988e9d42eb..192989d8a16 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,13 +348,18 @@ 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', @@ -365,8 +370,8 @@ CarveOverlay.ATTRS{ frame={w=0, h=0}, } -function CarveOverlay:onRenderFrame() - paintScreenCarve() +function DesignatedOverlay:onRenderFrame() + paintScreenDesignated() end -- -------------------------------- @@ -374,7 +379,7 @@ end -- OVERLAY_WIDGETS = { - asciicarve=CarveOverlay, + asciidesignated=DesignatedOverlay, warmdamp=WarmDampOverlay, warmdamptoolbar=WarmDampToolbarOverlay, } diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 74d0b96231f..5c04fba2df6 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 2774bd80ee0..80a6196e130 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -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 8d7c9a7e2d2..c1d1bcb4346 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -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 @@ -526,16 +538,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 +556,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 8dd4e507af9..3851920dafa 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 fe42cbb1ed6..af066fb1758 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, diff --git a/plugins/lua/sort/deathcause_button.lua b/plugins/lua/sort/deathcause_button.lua new file mode 100644 index 00000000000..b149601a7a9 --- /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 c0ec8185057..505e02b7431 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 feadee99c0d..4f0462dfb78 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -278,8 +278,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 +292,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 35a55694932..c353d5a9480 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 @@ -169,6 +170,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 +288,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 +317,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 c823c4c34e4..53a591251d8 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/spectate.lua b/plugins/lua/spectate.lua new file mode 100644 index 00000000000..953895eab02 --- /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-config/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 3b2b0fe6698..dc44d23045f 100644 --- a/plugins/lua/stockflow.lua +++ b/plugins/lua/stockflow.lua @@ -283,7 +283,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 diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index ac48ed6fc9b..33f6e375ada 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -8,7 +8,7 @@ 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_LIBRARY_DIR = dfhack.getHackPath() .. '/data/stockpiles' local BAD_FILENAME_REGEX = '[^%w._]' diff --git a/plugins/lua/suspendmanager.lua b/plugins/lua/suspendmanager.lua index 594fb31155f..86f0504ed39 100644 --- a/plugins/lua/suspendmanager.lua +++ b/plugins/lua/suspendmanager.lua @@ -26,6 +26,25 @@ 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 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 +88,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 +96,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 +132,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 +155,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 46f3e712d9e..d32a4b75703 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/zone.lua b/plugins/lua/zone.lua index 92940ef48ed..2923f8e3331 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 b81a4f1daa2..7ecdb0d344a 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 64cfcc301bc..e471ac20d37 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 89df7832453..95932acb520 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -44,8 +44,8 @@ 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 std::filesystem::path ORDERS_DIR = std::filesystem::path("dfhack-config") / "orders"; +static std::filesystem::path ORDERS_LIBRARY_DIR = Core::getInstance().getHackPath() / "data" / "orders"; static command_result orders_command(color_ostream & out, std::vector & parameters); @@ -93,7 +93,7 @@ static command_result orders_command(color_ostream & out, std::vector files; + std::map files; if (0 < Filesystem::listdir_recursive(ORDERS_LIBRARY_DIR, files, 0, false)) { // if the library directory doesn't exist, just skip it return; @@ -145,15 +145,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 +162,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; + std::map files; Filesystem::listdir_recursive(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); @@ -506,7 +506,7 @@ static command_result orders_export_command(color_ostream & out, const std::stri Filesystem::mkdir(ORDERS_DIR); - std::ofstream file(ORDERS_DIR + "/" + name + ".json"); + std::ofstream file(ORDERS_DIR / ( name + ".json")); file << orders << std::endl; @@ -765,8 +765,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 +774,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 +924,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 ? ORDERS_LIBRARY_DIR : ORDERS_DIR) / (fname + ".json")); Json::Value orders; { @@ -1008,8 +1007,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 580ca3fec64..85e5cce882e 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 3bf9e2c0106..3fd1eaaad1a 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 168c5f0bfd9..0370015dcb2 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 52f61516373..eca7b888de7 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 17f06c6c929..a695b22e318 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 31c2df5edca..9df22004063 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"); @@ -129,7 +134,7 @@ static command_result df_probe(color_ostream &out, vector & parameters) } 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(" {}", 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(" {}", 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 159b557e4fb..5dd712e872e 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 @@ -426,7 +428,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 +606,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 +633,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 +835,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 +890,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 d40e1bc6dcc..a17f5fb7e27 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 7cc715147b8..62475ce1aa4 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 1353243b2ba..58c84b90755 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 a092a120973..b7dc7bdc883 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 ae6683dee95..60c04ac25b7 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; } @@ -625,7 +627,7 @@ static command_result CheckHashes(color_ostream &stream, const EmptyMessage *in) } 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; } @@ -809,10 +811,10 @@ 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); MaterialDefinition *mat_def = out->add_material_list(); @@ -820,9 +822,9 @@ static command_result GetMaterialList(color_ostream &stream, const EmptyMessage 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++) @@ -890,7 +892,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 +1156,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 +1189,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 +1517,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 +1536,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 +1679,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 +1774,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 +1987,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++) { @@ -2133,28 +2131,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 +2262,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 +2365,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); diff --git a/plugins/rendermax/renderer_light.cpp b/plugins/rendermax/renderer_light.cpp index 469c849091f..b905cd93c58 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 5515bb39a2c..39e59e4f6b9 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 f1cfa90203d..6aacbde64f5 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 1feb72bbc6d..e5bd2479f57 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 994ac067ace..7eecabd2c56 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 198dd190e0b..977c767aebc 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 ca30fc76b41..3d9fbd537d4 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/sort.cpp b/plugins/sort.cpp index add54688c49..f1b700c5afc 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 00000000000..072ad813c74 --- /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 d2de072d8bf..00000000000 --- 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 f94b1e73a91..00000000000 --- 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 ab736ed531e..00000000000 --- 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 36bd2d94b29..00000000000 --- 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 befd53f10a0..06b14aa6c01 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 1671d08991a..524bd16896a 100644 --- a/plugins/stockflow.cpp +++ b/plugins/stockflow.cpp @@ -284,7 +284,7 @@ static bool apply_hooks(color_ostream &out, bool enabling) { } 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"); + out.printerr("Could not {} stockflow hooks!\n", enabling? "insert": "remove"); return false; } @@ -336,7 +336,7 @@ static command_result stockflow_cmd(color_ostream &out, vector & parame } } - 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", fast? ", in fast mode": ""); enabled = desired; return CR_OK; } diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index 1efdd4df605..1392e8c12c4 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -22,8 +22,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 +32,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 +56,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 +80,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 +106,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 f585ef2de4c..15f9838df26 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 f315e7d674f..46efd95044c 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)); @@ -340,7 +332,7 @@ 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; } @@ -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()); } @@ -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/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 681e7d9279f..49a6d1c42e5 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 33ce855b42b..7165449ccaf 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -29,7 +29,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 +62,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 +72,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 +87,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 +95,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 +111,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 +126,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 +150,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 +171,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 +187,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 9d851db0800..8791e2c2669 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 9d851db080012add38631af37ea5c5b60a893e79 +Subproject commit 8791e2c26693cea552c42700e38c87503b7ac7da diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index 4955d49e960..2ab221195df 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -149,7 +149,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 +164,18 @@ 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; + } + unit = df::unit::find(std::stoi(parameters[i])); + if (!unit) + return CR_FAILURE; + } else if(parameters[i] == "--unit") { unit = DFHack::Gui::getSelectedUnit(out); @@ -190,7 +202,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 +260,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 +428,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; } @@ -644,8 +656,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; @@ -722,8 +734,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)) @@ -805,8 +817,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 == 0 && getCreatedMetalBars(pref->matindex) > 0) mats.push_back(pref->matindex); } @@ -835,8 +847,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 +879,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)) diff --git a/plugins/suspendmanager.cpp b/plugins/suspendmanager.cpp index 10c8d241f0a..13a37d1901f 100644 --- a/plugins/suspendmanager.cpp +++ b/plugins/suspendmanager.cpp @@ -113,11 +113,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 +389,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 +471,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 +484,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 +504,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); @@ -639,7 +647,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(); @@ -685,7 +693,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) @@ -712,7 +720,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); } @@ -764,7 +772,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 +793,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 +854,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 +870,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; @@ -888,7 +896,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, true); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else if (parameters[2] == "false") { @@ -896,7 +904,7 @@ static command_result do_command(color_ostream &out, vector ¶meters) config.set_bool(CONFIG_PREVENT_BLOCKING, false); if (is_enabled) { do_cycle(out); - out.print("%s", suspendmanager_instance->getStatus(out).c_str()); + out.print("{}", suspendmanager_instance->getStatus(out)); } return CR_OK; } else @@ -915,7 +923,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 +938,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 ad96302887e..1018999ec4b 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 95c5bd44243..8c8407d1893 100644 --- a/plugins/tiletypes.cpp +++ b/plugins/tiletypes.cpp @@ -1156,7 +1156,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 +1196,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 +1399,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 a472c2efec3..deb6c45e4df 100644 --- a/plugins/timestream.cpp +++ b/plugins/timestream.cpp @@ -32,6 +32,7 @@ #include "df/activity_event_worshipst.h" #include "df/building_nest_boxst.h" #include "df/building_trapst.h" +#include "df/buildingitemst.h" #include "df/init.h" #include "df/item_eggst.h" #include "df/unit.h" @@ -77,7 +78,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 +157,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 +170,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 +181,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 +189,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 +221,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 +230,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 +246,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 +270,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; } @@ -339,7 +341,6 @@ static void decrement_counter(T *obj, FT T::*field, int32_t 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); @@ -409,7 +410,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 +616,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 +647,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 +669,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 +678,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 46f7101fab6..cab0241bd41 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 74276bba1cf..eeed5027a86 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 00000000000..097e86e0189 --- /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 00000000000..287c2489242 --- /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 ca9f1ffe060..6844a23623d 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 2b1d91b8264..24c3a67a3d9 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 612e042282d..1e69b7e193a 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 b1aa3b365d6..92aec15dd7e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b1aa3b365d611432195b80d1d064c942f54a78c7 +Subproject commit 92aec15dd7eff76b76ff74efb3164e31b7f0e5bd diff --git a/test/core.lua b/test/core.lua index 58f139a8cb7..7270f570676 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/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index a5748afd5f1..5a7d006d599 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/print.lua b/test/library/print.lua index 28a2e703704..79243d8989c 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 diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 13febff566b..ab58187b31e 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -24,12 +24,39 @@ function test.overlappingGlobals() end end -function test.viewscreenDtors() +local known_bad_types = { + -- renderer base class has non-destructible padding declared + renderer_2d_base=true, + renderer_2d=true, + renderer_offscreen=true, + + -- abstract base classes that aren't instantiable + active_script_varst=true, + widget_sheet_button=true, +} + +if dfhack.getOSType() == 'linux' then + -- empty destructors are declared inline for these types, + -- and gcc appears to optimize them out + known_bad_types.mental_picture_propertyst = true + known_bad_types.region_block_eventst = true +end + +function test.destructors() + local count = 1 for name, type in pairs(df) do - if name:startswith('viewscreen') then - print('testing', name) - local v = type:new() + if known_bad_types[name] then + goto continue + end + print(('testing constructor %5d: %s'):format(count, name)) + local ok, v = pcall(function() return type:new() end) + if not ok then + print(' constructor failed; skipping destructor test') + else + print(' destructor ok') expect.true_(v:delete(), "destructor returned false: " .. name) end + count = count + 1 + ::continue:: end end