diff --git a/.github/release_template.md b/.github/release_template.md new file mode 100644 index 000000000..5ca0a1983 --- /dev/null +++ b/.github/release_template.md @@ -0,0 +1,64 @@ +#### Q: How do I download DFHack? +**A:** Either add to your Steam library from our [Steam page](https://store.steampowered.com/app/2346660/DFHack) or scroll to the latest release on our [GitHub releases page](https://github.com/DFHack/dfhack/releases), expand the "Assets" list, and download the file for your platform (e.g. `dfhack-XX.XX-rX-Windows-64bit.zip`. + +------------- + +This release is compatible with all distributions of Dwarf Fortress: [Steam](https://store.steampowered.com/app/975370/Dwarf_Fortress/), [Itch](https://kitfoxgames.itch.io/dwarf-fortress), and [Classic](https://www.bay12games.com/dwarves/). + +- [Install DFHack from Steam](https://store.steampowered.com/app/2346660/DFHack) +- [Manual install](https://docs.dfhack.org/en/stable/docs/Installing.html#installing) +- [Quickstart guide (for players)](https://docs.dfhack.org/en/stable/docs/Quickstart.html#quickstart) +- [Modding guide (for modders)](https://docs.dfhack.org/en/stable/docs/guides/modding-guide.html) + +Please report any issues (or feature requests) on the DFHack [GitHub issue tracker](https://github.com/DFHack/dfhack/issues). When reporting issues, please upload a zip file of your savegame and a zip file of your `mods` directory to the cloud and add links to the GitHub issue. Make sure your files are downloadable by "everyone with the link". We need your savegame to reproduce the problem and test the fix, and we need your active mods so we can load your savegame. Issues with savegames and mods attached get fixed first! + +Highlights +---------------------------------- + +
+Highlight 1, Highlight 2 + +### Highlight 1 + +Demo screenshot/vidcap + +Text + +### Highlight 2 + +Demo screenshot/vidcap + +Text + +
+ +Announcements +---------------------------------- + +
+Annc 1, PSAs + +### Annc 1 + +Text + +### PSAs + +As always, remember that, just like the vanilla DF game, DFHack tools can also have bugs. It is a good idea to **save often and keep backups** of the forts that you care about. + +Many DFHack tools that worked in previous (pre-Steam) versions of DF have not been updated yet and are marked with the "unavailable" tag in their docs. If you try to run them, they will show a warning and exit immediately. You can run the command again to override the warning (though of course the tools may not work). We make no guarantees of reliability for the tools that are marked as "unavailable". + +The in-game interface for running DFHack commands (`gui/launcher`) will not show "unavailable" tools by default. You can still run them if you know their names, or you can turn on dev mode by hitting Ctrl-D while in `gui/launcher` and they will be added to the autocomplete list. Some tools do not compile yet and are not available at all, even when in dev mode. + +If you see a tool complaining about the lack of a cursor, know that it's referring to the **keyboard** cursor (which used to be the only real option in Dwarf Fortress). You can enable the keyboard cursor by entering mining mode or selecting the dump/forbid tool and hitting Alt-K (the DFHack keybinding for `toggle-kbd-cursor`). We're working on making DFHack tools more mouse-aware and accessible so this step isn't necessary in the future. + +
+ +Generated release notes +==================== + +
+New tools, fixes, and improvements + +%RELEASE_NOTES% +
diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 000000000..aa5571e0b --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,168 @@ +name: Build linux64 + +on: + workflow_call: + inputs: + dfhack_ref: + type: string + scripts_ref: + type: string + structures_ref: + type: string + artifact-name: + type: string + append-date-and-hash: + type: boolean + default: false + cache-id: + type: string + default: '' + cache-readonly: + type: boolean + default: false + platform-files: + type: boolean + default: true + common-files: + type: boolean + default: true + docs: + type: boolean + default: false + html: + type: boolean + default: true + stonesense: + type: boolean + default: false + extras: + type: boolean + default: false + tests: + type: boolean + default: false + xml-dump-type-sizes: + type: boolean + default: false + gcc-ver: + type: string + default: "10" + +jobs: + build-linux64: + name: Build linux64 + runs-on: ubuntu-22.04 + steps: + - name: Install basic build dependencies + run: | + sudo apt-get update + sudo apt-get install ninja-build + - name: Install binary build dependencies + if: inputs.platform-files || inputs.xml-dump-type-sizes + run: | + sudo apt-get install \ + ccache \ + gcc-${{ inputs.gcc-ver }} \ + g++-${{ inputs.gcc-ver }} \ + libxml-libxslt-perl + - name: Install stonesense dependencies + if: inputs.stonesense + run: sudo apt-get install libgl-dev + - name: Install doc dependencies + if: inputs.docs + run: pip install 'sphinx<4.4.0' + - name: Clone DFHack + uses: actions/checkout@v3 + with: + repository: ${{ inputs.dfhack_ref && github.repository || 'DFHack/dfhack' }} + ref: ${{ inputs.dfhack_ref }} + submodules: true + fetch-depth: ${{ !inputs.platform-files && 1 || 0 }} + - name: Clone scripts + if: inputs.scripts_ref + uses: actions/checkout@v3 + with: + repository: ${{ inputs.scripts_ref && github.repository || 'DFHack/scripts' }} + ref: ${{ inputs.scripts_ref }} + path: scripts + - name: Clone structures + if: inputs.structures_ref + uses: actions/checkout@v3 + with: + repository: ${{ inputs.structures_ref && github.repository || 'DFHack/df-structures' }} + ref: ${{ inputs.structures_ref }} + path: library/xml + - name: Fetch ccache + if: inputs.platform-files + uses: actions/cache/restore@v3 + with: + path: ~/.cache/ccache + key: linux-gcc-${{ inputs.gcc-ver }}-${{ inputs.cache-id }}-${{ github.sha }} + restore-keys: | + linux-gcc-${{ inputs.gcc-ver }}-${{ inputs.cache-id }} + linux-gcc-${{ inputs.gcc-ver }} + - name: Configure DFHack + env: + CC: gcc-${{ inputs.gcc-ver }} + CXX: g++-${{ inputs.gcc-ver }} + run: | + cmake \ + -S . \ + -B build \ + -G Ninja \ + -DCMAKE_INSTALL_PREFIX=build/image \ + -DCMAKE_BUILD_TYPE=Release \ + ${{ inputs.platform-files && '-DCMAKE_C_COMPILER_LAUNCHER=ccache' || '' }} \ + ${{ inputs.platform-files && '-DCMAKE_CXX_COMPILER_LAUNCHER=ccache' || '' }} \ + -DBUILD_LIBRARY:BOOL=${{ inputs.platform-files }} \ + -DBUILD_PLUGINS:BOOL=${{ inputs.platform-files }} \ + -DBUILD_STONESENSE:BOOL=${{ inputs.stonesense }} \ + -DBUILD_DEV_PLUGINS:BOOL=${{ inputs.extras }} \ + -DBUILD_SIZECHECK:BOOL=${{ inputs.extras }} \ + -DBUILD_SKELETON:BOOL=${{ inputs.extras }} \ + -DBUILD_DOCS:BOOL=${{ inputs.docs }} \ + -DBUILD_DOCS_NO_HTML:BOOL=${{ !inputs.html }} \ + -DBUILD_TESTS:BOOL=${{ inputs.tests }} \ + -DBUILD_XMLDUMP:BOOL=${{ inputs.xml-dump-type-sizes }} \ + ${{ inputs.xml-dump-type-sizes && '-DINSTALL_XMLDUMP:BOOL=1' || ''}} \ + -DINSTALL_DATA_FILES:BOOL=${{ inputs.common-files }} \ + -DINSTALL_SCRIPTS:BOOL=${{ inputs.common-files }} + - name: Build DFHack + run: ninja -C build install + - name: Run cpp tests + if: inputs.platform-files + run: ninja -C build test + - name: Finalize cache + if: inputs.platform-files + run: | + ccache --show-stats --verbose + ccache --max-size 40M + ccache --cleanup + ccache --max-size 500M + ccache --zero-stats + - name: Save ccache + if: inputs.platform-files && !inputs.cache-readonly + uses: actions/cache/save@v3 + with: + path: ~/.cache/ccache + key: linux-gcc-${{ inputs.gcc-ver }}-${{ inputs.cache-id }}-${{ github.sha }} + - name: Format artifact name + if: inputs.artifact-name + id: artifactname + run: | + if test "false" = "${{ inputs.append-date-and-hash }}"; then + echo name=${{ inputs.artifact-name }} >> $GITHUB_OUTPUT + else + echo name=${{ inputs.artifact-name }}-$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + fi + - name: Prep artifact + if: inputs.artifact-name + run: | + cd build/image + tar cjf ../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + - name: Upload artifact + if: inputs.artifact-name + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.artifactname.outputs.name }} + path: ${{ steps.artifactname.outputs.name }}.tar.bz2 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 000000000..795482d80 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,135 @@ +name: Build win64 + +on: + workflow_call: + inputs: + dfhack_ref: + type: string + scripts_ref: + type: string + structures_ref: + type: string + artifact-name: + type: string + append-date-and-hash: + type: boolean + default: false + cache-id: + type: string + default: '' + cache-readonly: + type: boolean + default: false + platform-files: + type: boolean + default: true + common-files: + type: boolean + default: true + docs: + type: boolean + default: false + html: + type: boolean + default: true + stonesense: + type: boolean + default: false + tests: + type: boolean + default: false + xml-dump-type-sizes: + type: boolean + default: false + launchdf: + type: boolean + default: false + + +jobs: + build-win64: + name: Build win64 + runs-on: ubuntu-22.04 + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install ccache + - name: Clone DFHack + uses: actions/checkout@v3 + with: + repository: ${{ inputs.dfhack_ref && github.repository || 'DFHack/dfhack' }} + ref: ${{ inputs.dfhack_ref }} + submodules: true + fetch-depth: 0 + - name: Clone scripts + if: inputs.scripts_ref + uses: actions/checkout@v3 + with: + repository: ${{ inputs.scripts_ref && github.repository || 'DFHack/scripts' }} + ref: ${{ inputs.scripts_ref }} + path: scripts + - name: Clone structures + if: inputs.structures_ref + uses: actions/checkout@v3 + with: + repository: ${{ inputs.structures_ref && github.repository || 'DFHack/df-structures' }} + ref: ${{ inputs.structures_ref }} + path: library/xml + - name: Get 3rd party SDKs + if: inputs.launchdf + uses: actions/checkout@v3 + with: + repository: DFHack/3rdparty + ref: main + ssh-key: ${{ secrets.DFHACK_3RDPARTY_TOKEN }} + path: depends/steam + - name: Fetch ccache + if: inputs.platform-files + uses: actions/cache/restore@v3 + with: + path: build/win64-cross/ccache + key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} + restore-keys: | + win-msvc-${{ inputs.cache-id }} + win-msvc + - name: Cross-compile + env: + CMAKE_EXTRA_ARGS: -DBUILD_LIBRARY=${{ inputs.platform-files }} -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' || '' }} + run: | + cd build + bash -x build-win64-from-linux.sh + - name: Finalize cache + run: | + cd build + ccache -d win64-cross/ccache --show-stats --verbose + ccache -d win64-cross/ccache --max-size 150M + ccache -d win64-cross/ccache --cleanup + ccache -d win64-cross/ccache --max-size 500M + ccache -d win64-cross/ccache --zero-stats + - name: Save ccache + if: inputs.platform-files && !inputs.cache-readonly + uses: actions/cache/save@v3 + with: + path: build/win64-cross/ccache + key: win-msvc-${{ inputs.cache-id }}-${{ github.sha }} + - name: Format artifact name + if: inputs.artifact-name + id: artifactname + run: | + if test "false" = "${{ inputs.append-date-and-hash }}"; then + echo name=${{ inputs.artifact-name }} >> $GITHUB_OUTPUT + else + echo name=${{ inputs.artifact-name }}-$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + fi + - name: Prep artifact + if: inputs.artifact-name + run: | + cd build/win64-cross/output + tar cjf ../../../${{ steps.artifactname.outputs.name }}.tar.bz2 . + - name: Upload artifact + if: inputs.artifact-name + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.artifactname.outputs.name }} + path: ${{ steps.artifactname.outputs.name }}.tar.bz2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0b621c4c..10a6d332d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,218 +3,32 @@ name: Build on: [push, pull_request] jobs: - build: - runs-on: ${{ matrix.os }} - name: build (Linux, GCC ${{ matrix.gcc }}, ${{ matrix.plugins }} plugins) - strategy: - fail-fast: false - matrix: - os: - - ubuntu-22.04 - gcc: - - 10 - plugins: - - default - include: - - os: ubuntu-22.04 - gcc: 12 - plugins: all - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install \ - ccache \ - libgtk2.0-0 \ - libncursesw5 \ - libsdl-image1.2-dev \ - libsdl-ttf2.0-dev \ - libsdl1.2-dev \ - libxml-libxml-perl \ - libxml-libxslt-perl \ - lua5.3 \ - ninja-build \ - zlib1g-dev - pip install 'sphinx<4.4.0' - - name: Install GCC - run: | - sudo apt-get install gcc-${{ matrix.gcc }} g++-${{ matrix.gcc }} - - name: Clone DFHack - uses: actions/checkout@v1 - with: - fetch-depth: 0 # unlimited - we need past tags - submodules: true - - name: Set up environment - id: env_setup - run: | - DF_VERSION="$(sh ci/get-df-version.sh)" - echo "df_version=${DF_VERSION}" >> $GITHUB_OUTPUT - echo "DF_VERSION=${DF_VERSION}" >> $GITHUB_ENV - echo "DF_FOLDER=${HOME}/DF/${DF_VERSION}/df_linux" >> $GITHUB_ENV - echo "CCACHE_DIR=${HOME}/.ccache" >> $GITHUB_ENV - # - name: Fetch DF cache - # uses: actions/cache@v3 - # with: - # path: ~/DF - # key: dfcache-${{ steps.env_setup.outputs.df_version }}-${{ hashFiles('ci/download-df.sh') }} - - name: Fetch ccache - uses: actions/cache@v3 - with: - path: ~/.ccache - key: ccache-${{ matrix.os }}-gcc-${{ matrix.gcc }}-${{ github.ref_name }}-${{ github.sha }} - restore-keys: | - ccache-${{ matrix.os }}-gcc-${{ matrix.gcc }}-${{ github.ref_name }} - ccache-${{ matrix.os }}-gcc-${{ matrix.gcc }} - # - name: Download DF - # run: | - # sh ci/download-df.sh - - name: Configure DFHack - env: - CC: gcc-${{ matrix.gcc }} - CXX: g++-${{ matrix.gcc }} - run: | - cmake \ - -S . \ - -B build-ci \ - -G Ninja \ - -DDFHACK_BUILD_ARCH=64 \ - -DBUILD_TESTS:BOOL=ON \ - -DBUILD_DEV_PLUGINS:BOOL=${{ matrix.plugins == 'all' }} \ - -DBUILD_SIZECHECK:BOOL=${{ matrix.plugins == 'all' }} \ - -DBUILD_SKELETON:BOOL=${{ matrix.plugins == 'all' }} \ - -DBUILD_STONESENSE:BOOL=${{ matrix.plugins == 'all' }} \ - -DBUILD_SUPPORTED:BOOL=1 \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DCMAKE_INSTALL_PREFIX="$DF_FOLDER" - - name: Build DFHack - run: | - ninja -C build-ci install - ccache --max-size 50M - ccache --cleanup - ccache --show-stats - - name: Run cpp unit tests - id: run_tests_cpp - run: | - ninja -C build-ci test - exit $? - # - name: Run lua tests - # id: run_tests_lua - # run: | - # export TERM=dumb - # status=0 - # script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) - # python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) - # mkdir -p artifacts - # cp "$DF_FOLDER"/test*.json "$DF_FOLDER"/*.log artifacts || status=$((status + 4)) - # exit $status - # - name: Upload test artifacts - # uses: actions/upload-artifact@v1 - # if: (success() || failure()) && steps.run_tests.outcome != 'skipped' - # continue-on-error: true - # with: - # name: test-artifacts-${{ matrix.gcc }} - # path: artifacts - - name: Clean up DF folder - # prevent DFHack-generated files from ending up in the cache - # (download-df.sh also removes them, this is just to save cache space) - if: success() || failure() - run: | - rm -rf "$DF_FOLDER" + test: + uses: ./.github/workflows/test.yml + with: + dfhack_ref: ${{ github.ref }} + secrets: inherit - build-cross-win64: - name: Build MSVC win64 - runs-on: ubuntu-22.04 - steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install ccache - - name: Clone DFHack - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 - - name: Fetch ccache - uses: actions/cache@v3 - with: - path: build/win64-cross/ccache - key: ccache-win64-cross-msvc-${{ github.ref_name }}-${{ github.sha }} - restore-keys: | - ccache-win64-cross-msvc-${{ github.ref_name }} - ccache-win64-cross-msvc - - name: Cross-compile win64 artifacts - env: - CMAKE_EXTRA_ARGS: '-DBUILD_STONESENSE:BOOL=1' - run: | - cd build - bash -x build-win64-from-linux.sh - ccache -d win64-cross/ccache --max-size 200M - ccache -d win64-cross/ccache --cleanup - ccache -d win64-cross/ccache --show-stats - - name: Format artifact name - id: artifactname - run: | - echo name=$(date +%Y%m%d)-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - - name: Upload win64 artifacts - uses: actions/upload-artifact@v3 - with: - name: dfhack-win64-build-${{ steps.artifactname.outputs.name }} - path: build/win64-cross/output/* + package: + uses: ./.github/workflows/package.yml + with: + dfhack_ref: ${{ github.ref }} + secrets: inherit docs: - runs-on: ubuntu-22.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install dependencies - run: | - pip install 'sphinx' - - name: Clone DFHack - uses: actions/checkout@v1 - with: - submodules: true - - name: Build docs - run: | - sphinx-build -W --keep-going -j auto --color . docs/html + uses: ./.github/workflows/build-linux.yml + with: + dfhack_ref: ${{ github.ref }} + platform-files: false + common-files: false + docs: true + secrets: inherit lint: - runs-on: ubuntu-22.04 - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install Lua - run: | - sudo apt-get update - sudo apt-get install lua5.3 - - name: Clone DFHack - uses: actions/checkout@v1 - with: - submodules: true - # don't need tags here - - name: Check whitespace - run: | - python ci/lint.py --git-only --github-actions - - name: Check Authors.rst - if: success() || failure() - run: | - python ci/authors-rst.py - - name: Check for missing documentation - if: success() || failure() - run: | - python ci/script-docs.py - - name: Check Lua syntax - if: success() || failure() - run: | - python ci/script-syntax.py --ext=lua --cmd="luac5.3 -p" --github-actions + uses: ./.github/workflows/lint.yml + with: + dfhack_ref: ${{ github.ref }} + secrets: inherit check-pr: runs-on: ubuntu-latest diff --git a/.github/workflows/buildmaster-rebuild.yml b/.github/workflows/buildmaster-rebuild.yml deleted file mode 100644 index d4c7a70e6..000000000 --- a/.github/workflows/buildmaster-rebuild.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Buildmaster rebuild - -on: - workflow_dispatch: - inputs: - pull_request: - description: Pull request ID - type: string - required: true # remove if we support commit rebuilds later - -jobs: - rebuild: - runs-on: ubuntu-latest - name: Trigger Buildmaster - steps: - - name: Set up Python 3 - uses: actions/setup-python@v4 - with: - python-version: 3 - - name: Install dependencies - run: | - pip install requests - - name: Clone DFHack - uses: actions/checkout@v1 - with: - fetch-depth: 1 - submodules: false - - name: Run - env: - DFHACK_BUILDMASTER_WEBHOOK_URL: ${{ secrets.DFHACK_BUILDMASTER_WEBHOOK_URL }} - DFHACK_BUILDMASTER_WEBHOOK_SECRET: ${{ secrets.DFHACK_BUILDMASTER_WEBHOOK_SECRET }} - GITHUB_REPO: ${{ github.repository }} - GITHUB_TOKEN: ${{ github.token }} - run: | - python ci/buildmaster-rebuild-pr.py --pull-request ${{ github.event.inputs.pull_request }} diff --git a/.github/workflows/clean-cache.yml b/.github/workflows/clean-cache.yml index d3e12959d..e88e46c01 100644 --- a/.github/workflows/clean-cache.yml +++ b/.github/workflows/clean-cache.yml @@ -1,5 +1,7 @@ name: Clean up PR caches + on: + workflow_call: pull_request_target: types: - closed @@ -18,7 +20,7 @@ jobs: BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" echo "Fetching list of cache keys" - cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1) + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1) set +e echo "Deleting caches..." diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 000000000..8ce6a3bfa --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,81 @@ +name: Deploy to GitHub + +on: + push: + tags: + - '*-r*' + + workflow_dispatch: + inputs: + ref: + description: Tag + required: true + +jobs: + package: + uses: ./.github/workflows/package.yml + with: + dfhack_ref: ${{ github.event.inputs && github.event.inputs.ref || github.event.ref }} + append-date-and-hash: false + cache-readonly: true + launchdf: true + secrets: inherit + + create-update-release: + name: Draft GitHub release + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Install doc dependencies + run: pip install 'sphinx<4.4.0' + - name: Clone DFHack + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs && github.event.inputs.ref || github.event.ref }} + submodules: true + - name: Get tag + id: gettag + run: | + TAG=$(git describe --tags --abbrev=0 --exact-match) + echo name="$TAG" >> $GITHUB_OUTPUT + echo type=$(echo "$TAG" | egrep 'r[0-9]+$' && echo "release" || echo "prerelease") >> $GITHUB_OUTPUT + - name: Generate release text + run: | + python docs/gen_changelog.py -a + CHANGELOG_FILE=docs/changelogs/${{ steps.gettag.outputs.name }}-github.txt + if ! test -f $CHANGELOG_FILE; then CHANGELOG_FILE=docs/changelogs/future-github.txt; fi + TOKEN_LINE=$(grep -Fhne '%RELEASE_NOTES%' .github/release_template.md | sed 's/:.*//') + head -n $((TOKEN_LINE - 1)) .github/release_template.md > release_body.md + CHANGELOG_LINES=$(wc -l <$CHANGELOG_FILE) + tail -n $((CHANGELOG_LINES - 4)) $CHANGELOG_FILE >> release_body.md + tail -n 1 .github/release_template.md >> release_body.md + cat release_body.md + - name: Stage release + uses: actions/download-artifact@v3 + - name: Prep artifacts + run: | + mkdir artifacts + cd dfhack-windows64-build + tar xjf dfhack-windows64-build.tar.bz2 + rm dfhack-windows64-build.tar.bz2 + zip -qr ../artifacts/dfhack-${{ steps.gettag.outputs.name }}-Windows-64bit.zip . + cd ../dfhack-linux64-build + mv dfhack-linux64-build.tar.bz2 ../artifacts/dfhack-${{ steps.gettag.outputs.name }}-Linux-64bit.tar.bz2 + - name: Create or update GitHub release + uses: ncipollo/release-action@v1 + with: + artifacts: "artifacts/dfhack-*" + bodyFile: "release_body.md" + allowUpdates: true + artifactErrorsFailBuild: true + draft: true + name: "DFHack ${{ steps.gettag.outputs.name }}" + omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true + prerelease: ${{ steps.gettag.outputs.type == 'prerelease' }} + replacesArtifacts: true + tag: ${{ steps.gettag.outputs.name }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..77be37f37 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +name: Lint + +on: + workflow_call: + inputs: + dfhack_ref: + type: string + scripts_ref: + type: string + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Install Lua + run: | + sudo apt-get update + sudo apt-get install lua5.3 + - name: Clone DFHack + uses: actions/checkout@v3 + with: + repository: ${{ inputs.dfhack_ref && github.repository || 'DFHack/dfhack' }} + ref: ${{ inputs.dfhack_ref }} + - name: Get scripts submodule ref + if: '!inputs.scripts_ref' + id: scriptssubmoduleref + run: echo ref=$(git submodule | fgrep scripts | cut -c2-41) >> $GITHUB_OUTPUT + - name: Clone scripts + uses: actions/checkout@v3 + with: + repository: ${{ inputs.scripts_ref && github.repository || 'DFHack/scripts' }} + ref: ${{ inputs.scripts_ref || steps.scriptssubmoduleref.outputs.ref }} + path: scripts + - name: Check whitespace + run: python ci/lint.py --git-only --github-actions + - name: Check Authors.rst + if: always() + run: python ci/authors-rst.py + - name: Check for missing documentation + if: always() + run: python ci/script-docs.py + - name: Check Lua syntax + if: always() + run: python ci/script-syntax.py --ext=lua --cmd="luac5.3 -p" --github-actions diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 000000000..733f567cf --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,52 @@ +name: Package + +on: + workflow_call: + inputs: + dfhack_ref: + type: string + scripts_ref: + type: string + structures_ref: + type: string + append-date-and-hash: + type: boolean + default: true + cache-readonly: + type: boolean + default: false + launchdf: + type: boolean + default: false + +jobs: + package-win64: + name: Windows + uses: ./.github/workflows/build-windows.yml + with: + dfhack_ref: ${{ inputs.dfhack_ref }} + scripts_ref: ${{ inputs.scripts_ref }} + structures_ref: ${{ inputs.structures_ref }} + artifact-name: dfhack-windows64-build + append-date-and-hash: ${{ inputs.append-date-and-hash }} + cache-id: release + cache-readonly: ${{ inputs.cache-readonly }} + stonesense: true + docs: true + launchdf: ${{ inputs.launchdf }} + secrets: inherit + + package-linux: + name: Linux + uses: ./.github/workflows/build-linux.yml + with: + dfhack_ref: ${{ inputs.dfhack_ref }} + scripts_ref: ${{ inputs.scripts_ref }} + structures_ref: ${{ inputs.structures_ref }} + artifact-name: dfhack-linux64-build + append-date-and-hash: ${{ inputs.append-date-and-hash }} + cache-id: release + cache-readonly: ${{ inputs.cache-readonly }} + stonesense: true + docs: true + secrets: inherit diff --git a/.github/workflows/steam-deploy.yml b/.github/workflows/steam-deploy.yml new file mode 100644 index 000000000..d4c4d9e8a --- /dev/null +++ b/.github/workflows/steam-deploy.yml @@ -0,0 +1,92 @@ +name: Deploy to Steam + +on: + push: + tags: + - '*-r*' + + workflow_dispatch: + inputs: + ref: + description: Branch or commit hash + type: string + required: true + default: develop + version: + description: Version or build description + type: string + required: true + release_channel: + description: Steam release channel + type: string + required: true + default: staging + +jobs: + depot-common: + name: Common depot files + uses: ./.github/workflows/build-linux.yml + with: + artifact-name: common-depot + dfhack_ref: ${{ github.event.inputs && github.event.inputs.ref || github.event.ref }} + platform-files: false + docs: true + stonesense: true + secrets: inherit + + depot-win64: + name: Windows depot files + uses: ./.github/workflows/build-windows.yml + with: + artifact-name: win64-depot + dfhack_ref: ${{ github.event.inputs && github.event.inputs.ref || github.event.ref }} + cache-id: release + cache-readonly: true + common-files: false + stonesense: true + launchdf: true + secrets: inherit + + depot-linux64: + name: Linux depot files + uses: ./.github/workflows/build-linux.yml + with: + artifact-name: linux64-depot + dfhack_ref: ${{ github.event.inputs && github.event.inputs.ref || github.event.ref }} + cache-id: release + cache-readonly: true + common-files: false + stonesense: true + secrets: inherit + + deploy-to-steam: + name: Deploy to Steam + needs: + - depot-common + - depot-win64 + - depot-linux64 + runs-on: ubuntu-latest + concurrency: steam + steps: + - name: Download depot files + uses: actions/download-artifact@v3 + - name: Stage depot files + run: | + for name in common win64 linux64; do + cd ${name}-depot + tar xjf ${name}-depot.tar.bz2 + rm ${name}-depot.tar.bz2 + cd .. + done + - name: Steam deploy + uses: game-ci/steam-deploy@v3 + with: + username: ${{ secrets.STEAM_USERNAME }} + configVdf: ${{ secrets.STEAM_CONFIG_VDF}} + appId: 2346660 + buildDescription: ${{ github.event.inputs && github.event.inputs.version || github.ref_name }} + rootPath: . + depot1Path: common-depot + depot2Path: win64-depot + depot3Path: linux64-depot + releaseBranch: ${{ github.event.inputs && github.event.inputs.release_channel || 'staging' }} diff --git a/.github/workflows/steam.yml b/.github/workflows/steam.yml deleted file mode 100644 index e4178e1d0..000000000 --- a/.github/workflows/steam.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Deploy to Steam - -on: - workflow_dispatch: - inputs: - ref: - description: Branch or commit hash - type: string - required: true - default: develop - version: - description: Version or build description - type: string - required: true - release_channel: - description: Release channel - type: string - required: true - default: staging - -jobs: - deploy-to-steam: - name: Deploy to Steam - runs-on: ubuntu-22.04 - steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install ccache - - name: Clone DFHack - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 - ref: ${{ github.event.inputs.ref }} - - name: Get 3rd party SDKs - uses: actions/checkout@v3 - with: - repository: DFHack/3rdparty - ref: main - ssh-key: ${{ secrets.DFHACK_3RDPARTY_TOKEN }} - path: depends/steam - - name: Fetch ccache - uses: actions/cache@v3 - with: - path: build/win64-cross/ccache - key: ccache-win64-cross-msvc-${{ github.sha }} - restore-keys: | - ccache-win64-cross-msvc - - name: Cross-compile win64 artifacts - env: - CMAKE_EXTRA_ARGS: '-DBUILD_STONESENSE:BOOL=1 -DBUILD_DFLAUNCH:BOOL=1' - steam_username: ${{ secrets.STEAM_SDK_USERNAME }} - steam_password: ${{ secrets.STEAM_SDK_PASSWORD }} - run: | - echo "ref: ${{ github.event.inputs.ref }}" - echo "sha: ${{ github.sha }}" - echo "version: ${{ github.event.inputs.version }}" - echo "release_channel: ${{ github.event.inputs.release_channel }}" - echo - cd build - bash -x build-win64-from-linux.sh - ccache -d win64-cross/ccache --max-size 200M - ccache -d win64-cross/ccache --cleanup - ccache -d win64-cross/ccache --show-stats - - name: Steam deploy - uses: game-ci/steam-deploy@v2 - with: - username: ${{ secrets.STEAM_USERNAME }} - password: ${{ secrets.STEAM_PASSWORD }} - configVdf: ${{ secrets.STEAM_CONFIG_VDF}} - ssfnFileName: ${{ secrets.STEAM_SSFN_FILE_NAME }} - ssfnFileContents: ${{ secrets.STEAM_SSFN_FILE_CONTENTS }} - appId: 2346660 - buildDescription: ${{ github.event.inputs.version }} - rootPath: build - depot1Path: win64-cross/output - releaseBranch: ${{ github.event.inputs.release_channel }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..6255219e6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,146 @@ +name: Test + +on: + workflow_call: + inputs: + dfhack_ref: + type: string + scripts_ref: + type: string + structures_ref: + type: string + +jobs: + build-windows: + name: Windows MSVC + uses: ./.github/workflows/build-windows.yml + with: + dfhack_ref: ${{ inputs.dfhack_ref }} + scripts_ref: ${{ inputs.scripts_ref }} + structures_ref: ${{ inputs.structures_ref }} + artifact-name: test-msvc + cache-id: test + docs: true + html: false + tests: true + + build-linux: + name: Linux gcc-${{ matrix.gcc }} + uses: ./.github/workflows/build-linux.yml + with: + dfhack_ref: ${{ inputs.dfhack_ref }} + scripts_ref: ${{ inputs.scripts_ref }} + structures_ref: ${{ inputs.structures_ref }} + artifact-name: test-gcc-${{ matrix.gcc }} + cache-id: test + stonesense: ${{ matrix.plugins == 'all' }} + extras: ${{ matrix.plugins == 'all' }} + docs: true + html: false + tests: true + gcc-ver: ${{ matrix.gcc }} + secrets: inherit + strategy: + fail-fast: false + matrix: + include: + - gcc: 10 + plugins: "default" + - gcc: 12 + plugins: "all" + + run-tests: + name: Test (${{ matrix.os }}, ${{ matrix.compiler }}, ${{ matrix.plugins }} plugins, ${{ matrix.config }} config) + needs: + - build-windows + - build-linux + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + include: + - os: windows + compiler: msvc + plugins: "default" + config: "default" + - os: windows + compiler: msvc + plugins: "default" + config: "empty" + - os: ubuntu + compiler: gcc-10 + plugins: "default" + config: "default" + - os: ubuntu + compiler: gcc-12 + plugins: "all" + config: "default" + steps: + - name: Set env + shell: bash + run: echo "DF_FOLDER=DF" >> $GITHUB_ENV + - name: Install dependencies + if: matrix.os == 'ubuntu' + run: | + sudo apt-get update + sudo apt-get install \ + libsdl2-2.0-0 \ + libsdl2-image-2.0-0 + - name: Clone DFHack + uses: actions/checkout@v3 + with: + repository: ${{ inputs.dfhack_ref && github.repository || 'DFHack/dfhack' }} + ref: ${{ inputs.dfhack_ref }} + - name: Detect DF version + shell: bash + run: echo DF_VERSION="$(sh ci/get-df-version.sh)" >> $GITHUB_ENV + - name: Fetch DF cache + id: restore-df + uses: actions/cache/restore@v3 + with: + path: ${{ env.DF_FOLDER }} + key: df-${{ matrix.os }}-${{ env.DF_VERSION }}-${{ hashFiles('ci/download-df.sh') }} + - name: Download DF + if: steps.restore-df.outputs.cache-hit != 'true' + run: sh ci/download-df.sh ${{ env.DF_FOLDER }} ${{ matrix.os }} ${{ env.DF_VERSION }} + - name: Save DF cache + if: steps.restore-df.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + with: + path: ${{ env.DF_FOLDER }} + key: df-${{ matrix.os }}-${{ env.DF_VERSION }}-${{ hashFiles('ci/download-df.sh') }} + - name: Install blank DFHack init scripts + if: matrix.config == 'empty' + shell: bash + run: | + mkdir -p ${{ env.DF_FOLDER }}/dfhack-config/init + cd data/dfhack-config/init + for fname in *.init; do touch ../../../${{ env.DF_FOLDER }}/dfhack-config/init/$fname; done + - name: Download DFHack + uses: actions/download-artifact@v3 + with: + name: test-${{ matrix.compiler }} + - name: Install DFHack + shell: bash + run: tar xjf test-${{ matrix.compiler }}.tar.bz2 -C ${{ env.DF_FOLDER }} + - name: Start X server + if: matrix.os == 'ubuntu' + run: Xvfb :0 -screen 0 1600x1200x24 & + - name: Run lua tests + timeout-minutes: 10 + env: + DISPLAY: :0 + TERM: xterm-256color + run: python ci/run-tests.py --keep-status "${{ env.DF_FOLDER }}" + - name: Check RPC interface + run: python ci/check-rpc.py "${{ env.DF_FOLDER }}/dfhack-rpc.txt" + - name: Upload test artifacts + uses: actions/upload-artifact@v3 + if: always() + continue-on-error: true + with: + name: test-output-${{ matrix.compiler }}-${{ matrix.plugins }}_plugins-${{ matrix.config }}_config + path: | + ${{ env.DF_FOLDER }}/dfhack-rpc.txt + ${{ env.DF_FOLDER }}/test*.json + ${{ env.DF_FOLDER }}/*.log diff --git a/.gitignore b/.gitignore index a386b260a..5b4c7f6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # linux backup files *~ -#Kdevelop project files +# Kdevelop project files *.kdev4 .kdev4 @@ -70,7 +70,7 @@ tags # Mac OS X .DS_Store files .DS_Store -#VS is annoying about this one. +# VS is annoying about this one. /build/win64/DF_PATH.txt /build/win32/DF_PATH.txt /.vs @@ -81,5 +81,6 @@ tags # external plugins /plugins/CMakeLists.custom.txt -# steam api +# 3rd party downloads depends/steam +depends/SDL2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index efa59812d..45d51dc30 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: v4.4.0 + rev: v4.5.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.23.1 + rev: 0.27.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.1 + rev: v1.5.4 hooks: - id: forbid-tabs exclude_types: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1309daa4e..7f61bb111 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ cmake_policy(SET CMP0074 NEW) project(dfhack) # set up versioning. -set(DF_VERSION "50.08") -set(DFHACK_RELEASE "r4") +set(DF_VERSION "50.11") +set(DFHACK_RELEASE "r2") set(DFHACK_PRERELEASE FALSE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") @@ -30,14 +30,16 @@ else(CMAKE_CONFIGURATION_TYPES) endif(CMAKE_CONFIGURATION_TYPES) option(BUILD_DOCS "Choose whether to build the documentation (requires python and Sphinx)." OFF) +option(BUILD_DOCS_NO_HTML "Don't build the HTML docs, only the in-game docs." OFF) option(REMOVE_SYMBOLS_FROM_DF_STUBS "Remove debug symbols from DF stubs. (Reduces libdfhack size to about half but removes a few useful symbols)" ON) 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 "4.8") - message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 4.8 or later") - elseif(${GCC_VERSION_OUT} VERSION_GREATER "4.9.9") + if(${GCC_VERSION_OUT} VERSION_LESS "10") + message(SEND_ERROR "${compiler_path} version ${GCC_VERSION_OUT} cannot be used - use GCC 10 or later") + # TODO: this may need to be removed when DF linux actually comes out + # TODO: and we can test # GCC 5 changes ABI name mangling to enable C++11 changes. # This must be disabled to enable linking against DF. # http://developerblog.redhat.com/2015/02/05/gcc5-and-the-c11-abi/ @@ -60,14 +62,14 @@ endif() if(WIN32) if(NOT MSVC) - message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1935 is required.") - elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1935)) - message(SEND_ERROR "MSVC 2022 version 1930 to 1935 is required, Version Found: ${MSVC_VERSION}") + message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1937 is required.") + elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1937)) + message(SEND_ERROR "MSVC 2022 version 1930 to 1937 is required, Version Found: ${MSVC_VERSION}") endif() endif() -# Ask for C++11 standard from compilers -set(CMAKE_CXX_STANDARD 11) +# Ask for C++-20 standard from compilers +set(CMAKE_CXX_STANDARD 20) # Require the standard support from compilers. set(CMAKE_CXX_STANDARD_REQUIRED ON) # Use only standard c++ to keep code portable @@ -209,28 +211,23 @@ set(DFHACK_BINARY_DESTINATION .) set(DFHACK_PLUGIN_DESTINATION ${DFHACK_DATA_DESTINATION}/plugins) # dfhack lua files go here: set(DFHACK_LUA_DESTINATION ${DFHACK_DATA_DESTINATION}/lua) -# the windows .lib file goes here: -set(DFHACK_DEVLIB_DESTINATION ${DFHACK_DATA_DESTINATION}) # user documentation goes here: set(DFHACK_USERDOC_DESTINATION ${DFHACK_DATA_DESTINATION}) -# developer documentation goes here: -set(DFHACK_DEVDOC_DESTINATION ${DFHACK_DATA_DESTINATION}) # some options for the user/developer to play with option(BUILD_LIBRARY "Build the DFHack library." ON) option(BUILD_PLUGINS "Build the DFHack plugins." ON) +option(INSTALL_SCRIPTS "Install DFHack scripts." ON) +option(INSTALL_DATA_FILES "Install DFHack platform independent files." ON) set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) if(UNIX) ## flags for GCC # default to hidden symbols # ensure compatibility with older CPUs - # enable C++11 features add_definitions(-DLINUX_BUILD) - add_definitions(-D_GLIBCXX_USE_C99) - set(GCC_COMMON_FLAGS "-fvisibility=hidden -mtune=generic -Wall -Werror") - set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -g") + set(GCC_COMMON_FLAGS "-fvisibility=hidden -mtune=generic -Wall -Werror -Wl,--disable-new-dtags") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COMMON_FLAGS}") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${GCC_COMMON_FLAGS}") if(DFHACK_BUILD_64) @@ -292,22 +289,21 @@ endif() find_package(ZLIB REQUIRED) include_directories(${ZLIB_INCLUDE_DIRS}) -if(WIN32) - # Do the same for SDL.dll - # (DFHack doesn't require this at build time, so no need to move it to the build folder) - # TODO: remove SDL.dll from our distribution once DF moves to SDL2. we only - # continue to include it so we don't break Steam players on update by removing - # the SDL.dll that DF needs. - set(SDL_DOWNLOAD_DIR ${dfhack_SOURCE_DIR}/package/windows/win${DFHACK_BUILD_ARCH}) - if(${DFHACK_BUILD_ARCH} STREQUAL "64") - download_file("https://github.com/DFHack/dfhack-bin/releases/download/0.44.09/win64-SDL.dll" - ${SDL_DOWNLOAD_DIR}/SDL.dll - "1ae242c4b94cb03756a1288122a66faf") - else() - download_file("https://github.com/DFHack/dfhack-bin/releases/download/0.44.09/win32-SDL.dll" - ${SDL_DOWNLOAD_DIR}/SDL.dll - "5a09604daca6b2b5ce049d79af935d6a") - endif() +if(BUILD_LIBRARY) + # Download SDL release and extract into depends in the build dir + # all we need are the header files (including generated headers), so the same release package + # will work for all platforms + # (the above statement is untested for OSX) + set(SDL_VERSION 2.26.2) + set(SDL_ZIP_MD5 574daf26d48de753d0b1e19823c9d8bb) + set(SDL_ZIP_FILE SDL2-devel-${SDL_VERSION}-VC.zip) + set(SDL_ZIP_PATH ${dfhack_SOURCE_DIR}/depends/SDL2/) + download_file("https://github.com/libsdl-org/SDL/releases/download/release-${SDL_VERSION}/${SDL_ZIP_FILE}" + ${SDL_ZIP_PATH}${SDL_ZIP_FILE} + ${SDL_ZIP_MD5}) + file(ARCHIVE_EXTRACT INPUT ${SDL_ZIP_PATH}${SDL_ZIP_FILE} + DESTINATION ${SDL_ZIP_PATH}) + include_directories(${SDL_ZIP_PATH}/SDL2-${SDL_VERSION}/include) endif() if(APPLE) @@ -391,14 +387,17 @@ include_directories(depends/lodepng) include_directories(depends/tthread) include_directories(depends/clsocket/src) include_directories(depends/xlsxio/include) -add_subdirectory(depends) + +if(BUILD_LIBRARY) + add_subdirectory(depends) +endif() # Testing with CTest macro(dfhack_test name files) -if(UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated +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 SDL) + target_link_libraries(${name} dfhack gtest) add_test(NAME ${name} COMMAND ${name}) endif() endmacro() @@ -410,22 +409,25 @@ if(NOT GIT_FOUND) endif() # build the lib itself +add_subdirectory(library) if(BUILD_LIBRARY) - add_subdirectory(library) - install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) - install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) + file(WRITE ${CMAKE_BINARY_DIR}/dfhack_setarch.txt ${DFHACK_SETARCH}) + install(FILES ${CMAKE_BINARY_DIR}/dfhack_setarch.txt DESTINATION ${DFHACK_DATA_DESTINATION}) endif() -file(WRITE "${CMAKE_BINARY_DIR}/dfhack_setarch.txt" ${DFHACK_SETARCH}) -install(FILES "${CMAKE_BINARY_DIR}/dfhack_setarch.txt" DESTINATION "${DFHACK_DATA_DESTINATION}") - # build the plugins -if(BUILD_PLUGINS) - add_subdirectory(plugins) +add_subdirectory(plugins) + +if(INSTALL_DATA_FILES) + add_subdirectory(data) + install(FILES LICENSE.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) + install(FILES docs/changelog-placeholder.txt DESTINATION ${DFHACK_USERDOC_DESTINATION} RENAME changelog.txt) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/depends/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) endif() -add_subdirectory(data) -add_subdirectory(scripts) +if(INSTALL_SCRIPTS) + add_subdirectory(scripts) +endif() if(BUILD_DOCS) find_package(Python3) @@ -466,7 +468,14 @@ if(BUILD_DOCS) "${CMAKE_CURRENT_SOURCE_DIR}/conf.py" ) - set(SPHINX_OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/docs/html/.buildinfo") + if(BUILD_DOCS_NO_HTML) + set(SPHINX_OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/docs/text/index.txt") + set(SPHINX_BUILD_TARGETS text) + else() + set(SPHINX_OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/docs/html/.buildinfo") + set(SPHINX_BUILD_TARGETS html text) + endif() + set_property( DIRECTORY PROPERTY ADDITIONAL_CLEAN_FILES TRUE "${CMAKE_CURRENT_SOURCE_DIR}/docs/changelogs" @@ -483,9 +492,10 @@ if(BUILD_DOCS) "${CMAKE_BINARY_DIR}/docs/text" "${CMAKE_BINARY_DIR}/docs/xml" ) + add_custom_command(OUTPUT ${SPHINX_OUTPUT} COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/build.py" - html text --sphinx="${SPHINX_EXECUTABLE}" -- -q + ${SPHINX_BUILD_TARGETS} --sphinx="${SPHINX_EXECUTABLE}" -- -q -W DEPENDS ${SPHINX_DEPS} COMMENT "Building documentation with Sphinx" ) @@ -498,10 +508,12 @@ if(BUILD_DOCS) add_custom_command(TARGET dfhack_docs POST_BUILD COMMAND ${CMAKE_COMMAND} -E touch ${SPHINX_OUTPUT}) - install(DIRECTORY ${dfhack_SOURCE_DIR}/docs/html/ - DESTINATION ${DFHACK_USERDOC_DESTINATION}/docs - FILES_MATCHING PATTERN "*" - PATTERN html/_sources EXCLUDE) + if(NOT BUILD_DOCS_NO_HTML) + install(DIRECTORY ${dfhack_SOURCE_DIR}/docs/html/ + DESTINATION ${DFHACK_USERDOC_DESTINATION}/docs + FILES_MATCHING PATTERN "*" + PATTERN html/_sources EXCLUDE) + endif() install(DIRECTORY ${dfhack_SOURCE_DIR}/docs/text/ DESTINATION ${DFHACK_USERDOC_DESTINATION}/docs) install(FILES docs/changelogs/news.rst docs/changelogs/news-dev.rst DESTINATION ${DFHACK_USERDOC_DESTINATION}) @@ -586,7 +598,7 @@ endif() set(DFHACK_BUILD_ARCH_PREV "${DFHACK_BUILD_ARCH}" CACHE STRING "Previous build architecture" FORCE) option(BUILD_SIZECHECK "Build the sizecheck library, for research" OFF) -if(BUILD_SIZECHECK) +if(BUILD_LIBRARY AND BUILD_SIZECHECK) add_subdirectory(depends/sizecheck) add_dependencies(dfhack sizecheck) endif() diff --git a/build/build-win64-from-linux.sh b/build/build-win64-from-linux.sh index c0559a999..9404188fd 100755 --- a/build/build-win64-from-linux.sh +++ b/build/build-win64-from-linux.sh @@ -45,7 +45,7 @@ if ! docker run --rm -i -v "$srcdir":/src -v "$srcdir/build/win64-cross/":/src/b -e steam_password \ --name dfhack-win \ ghcr.io/dfhack/build-env:msvc \ - bash -c "cd /src/build && dfhack-configure windows 64 Release -DCMAKE_INSTALL_PREFIX=/src/build/output cmake .. -DBUILD_DOCS=1 $CMAKE_EXTRA_ARGS && dfhack-make -j$jobs install" \ + bash -c "cd /src/build && dfhack-configure windows 64 Release -DCMAKE_INSTALL_PREFIX=/src/build/output -DBUILD_DOCS=1 $CMAKE_EXTRA_ARGS && dfhack-make -j$jobs install" \ ; then echo echo "Build failed" diff --git a/ci/buildmaster-rebuild-pr.py b/ci/buildmaster-rebuild-pr.py deleted file mode 100755 index 7ce988683..000000000 --- a/ci/buildmaster-rebuild-pr.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import hashlib -import hmac -import json -import logging -import os -import uuid - -import requests - -logging.basicConfig(level=logging.DEBUG) - - -def get_required_env(name): - value = os.environ.get(name) - if not value: - raise ValueError(f'Expected environment variable {name!r} to be non-empty') - return value - - -def make_sig(body, secret, algorithm): - return hmac.new(secret.encode(), body.encode(), algorithm).hexdigest() - - -webhook_url = get_required_env('DFHACK_BUILDMASTER_WEBHOOK_URL') -secret = get_required_env('DFHACK_BUILDMASTER_WEBHOOK_SECRET') -api_token = get_required_env('GITHUB_TOKEN') -repo = get_required_env('GITHUB_REPO') - - -parser = argparse.ArgumentParser() -target_group = parser.add_mutually_exclusive_group(required=True) -target_group.add_argument('--pull-request', type=int, help='Pull request to rebuild') -args = parser.parse_args() - - -response = requests.get( - f'https://api.github.com/repos/{repo}/pulls/{args.pull_request}', - headers={ - 'Authorization': f'Bearer {api_token}', - }, -) -response.raise_for_status() -pull_request_data = response.json() - - -body = json.dumps({ - 'action': 'synchronize', - 'number': args.pull_request, - 'pull_request': pull_request_data, -}) - -response = requests.post( - 'https://lubar-webhook-proxy.appspot.com/github/buildmaster', - headers={ - 'Content-Type': 'application/json', - 'User-Agent': 'GitHub-Hookshot/' + requests.utils.default_user_agent(), - 'X-GitHub-Delivery': 'dfhack-rebuild-' + str(uuid.uuid4()), - 'X-GitHub-Event': 'pull_request', - 'X-Hub-Signature': 'sha1=' + make_sig(body, secret, hashlib.sha1), - 'X-Hub-Signature-256': 'sha256=' + make_sig(body, secret, hashlib.sha256), - }, - data=body, - timeout=15, -) - -print(response) -print(str(response.text)) -response.raise_for_status() diff --git a/ci/check-rpc.py b/ci/check-rpc.py index aba3e3811..be7d07986 100755 --- a/ci/check-rpc.py +++ b/ci/check-rpc.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 import glob +import itertools import sys actual = {'': {}} +SEP = ('=' * 80) with open(sys.argv[1]) as f: plugin_name = '' @@ -26,7 +28,7 @@ for p in glob.iglob('library/proto/*.proto'): parts = line.split(' ') expected[''][parts[2]] = (parts[4], parts[6]) -for p in glob.iglob('plugins/proto/*.proto'): +for p in itertools.chain(glob.iglob('plugins/proto/*.proto'), glob.iglob('plugins/*/proto/*.proto')): plugin_name = '' with open(p) as f: for line in f: @@ -53,6 +55,7 @@ for plugin_name in actual: methods = actual[plugin_name] if plugin_name not in expected: + print(SEP) print('Missing documentation for plugin proto files: ' + plugin_name) print('Add the following lines:') print('// Plugin: ' + plugin_name) @@ -73,12 +76,14 @@ for plugin_name in actual: missing.append('// RPC ' + m + ' : ' + io[0] + ' -> ' + io[1]) if len(missing) > 0: + print(SEP) print('Incomplete documentation for ' + ('core' if plugin_name == '' else 'plugin "' + plugin_name + '"') + ' proto files. Add the following lines:') for m in missing: print(m) error_count += 1 if len(wrong) > 0: + print(SEP) print('Incorrect documentation for ' + ('core' if plugin_name == '' else 'plugin "' + plugin_name + '"') + ' proto files. Replace the following comments:') for m in wrong: print(m) @@ -88,6 +93,7 @@ for plugin_name in expected: methods = expected[plugin_name] if plugin_name not in actual: + print(SEP) print('Incorrect documentation for plugin proto files: ' + plugin_name) print('The following methods are documented, but the plugin does not provide any RPC methods:') for m in methods: @@ -102,6 +108,7 @@ for plugin_name in expected: missing.append('// RPC ' + m + ' : ' + io[0] + ' -> ' + io[1]) if len(missing) > 0: + print(SEP) print('Incorrect documentation for ' + ('core' if plugin_name == '' else 'plugin "' + plugin_name + '"') + ' proto files. Remove the following lines:') for m in missing: print(m) diff --git a/ci/download-df.sh b/ci/download-df.sh index 8d94d54dd..399b75714 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -1,52 +1,53 @@ #!/bin/sh +DF_FOLDER=$1 +OS_TARGET=$2 +DF_VERSION=$3 + set -e -df_tardest="df.tar.bz2" -save_tardest="test_save.tgz" - -cd "$(dirname "$0")" -echo "DF_VERSION: $DF_VERSION" -echo "DF_FOLDER: $DF_FOLDER" -mkdir -p "$DF_FOLDER" -# back out of df_linux -cd "$DF_FOLDER/.." - -if ! test -f "$df_tardest"; then - minor=$(echo "$DF_VERSION" | cut -d. -f2) - patch=$(echo "$DF_VERSION" | cut -d. -f3) - echo "Downloading DF $DF_VERSION" - while read url; do - echo "Attempting download: ${url}" - if wget -v "$url" -O "$df_tardest"; then - break - fi - done <] [] If a done_command is specified, it will be run after the tests complete. -Options: - - -h, --help display this help message and exit. - -d, --test_dir specifies which directory to look in for tests. defaults to - the "hack/scripts/test" folder in your DF installation. - -m, --modes only run tests in the given comma separated list of modes. - see the next section for a list of valid modes. if not - specified, the tests are not filtered by modes. - -r, --resume skip tests that have already been run. remove the - test_status.json file to reset the record. - -s, --save_dir the save folder to load for "fortress" mode tests. this - save is only loaded if a fort is not already loaded when - a "fortress" mode test is run. if not specified, defaults to - 'region1'. - -t, --tests only run tests that match one of the comma separated list of - patterns. if not specified, no tests are filtered. - -Modes: - - none the test can be run on any screen - title the test must be run on the DF title screen. note that if the game - has a map loaded, "title" mode tests cannot be run - fortress the test must be run while a map is loaded. if the game is - currently on the title screen, the save specified by the save_dir - parameter will be loaded. - -Examples: - - test runs all tests - test -r runs all tests that haven't been run before - test -m none runs tests that don't need the game to be in a - specific mode - test -t quickfort runs quickfort tests - test -d /path/to/dfhack-scripts/repo/test - runs tests in your dev scripts repo +Options +------- + +-d, --test_dir specifies which directory to look in for tests. defaults to + the "hack/scripts/test" folder in your DF installation. +-m, --modes only run tests in the given comma separated list of modes. + see the next section for a list of valid modes. if not + specified, the tests are not filtered by modes. +-r, --resume skip tests that have already been run. remove the + test_status.json file to reset the record. +-s, --save_dir the save folder to load for "fortress" mode tests. this + save is only loaded if a fort is not already loaded when + a "fortress" mode test is run. if not specified, defaults to + 'region1'. +-t, --tests only run tests that match one of the comma separated list of + patterns. if not specified, no tests are filtered and all tessts + are run. + +Modes +----- + +none the test can be run on any screen +title the test must be run on the DF title screen. note that if the game + has a map loaded, "title" mode tests cannot be run +fortress the test must be run while a map is loaded. if the game is + currently on the title screen, the save specified by the save_dir + parameter will be loaded. + +Examples +-------- + +test runs all tests +test -r runs all tests that haven't been run before +test -m none runs tests that don't need the game to be in a + specific mode +test -t quickfort runs quickfort tests +test -d /path/to/dfhack-scripts/repo/test + runs tests in your dev scripts repo Default values for the options may be set in a file named test_config.json in your DF folder. Options with comma-separated values should be written as json @@ -140,18 +152,37 @@ end test_envvars.require = clean_require test_envvars.reqscript = clean_reqscript -local function is_title_screen(scr) - scr = scr or dfhack.gui.getCurViewscreen() - return df.viewscreen_titlest:is_instance(scr) +local function is_title_screen() + return dfhack.gui.matchFocusString('title/Default') +end + +local function wait_for(ms, desc, predicate) + local start_ms = dfhack.getTickCount() + local prev_ms = start_ms + while not predicate() do + delay(10) + local now_ms = dfhack.getTickCount() + if now_ms - start_ms > ms then + qerror(('%s took too long (timed out at %s)'):format( + desc, dfhack.gui.getCurFocus(true)[1])) + end + if now_ms - prev_ms > 1000 then + print(('Waiting for %s...'):format(desc)) + prev_ms = now_ms + end + end end -- This only handles pre-fortress-load screens. It will time out if the player -- has already loaded a fortress or is in any screen that can't get to the title -- screen by sending ESC keys. local function ensure_title_screen() + if df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getDFViewscreen(true)) then + qerror('Cannot reach title screen from loaded fort') + end for i = 1, 100 do local scr = dfhack.gui.getCurViewscreen() - if is_title_screen(scr) then + if is_title_screen() then print('Found title screen') return end @@ -160,57 +191,94 @@ local function ensure_title_screen() if i % 10 == 0 then print('Looking for title screen...') end end qerror(string.format('Could not find title screen (timed out at %s)', - dfhack.gui.getCurFocus(true))) + dfhack.gui.getCurFocus(true)[1])) end -local function is_fortress(focus_string) - focus_string = focus_string or dfhack.gui.getCurFocus(true) - return focus_string == 'dwarfmode/Default' +local function is_fortress() + return dfhack.gui.matchFocusString('dwarfmode/Default') +end + +-- error out if we're not running in a CI environment +-- the tests may corrupt saves, and we don't want to unexpectedly ruin a real player save +-- this heuristic is not perfect, but it should be able to detect most cases +local function ensure_ci_save(scr) + if #scr.savegame_header ~= 1 + or #scr.savegame_header_world ~= 1 + or not string.find(scr.savegame_header[0].fort_name, 'Dream') + then + qerror('Unexpected test save in slot 0; please manually load a fort for ' .. + 'running fortress mode tests. note that tests may alter or corrupt the ' .. + 'fort! Do not save after running tests.') + end +end + +local function click_top_title_button(scr) + local sw, sh = dfhack.screen.getWindowSize() + df.global.gps.mouse_x = sw // 2 + df.global.gps.precise_mouse_x = df.global.gps.mouse_x * df.global.gps.tile_pixel_x + if sh < 60 then + df.global.gps.mouse_y = 25 + else + df.global.gps.mouse_y = (sh // 2) + 3 + end + df.global.gps.precise_mouse_y = df.global.gps.mouse_y * df.global.gps.tile_pixel_y + gui.simulateInput(scr, '_MOUSE_L') +end + +local function load_first_save(scr) + if #scr.savegame_header == 0 then + qerror('no savegames available to load') + end + + click_top_title_button(scr) + wait_for(1000, 'world list', function() + return scr.mode == 2 + end) + click_top_title_button(scr) + wait_for(1000, 'savegame list', function() + return scr.mode == 3 + end) + click_top_title_button(scr) + wait_for(1000, 'loadgame progress bar', function() + return dfhack.gui.matchFocusString('loadgame') + end) end -- Requires that a fortress game is already loaded or is ready to be loaded via --- the "Continue Playing" option in the title screen. Otherwise the function +-- the "Continue active game" option in the title screen. Otherwise the function -- will time out and/or exit with error. local function ensure_fortress(config) - local focus_string = dfhack.gui.getCurFocus(true) for screen_timeout = 1,10 do - if is_fortress(focus_string) then - print('Loaded fortress map') + if is_fortress() then + print('Fortress map is loaded') -- pause the game (if it's not already paused) dfhack.gui.resetDwarfmodeView(true) return end - local scr = dfhack.gui.getCurViewscreen(true) - if focus_string == 'title' or - focus_string == 'dfhack/lua/load_screen' then + local scr = dfhack.gui.getCurViewscreen() + if dfhack.gui.matchFocusString('title/Default', scr) then + print('Attempting to load the test fortress') + -- TODO: reinstate loading of a specified save dir; for now + -- just load the first possible save, which will at least let us + -- run fortress tests in CI -- qerror()'s on falure - dfhack.run_script('load-save', config.save_dir) - elseif focus_string ~= 'loadgame' then + -- dfhack.run_script('load-save', config.save_dir) + ensure_ci_save(scr) + load_first_save(scr) + elseif not dfhack.gui.matchFocusString('loadgame', scr) then -- if we're not actively loading a game, hope we're in -- a screen where hitting ESC will get us to the game map -- or the title screen scr:feed_key(df.interface_key.LEAVESCREEN) end -- wait for current screen to change - local prev_focus_string = focus_string - for frame_timeout = 1,100 do - delay(10) - focus_string = dfhack.gui.getCurFocus(true) - if focus_string ~= prev_focus_string then - goto next_screen - end - if frame_timeout % 10 == 0 then - print(string.format( - 'Loading fortress (currently at screen: %s)', - focus_string)) - end - end - print('Timed out waiting for screen to change') - break - ::next_screen:: + local prev_focus_string = dfhack.gui.getCurFocus()[1] + wait_for(60000, 'screen change', function() + return dfhack.gui.getCurFocus()[1] ~= prev_focus_string + end) end qerror(string.format('Could not load fortress (timed out at %s)', - focus_string)) + table.concat(dfhack.gui.getCurFocus(), ' '))) end local MODES = { @@ -236,6 +304,25 @@ local function load_test_config(config_file) return config end +local function TestTable() + local inner = utils.OrderedTable() + local meta = copyall(getmetatable(inner)) + + function meta:__newindex(k, v) + if inner[k] then + error('Attempt to overwrite existing test: ' .. k) + elseif type(v) ~= 'function' then + error('Attempt to define test as non-function: ' .. k .. ' = ' .. tostring(v)) + else + inner[k] = v + end + end + + local self = {} + setmetatable(self, meta) + return self +end + -- we have to save and use the original dfhack.printerr here so our test harness -- output doesn't trigger its own dfhack.printerr usage detection (see -- detect_printerr below) @@ -280,7 +367,7 @@ end local function build_test_env(path) local env = { - test = utils.OrderedTable(), + test = TestTable(), -- config values can be overridden in the test file to define -- requirements for the tests in that file config = { @@ -352,33 +439,46 @@ local function load_tests(file, tests) if not code then dfhack.printerr('Failed to load file: ' .. tostring(err)) return false - else - dfhack.internal.IN_TEST = true - local ok, err = dfhack.pcall(code) - dfhack.internal.IN_TEST = false - if not ok then - dfhack.printerr('Error when running file: ' .. tostring(err)) - return false - else - if not MODES[env.config.mode] then - dfhack.printerr('Invalid config.mode: ' .. tostring(env.config.mode)) - return false - end - for name, test_func in pairs(env.test) do - if env.config.wrapper then - local fn = test_func - test_func = function() env.config.wrapper(fn) end - end - local test_data = { - full_name = short_filename .. ':' .. name, - func = test_func, - private = env_private, - config = env.config, - } - test_data.name = test_data.full_name:gsub('test/', ''):gsub('.lua', '') - table.insert(tests, test_data) - end + end + dfhack.internal.IN_TEST = true + local ok, err = dfhack.pcall(code) + dfhack.internal.IN_TEST = false + if not ok then + dfhack.printerr('Error when running file: ' .. tostring(err)) + return false + end + if not MODES[env.config.mode] then + dfhack.printerr('Invalid config.mode: ' .. tostring(env.config.mode)) + return false + end + if not env.config.target then + dfhack.printerr('Skipping tests for unspecified target in ' .. file) + return true -- TODO: change to false once existing tests have targets specified + end + local targets = type(env.config.target) == 'table' and env.config.target or {env.config.target} + for _,target in ipairs(targets) do + if target == 'core' then goto continue end + if type(target) ~= 'string' or not helpdb.is_entry(target) or + helpdb.get_entry_tags(target).unavailable + then + dfhack.printerr('Skipping tests for unavailable target: ' .. target) + return true + end + ::continue:: + end + for name, test_func in pairs(env.test) do + if env.config.wrapper then + local fn = test_func + test_func = function() env.config.wrapper(fn) end end + local test_data = { + full_name = short_filename .. ':' .. name, + func = test_func, + private = env_private, + config = env.config, + } + test_data.name = test_data.full_name:gsub('test/', ''):gsub('.lua', '') + table.insert(tests, test_data) end return true end @@ -520,6 +620,10 @@ local function filter_tests(tests, config) end local function run_tests(tests, status, counts, config) + wait_for(60000, 'game load', function() + local scr = dfhack.gui.getDFViewscreen() + return not df.viewscreen_initial_prepst:is_instance(scr) + end) print(('Running %d tests'):format(#tests)) local start_ms = dfhack.getTickCount() local num_skipped = 0 @@ -529,6 +633,7 @@ local function run_tests(tests, status, counts, config) goto skip end if not MODES[test.config.mode].detect() then + print(('Switching to %s mode for test: %s'):format(test.config.mode, test.name)) local ok, err = pcall(MODES[test.config.mode].navigate, config) if not ok then MODES[test.config.mode].failed = true @@ -537,12 +642,13 @@ local function run_tests(tests, status, counts, config) goto skip end end + -- pre-emptively mark the test as failed in case we induce a crash + status[test.full_name] = TestStatus.FAILED + save_test_status(status) if run_test(test, status, counts) then status[test.full_name] = TestStatus.PASSED - else - status[test.full_name] = TestStatus.FAILED + save_test_status(status) end - save_test_status(status) ::skip:: end local elapsed_ms = dfhack.getTickCount() - start_ms @@ -575,7 +681,7 @@ local function dump_df_state() enabler = { fps = df.global.enabler.fps, gfps = df.global.enabler.gfps, - fullscreen = df.global.enabler.fullscreen, + fullscreen_state = df.global.enabler.fullscreen_state.whole, }, gps = { dimx = df.global.gps.dimx, diff --git a/ci/update-submodules.manifest b/ci/update-submodules.manifest index e97cae6f3..0c8f03e81 100644 --- a/ci/update-submodules.manifest +++ b/ci/update-submodules.manifest @@ -2,6 +2,7 @@ library/xml master scripts master plugins/stonesense master plugins/isoworld dfhack +depends/clsocket master depends/libzip dfhack depends/libexpat dfhack depends/xlsxio dfhack diff --git a/conf.py b/conf.py index 60c3be579..ffcc24b75 100644 --- a/conf.py +++ b/conf.py @@ -78,8 +78,7 @@ def write_tool_docs(): os.makedirs(os.path.join('docs/tools', os.path.dirname(k[0])), mode=0o755, exist_ok=True) with write_file_if_changed('docs/tools/{}.rst'.format(k[0])) as outfile: - if k[0] != 'search': - outfile.write(label) + outfile.write(label) outfile.write(include) diff --git a/data/art/dfhack.png b/data/art/logo.png similarity index 100% rename from data/art/dfhack.png rename to data/art/logo.png diff --git a/data/art/logo_hovered.png b/data/art/logo_hovered.png new file mode 100644 index 000000000..fe82cc1ae Binary files /dev/null and b/data/art/logo_hovered.png differ diff --git a/data/art/pathable.png b/data/art/pathable.png new file mode 100644 index 000000000..00f7f831d Binary files /dev/null and b/data/art/pathable.png differ diff --git a/data/art/unsuspend.png b/data/art/unsuspend.png new file mode 100644 index 000000000..f1d1d33da Binary files /dev/null and b/data/art/unsuspend.png differ diff --git a/data/blueprints/dreamfort.csv b/data/blueprints/dreamfort.csv index 93cb4e3f7..22b78230a 100644 --- a/data/blueprints/dreamfort.csv +++ b/data/blueprints/dreamfort.csv @@ -20,7 +20,7 @@ "Dreamfort blueprints take care of everything to get the fort up and running. You don't need to clear any extra trees or create any extra buildings or stockpiles (though of course you are free to do so). Blueprints that do require manual steps, like 'assign minecart to hauling route', will leave a message telling you so when you run them. Note that blueprints will designate buildings to build even if you don't have the materials needed to build them. You can use the ""o"" hotkey to automatically create the manager orders for all the needed items when you have a blueprint loaded in gui/quickfort. Make sure your manager is available to validate all the incoming work orders!" "" "There are some tasks common to all forts that Dreamfort doesn't specifically handle for you. For example, Dreamfort sets up a barracks, but managing squads is up to you. Here are some other common tasks that may need to be done manually (or with some other tool):" -- Exploratory mining for specific resources like iron +- Exploratory mining for specific resources like iron (see gui/design for help with this) "- Filling the well system with water (if you have a light aquifer, see library/aquifer_tap.csv for help with this)" - Bringing magma up to the industry level to power magma forges/furnaces (see library/pump_stack.csv for help with this) - Manufacturing trade goods @@ -42,7 +42,7 @@ interactively." "# The dreamfort.csv distributed with DFHack is generated with the following command: for fname in dreamfort*.xlsx; do xlsx2csv -a -p '' ""$fname""; done | sed 's/,*$//'" #notes label(checklist) command checklist -"Here is the recommended order for Dreamfort commands. Each line is a blueprint that you run with gui/quickfort (default keybinding: Ctrl-Shift-Q), except where we use other tools as noted. If you set ""dreamfort"" as the filter when you open gui/quickfort, you'll conveniently only see Dreamfort blueprints to choose from. See the walkthroughs (the ""help"" blueprints) for context and details." +"Here is the recommended order for Dreamfort commands. Each line is a blueprint that you run with gui/quickfort (default keybinding: Ctrl-Shift-Q), except where we use other tools as noted. If you set ""dreamfort"" as the filter when you open gui/quickfort, you'll conveniently only see Dreamfort blueprints to choose from. See the walkthroughs (the ""help"" blueprints) for context and details. You can also copy text from this spreadsheet online and paste it in gui/launcher with Ctrl-V." "If the checklist indicates that you should generate orders, that means to hit the ""o"" hotkey when the blueprint is loaded in gui/quickfort. You'll get a popup saying which orders were generated. Also remember to read the messages the blueprints display after you run them so you don't miss any important manual steps!" "" -- Preparation (before you embark!) -- @@ -61,7 +61,7 @@ gui/quickfort,/perimeter,,Run at embark. Don't actually apply the blueprint -- i -- Dig -- DFHack command,Blueprint,Generate orders,Notes gui/quickfort,/surface1,,Clear some trees and dig central staircase. Run when you find your center tile. Deconstruct your wagon if it is in the way. -gui/quickfort,/dig_all,,"Run when you find a suitable (non-aquifer) rock layer for the industry level. It designates digging for industry, services, guildhall, suites, apartments, and the crypt all in one go. This list does not include the farming level, which we'll dig in the uppermost soil layer a bit later. Note that it is more efficient for your miners if you designate your digging before they dig the central stairs past that level since the stairs are dug at a low priority. This keeps your miners focused on one level at a time. If you need to designate your levels individually due to caverns interrupting the sequence or just because it is your preference, run the level-specific dig blueprints (i.e. /industry1, /services1, /guildhall1, /suites1, 3 levels of /apartments1, and /crypt1) instead of running /dig_all." +gui/quickfort,/dig_all,,"Run when you find a suitable (non-aquifer) rock layer for the industry level. It designates digging for industry, services, guildhall, suites, apartments, and the crypt all in one go. This list does not include the farming level, which we'll designate in the uppermost soil layer once the surface miasma channels are dug. Note that it is more efficient for your miners if you designate the digging for a level before they dig the central stairs past that level. The stairs down on each level are designated at priority 5 instead of the regular priority 4. This lets the miners focus on one z-level at a time and not run up and down the stairs attempting to dig out two blueprints simultaneously. If you need to designate your levels individually due to caverns interrupting the sequence or just because it is your preference, run the level-specific dig blueprints (i.e. /industry1, /services1, /guildhall1, /suites1, 3 levels of /apartments1, and /crypt1) instead of running /dig_all." "" -- Core fort (should finish at about the third migration wave) -- DFHack command,Blueprint,Generate orders,Notes @@ -70,7 +70,7 @@ gui/quickfort,/farming1,,Dig out the farming level. Run when channels on the sur gui/quickfort,/farming2,,Build farming level. Run as soon as the farming level has been completely dug out. gui/quickfort,/surface3,,Cover the miasma vents and start protecting the central staircase from early invasions. Run right after /farming2. gui/quickfort,/industry2,,Build industry level. Run as soon as the industry level has been completely dug out. -prioritize ConstructBuilding,,,"To get those workshops up and running ASAP. You may have to run this several times as the materials for the building construction jobs become ready. As industry workshops are built, you can remove the temporary workshops and stockpiles on the surface. **Be sure that there are no items attached to jobs left in the workshops before deconstructing them, otherwise you'll get canceled jobs!**" +prioritize ConstructBuilding,,,"To get those workshops up and running ASAP. You may have to run this several times as the materials for the building construction jobs become ready. As industry workshops are built, you can remove the temporary workshops and stockpiles on the surface. Be sure that there are no items attached to jobs left in the surface workshops before deconstructing them, otherwise you'll get canceled jobs!" gui/quickfort,/surface4,,Finish protecting the staircase and lay flooring for future buildings. Run after the walls and floors around the staircase are built and you have moved production from the surface to the industry level. gui/quickfort,/industry3,,Build the rest of the industry level. Run once /surface4 is mostly complete. orders import library/basic,,,"Run after the first migration wave, so you have dwarves to do all the basic tasks. Note that this is the ""orders"" plugin, not the ""quickfort orders"" command." @@ -81,7 +81,7 @@ gui/quickfort,/surface6,Yes,Build security perimeter. Run once you have linked a gui/quickfort,/surface7,Yes,Build roof. Run after the surface walls are completed and any marked trees are chopped down. Be sure to give your haulers some time to fill the stonecutter's stockpile with some stone first so your stonecutters won't be hauling it up from the depths by hand. "" -- Plumbing -- -"If you haven't done it already, this is a good time to fill your well cisterns, either with a bucket brigade or by routing water from a freshwater stream or an aquifer (see the library/aquifer_tap.csv blueprint for help with this)." +"If you haven't done it already, this is a good time to fill your well cisterns, either with a bucket brigade or by routing water from a freshwater stream or an aquifer (see the aquifer_tap library blueprint for help with this)." Also consider bringing magma up to your services level so you can replace the forge and furnaces on your industry level with more powerful magma versions. This is especially important if your embark has insufficient trees to convert into charcoal. Keep in mind that moving magma is a tricky process and can take a long time. Don't forget to continue making progress through this checklist! "" -- Mature fort (fourth migration wave onward) -- @@ -112,7 +112,7 @@ You can save some time by setting up your settings in gui/control-panel before y "" "On the gui/control-panel ""Autostart"" tab, additionally enable:" - autobutcher -- autobutcher target 50 50 14 2 BIRD_GOOSE +- autobutcher target 10 10 14 2 BIRD_GOOSE - autochop - autofarm - autofish @@ -121,6 +121,7 @@ You can save some time by setting up your settings in gui/control-panel before y - ban-cooking all - buildingplan set boulders false - buildingplan set logs false +- dwarfvet - nestboxes - prioritize - seedwatch @@ -144,9 +145,6 @@ On the work details screen (Labor -> Work details) In standing orders (Labor -> Standing orders): " - Change ""Automatically weave all thread"" to ""No automatic weaving"" so the hospital always has thread -- we'll be managing cloth production with automated orders" "- On the ""Other"" tab, change ""Everybody harvests"" to ""Only farmers harvest""" -"" -"Finally, in the burrows screen:" -"- Create a burrow named ""Inside"" and register it as a civilian alert burrow in gui/civ-alert so you can use it to get your civilians to safety during sieges. You will have to periodically expand the burrow area as the fort expands." "#meta label(dig_all) start(central stairs on industry level) dig industry, services, guildhall, suites, apartments, and crypt levels. does not include farming." # Note that this blueprint will only work for the unified dreamfort.csv. It won't work for the individual .xlsx files (since #meta blueprints can't cross file boundaries). "" @@ -248,6 +246,8 @@ Features: - Optional extended trap hallways (to handle larger sieges with a smaller/no military) "- Protected trade depot, with separate trade goods stockpiles for organics and inorganics (for easy elven trading)" - A grid of small farm plots for lucrative surface farming +"- A burrow named ""Inside+"" that grows with your fort as you dig it out. It is pre-registered as a civilian alert burrow so you can use it to get your civilians to safety during sieges." +"- A burrow named ""Clearcutting area"" that is automatically registered with autochop (if you have it enabled) to keep the area around your fort clear of trees" "" Manual steps you have to take: "- Assign grazing livestock to the large pasture, dogs to the pasture over the central stairs, and male birds to the zone between the rows of nestboxes (DFHack's autonestbox will auto-assign the female egg-laying birds to the nestbox zones)" @@ -280,11 +280,11 @@ Surface Walkthrough: Sieges and Prisoner Processing: Here are some tips and procedures for handling seiges -- including how to clean up afterwards! "" -"- Ensure your ""Inside"" burrow includes only below-ground areas and safe surface areas of your fort. In particular, don't 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 of your fort. In particular, it should 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)." +"- 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, 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." +"- 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!" "" "- 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." "" @@ -292,13 +292,13 @@ Here are some tips and procedures for handling seiges -- including how to clean "" "After a siege, you can use the caged prisoners to safely train your military. Here's how:" "" -"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile, run ""stripcaged all"" in the DFHack gui/launcher." +"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile in the barracks, run ""stripcaged all"" in DFHack's gui/launcher." "" "- After all the prisoners' items have been confiscated, bring your military dwarves to the barracks (if they aren't already there)." "" -- Assign a group prisoners to the pasture that overlaps the prisoner quantum stockpile +- Assign a group of prisoners to the pasture that overlaps the prisoner quantum stockpile "" -"- Hauler dwarves will come and release prisoners one by one. Your military dwarves will immediately pounce on the released prisoner and chop them to bits, saving the hauler dwarves from being attacked. Repeat until all prisoners have been ""processed""." +"- Hauler dwarves will come and release prisoners one by one. Your military dwarves will immediately pounce on the released prisoners and chop them to bits, saving the hauler dwarves from being attacked. Repeat until all prisoners have been ""processed"". Some prisoners are not directly hostile (like cavern-caught gorlaks) and you may need to be target them explicitly to get your soldiers to attack them." #dig label(central_stairs_odd) start(2;2) hidden() carved spiral stairs odd levels `,j6,` u,`,u @@ -330,6 +330,7 @@ message(Once the central stairs are mined out deeply enough, you should start di If your wagon is within the fort perimeter, deconstruct it to get it out of the way. Once the marked trees are all chopped down (if any), continue with /surface2.) clear trees and set up pastures" clear_small/surface_clear_small +burrow_start/surface_burrow_start zones/surface_zones #> central_stairs/central_stairs repeat(down 10) @@ -367,7 +368,8 @@ traps/surface_traps clear_large/surface_clear_large "" "#meta label(surface7) start(central stairs (on ground level)) message(Remember to enqueue manager orders for this blueprint. -For extra security, you can run /surface8 at any time to extend the trap corridors.) build roof" +For extra security, you can run /surface8 at any time to extend the trap corridors.) expand Inside+ burrow to safe surface areas and build roof" +burrows/surface_burrows #< roof/surface_roof roof2/surface_roof2 @@ -412,7 +414,41 @@ corridor_traps/surface_corridor_traps -"#zone label(surface_zones) start(19; 19) hidden() message(Remember to assign your dogs to the pasture surrounding the central stairs, your grazing animals to the large pasture, and your male birds to the zone between the rows of nestboxes.) pastures and training areas" +#burrow label(surface_burrow_start) start(19; 19) hidden() create safety burrow that will grow with your fort + + + +,,,`,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,,`,`,`,`,`,`,,`,,`,`,`,`,`,,`,`,`,,`,`,,` +,,,`,,`,,`,,,,,,,,`,a{name=Inside+ create=true civalert=true}(5x5),,,,,,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,`,,~,,~,,`,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,`,,,,,,`,,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,`,`,,`,`,`,`,,`,,`,`,`,`,,,,,,,,`,,` +,,,`,,`,,,,,,,,,`,,,,,,,,`,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,`,,,,,,,,`,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,` +,,,`,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,` +,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,` + + + +"#zone label(surface_zones) start(19; 19) hidden() message(Remember to assign your dogs to the pasture surrounding the central stairs, your grazing animals to the large pasture, and your male birds to the zone between the rows of nestboxes. If your wagon is far away, you can let your animals wander closer to the fort before pasturing them to save hauling time.) pastures and training areas" @@ -783,7 +819,7 @@ corridor_traps/surface_corridor_traps ,,,`,,`,Cf,Cf,,,,,Cf,,`,,,~,~,~,,,`,Cf,,Cf,,,Cf,,Cf,`,,` ,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,` ,,,`,Cf,Cf,,,,,,,,Cf,Cf,,,,~,,,,Cf,Cf,,,,,,,,Cf,Cf,` -,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,` +,,,`,`,`,`,`,`,`,`,`,`,`,`,Cf,Cf,Cf,Cf,Cf,Cf,Cf,`,`,`,`,`,`,`,`,`,`,`,` @@ -834,7 +870,7 @@ You might also want to set the ""trade goods quantum"" stockpile to autotrade.) ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` -,,,`,,`,,,,,,,,,,,,,,,,,"a{name=""Pets/Prisoner feeder"" autotrain=true}(9x5)",,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,"a10{name=""Pets/Prisoner feeder"" autotrain=true}(9x5)",,,,,,,,,`,,` ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` ,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` @@ -857,7 +893,6 @@ You might also want to set the ""trade goods quantum"" stockpile to autotrade.) -#aliases "#build label(surface_build) start(19; 19) hidden() message(Use autofarm to manage farm crop selection. Remember to connect the levers to the gates once they are built.) gates, barracks, farm area, and trade area" @@ -1029,6 +1064,40 @@ t1(37x33) ,,,,,,,,,,,,,Tc,Tc,,,,,,,,Tc,Tc ,,,,,,,,,,,,,Tc,Tc,,,,,,,,Tc,Tc +#burrow label(surface_burrows) start(19; 19) hidden() extend safety burrow to newly safe surface areas and set up surrounding clearcutting area + + +,,"a{name=""Clearcutting area"" create=true autochop_clear=true}(-10x-10)","a{name=""Clearcutting area"" create=true autochop_clear=true}(32x-10)",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"a{name=""Clearcutting area"" create=true autochop_clear=true}(10x-10)" +,,"a{name=""Clearcutting area"" create=true autochop_clear=true}(-10x27)",`,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,`,"a{name=""Clearcutting area"" create=true autochop_clear=true}(10x27)" +,,,`,,`,a{name=Inside+}(25x17),,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,,`,`,`,`,`,`,,`,,`,`,`,`,`,,`,`,`,,`,`,,` +,,,`,,`,,`,,,,,,,,`,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,`,,~,,~,,`,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,`,,,,,,,,`,,,,,,`,,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,`,`,a{name=Inside+},`,`,`,`,,`,,`,`,`,`,a{name=Inside+}(7x6),,,,,,,`,,` +,,,`,,`,a{name=Inside+}(7x5),,,,,,,,`,,,,,,,,`,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,,,,,,,,,,,,,,,,,,`,,` +,,,`,,`,,,,,,,,,`,,,,,,,,`,,,,,,,,,`,,` +,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,` +,,,`,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,` +,,,`,`,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,` +,,"a{name=""Clearcutting area"" create=true autochop_clear=true}(-10x10)","a{name=""Clearcutting area"" create=true autochop_clear=true}(32x10)",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"a{name=""Clearcutting area"" create=true autochop_clear=true}(10x10)" + + #build label(surface_roof) start(19; 19) hidden() roof hatch and adjacent tiles @@ -1431,7 +1500,7 @@ doors/farming_doors ,,c,c,c,c,c,c,c,c,c,,,,`,,`,,,,c,c,c,c,c,c,c,c,c ,,c,c,c,c,c,c,c,c,,,c,`,`,`,`,`,c,,,c,c,c,c,c,c,c,c ,,c,c,c,c,c,c,c,c,,"c{name=""Unprepared fish"" take_from=""Starting food""}:+unpreparedfish",c,,,`,,,"c{name=""Rawhides"" take_from=""Starting cloth/trash""}:+rawhides",c,,c,c,c,c,c,c,c,c -,,c,c,c,c,c,c,c,,,c,c,,"c{name=""Refuse feeder"" give_to=""Rawhides"" take_from=""Starting cloth/trash""}:+cat_refuse/type(1x3)","y{name=""Corpse feeder"" take_from=""Starting cloth/trash""}:+cat_refuse/corpses,bodyparts(2x3)",~,,c,c,,,c,c,c,c,c,c,c +,,c,c,c,c,c,c,c,,,c,c,,"c{name=""Refuse feeder"" give_to=""Rawhides"" take_from=""Starting cloth/trash""}:+cat_refuse/type(1x3)","y2{name=""Corpse feeder"" take_from=""Starting cloth/trash""}:+cat_refuse/corpses,bodyparts(2x3)",~,,c,c,,,c,c,c,c,c,c,c ,,c,c,c,c,c,c,c,,`,`,`,,~,~,~,,`,`,`,,c,c,c,c,c,c,c ,,c,c,c,c,c,c,c,,`,`,`,,~,~,~,,`,`,`,,c,c,c,c,c,c,c ,,c,c,c,c,c,c,c,,`,`,`,,,`,,,`,`,`,,c,c,c,c,c,c,c @@ -1687,7 +1756,7 @@ build2/industry_build2 ,,w,`,`,`,`,`,`,"w{name=""Wood feeder""}(2x5)",,"g{name=""Goods feeder"" containers=0}:+cat_food/tallow+wax-crafts-goblets(3x3)",,`,,`,`,`,`,`,,"hlS{name=""Cloth/bones feeder"" containers=0}:+cat_refuse/skulls/,bones/,hair/,shells/,teeth/,horns/-adamantinethread(5x5)",,,~,~,`,`,`,`,`,`,c ,,w,`,`,`,`,`,`,~,~,~,~,~,`,`,,,,`,`,~,~,~,~,~,`,`,`,`,`,`,c ,,`,`,`,`,`,"c{name=""Goods/wood quantum"" quantum=true give_to=""Pots,Barrels,Jugs,Bags,Seeds feeder""}:+all",`,~,~,~,~,~,,`,,`,,`,,~,~,~,~,~,`,"r{name=""Cloth/bones quantum"" quantum=true}:+all",`,`,`,`,c -,,"c{name=""Lye"" barrels=2}:+miscliquid",`,`,`,`,`,`,~,~,"u{name=""Furniture feeder""}:-sand(3x2)",~,~,`,`,,,,`,`,~,~,~,~,~,`,`,`,`,`,`,c +,,"c{name=""Lye"" barrels=0}:+miscliquid",`,`,`,`,`,`,~,~,"u2{name=""Furniture feeder""}:-sand(3x2)",~,~,`,`,,,,`,`,~,~,~,~,~,`,`,`,`,`,`,c ,,c,`,`,`,`,`,`,~,~,~,~,~,,`,`,`,`,`,,~,~,~,~,~,`,`,`,`,`,`,c ,,c,`,`,`,`,`,`,`,`,`,`,`,,,`,,`,,,`,`,`,`,`,`,`,`,`,`,`,c ,,c,`,`,`,`,`,`,`,`,`,`,`,`,"bnpdz{name=""Bar/military feeder"" containers=0}:-potash+adamantinethread(5x3)",,,,~,`,`,`,`,`,`,`,`,`,`,`,`,c @@ -1709,7 +1778,7 @@ build2/industry_build2 ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` -,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,~,`,`,`,`,`,`,`,`,`,`,`,`,` +,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,"wj{name=""Encruster"" take_from=""Goods/wood quantum,Stoneworker quantum,Gem feeder""}",`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,"wr{name=""Stone craftsdwarf""}",`,`,`,`,`,`,`,wt,`,`,`,`,`,`,`,`,` @@ -1745,7 +1814,7 @@ build2/industry_build2 ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` -,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,"wj{name=""Encruster"" take_from=""Goods/wood quantum,Stoneworker quantum,Gem feeder""}",`,`,`,`,`,`,`,`,`,`,`,`,` +,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,~,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,~,`,`,`,`,`,`,`,~,`,`,`,`,`,`,`,`,` @@ -1771,7 +1840,7 @@ build2/industry_build2 ,,,,`,`,`,`,`,`,`,`,`,es,`,`,`,`,`,`,`,es,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` -,,,,`,`,`,`,`,`,`,`,`,eg,`,`,`,wf,`,`,`,ek,`,`,`,`,`,`,`,`,` +,,,,`,`,`,`,`,`,`,`,`,eg,`,`,`,wf{max_general_orders=9},`,`,`,ek,`,`,`,`,`,`,`,`,` ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` ,,,,,,,,,,,`,`,`,`,`,`,`,`,`,`,`,`,` @@ -2452,7 +2521,7 @@ doors/guildhall_doors "#build label(guildhall_furnish) start(15; 15; central stairs) hidden() furnish 4 guildhalls, 3 temples, and a library" -,,`,`,`,`,s,`,`,,,`,s,`,c,`,`,f,,,`,`,s,`,`,`,` +,,F,`,`,`,s,`,`,,,F,s,`,c,`,`,f,,,`,`,s,`,`,`,F ,,`,`,c,`,`,`,`,,,`,`,h,~a,`,`,f,,,`,`,`,`,c,`,` ,,`,c,t,c,`,`,s,,,`,`,`,`,`,`,`,,,s,`,`,c,t,c,` ,,`,`,c,`,c,`,`,,,`,`,`,`,`,`,`,,,`,`,c,`,c,`,` @@ -2467,7 +2536,7 @@ doors/guildhall_doors ,,c,~a,`,`,c,c,`,,~,,`,,`,,`,,~,,`,c,c,`,`,~a,c ,,`,h,`,`,c,c,`,~,`,~,`,,,,`,~,`,~,`,c,c,`,`,h,` ,,s,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,s -,,`,`,`,`,`,`,t,,,,,~,,~,,,,,t,`,`,`,`,`,` +,,F,`,`,`,`,`,t,,,,,~,,~,,,,,t,`,`,`,`,`,F ,,,,,,,~,,,,,,`,~,`,,,,,,~ ,,,,,,,~,,,,,,~,,~,,,,,,~ ,,`,`,s,`,`,`,t,,,t,`,`,`,`,`,t,,,t,`,`,`,s,`,` @@ -2476,7 +2545,7 @@ doors/guildhall_doors ,,`,`,c,`,c,`,`,,,t,c,`,`,`,c,t,,,`,`,c,`,c,`,` ,,`,c,t,c,`,`,s,,,t,c,`,`,`,c,t,,,s,`,`,c,t,c,` ,,`,`,c,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,c,`,` -,,`,`,`,`,s,`,`,,,h,~c,~c,s,~c,~c,h,,,`,`,s,`,`,`,` +,,F,`,`,`,s,`,`,,,h,~c,~c,s,~c,~c,h,,,`,`,s,`,`,`,F #notes label(beds_help) @@ -2501,37 +2570,37 @@ Apartments Walkthrough: "#dig label(suites1) start(18; 18; central stairs) message(Once the area is dug out, run /suites2) noble suites" ,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d -,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d -,d,d,,,,d,,,,,,d,,,,d,,d,,,,d,,,,,,d,,,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,,,d,,,,,,d,,,,d,d,d,,,,d,,,,,,d,,,,d,d +,d,,,,d,,,,,,,d,,,,d,d,d,,,,d,,,,,,,d,,,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,,d +,d,,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,,,d,,,,,,,d,,,,d,d,d,,,,d,,,,,,,d,,,,d ,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,`,`,`,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d ,d,d,,d,d,d,d,d,d,d,d,d,d,,d,`,~,`,d,,d,d,d,d,d,d,d,d,d,d,,d,d ,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,`,`,`,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d -,d,d,,,,d,,,,,,d,,,,d,d,d,,,,d,,,,,,d,,,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,d,d,d,d,d,d,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,d,d,d,d,d,,d,d -,d,d,,,,d,,,,,,d,,,,d,,d,,,,d,,,,,,d,,,,d,d -,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d +,d,,,,d,,,,,,,d,,,,d,d,d,,,,d,,,,,,,d,,,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,,d +,d,,,,,,,,,,,,,,,d,d,d,,,,,,,,,,,,,,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,d,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,d,d,d,d,d,,,d,d,d,d,d,,d,,d,,d,d,d,d,d,,,d,d,d,d,d,,d +,d,,,,d,,,,,,,d,,,,d,d,d,,,,d,,,,,,,d,,,,d ,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d "#meta label(suites2) start(central stairs) message(Remember to enqueue manager orders for this blueprint. @@ -2541,73 +2610,73 @@ build_suites/suites_build #dig label(suites_traffic) start(18; 18; central stairs) hidden() don't path through other dwarves' rooms ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` -,`,`,,,,or,,,,,,or,,,,oh,,oh,,,,or,,,,,,or,,,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,or,`,`,`,`,`,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,`,`,`,`,`,or,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,or,`,`,`,`,`,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,`,`,`,`,`,or,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,,,or,,,,,,or,,,,oh,`,oh,,,,or,,,,,,or,,,,`,` +,`,,,,or,,,,,,,or,,,,`,`,`,,,,or,,,,,,,or,,,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,or,`,`,`,`,`,,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,,`,`,`,`,`,or,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,,` +,`,,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,or,`,`,`,`,`,,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,,`,`,`,`,`,or,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,,,or,,,,,,,or,,,,oh,`,oh,,,,or,,,,,,,or,,,,` ,`,`,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,`,`,`,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,`,` ,`,`,,`,`,`,`,`,`,`,`,`,`,,`,`,~,`,`,,`,`,`,`,`,`,`,`,`,`,,`,` ,`,`,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,`,`,`,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,oh,`,` -,`,`,,,,or,,,,,,or,,,,oh,`,oh,,,,or,,,,,,or,,,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,or,`,`,`,`,`,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,`,`,`,`,`,or,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,or,`,`,`,`,`,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,`,`,`,`,`,or,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,`,`,`,`,`,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,`,`,`,`,`,,`,` -,`,`,,,,or,,,,,,or,,,,oh,,oh,,,,or,,,,,,or,,,,`,` -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` +,`,,,,or,,,,,,,or,,,,oh,`,oh,,,,or,,,,,,,or,,,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,or,`,`,`,`,`,,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,,`,`,`,`,`,or,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,,` +,`,,,,,,,,,,,,,,,oh,`,oh,,,,,,,,,,,,,,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,or,`,`,`,`,`,,,`,`,`,`,`,or,oh,`,oh,or,`,`,`,`,`,,,`,`,`,`,`,or,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,`,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,`,`,`,`,`,,,`,`,`,`,`,,oh,,oh,,`,`,`,`,`,,,`,`,`,`,`,,` +,`,,,,or,,,,,,,or,,,,`,`,`,,,,or,,,,,,,or,,,,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` #build label(suites_build) start(18; 18; central stairs) hidden() ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` -,`,`,,,,d,,,,,,d,,,,d,,d,,,,d,,,,,,d,,,,`,` -,`,`,,a,r,`,`,h,,h,`,`,r,a,,`,s,`,,a,r,`,`,h,,h,`,`,r,a,,`,` -,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,` -,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,` -,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,s,`,,c,`,`,`,f,,f,`,`,`,c,,`,` -,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,` -,`,`,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,`,` -,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,n,`,s,`,t,,`,` -,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,` -,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,` -,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,h,`,`,`,`,,`,` -,`,`,,a,r,`,`,h,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,h,`,`,r,a,,`,` -,`,`,,,,d,,,,,,d,,,,`,`,`,,,,d,,,,,,d,,,,`,` +,`,,,,d,,,,,,,d,,,,`,`,`,,,,d,,,,,,,d,,,,` +,`,,a,r,`,`,h,,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,,h,`,`,r,a,,` +,`,,`,`,`,`,h,,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,,h,`,`,`,`,,` +,`,d,`,`,b,`,`,,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,,`,`,b,`,`,d,` +,`,,c,`,`,`,f,,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,,f,`,`,`,c,,` +,`,,t,`,s,`,n,,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,,n,`,s,`,t,,` +,`,,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,,` +,`,,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,,` +,`,,t,`,s,`,n,,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,,n,`,s,`,t,,` +,`,,c,`,`,`,f,,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,,f,`,`,`,c,,` +,`,d,`,`,b,`,`,,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,,`,`,b,`,`,d,` +,`,,`,`,`,`,h,,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,,h,`,`,`,`,,` +,`,,a,r,`,`,h,,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,,h,`,`,r,a,,` +,`,,,,d,,,,,,,d,,,,`,`,`,,,,d,,,,,,,d,,,,` ,`,`,d,`,`,`,`,`,`,`,`,`,`,d,`,`,`,`,`,d,`,`,`,`,`,`,`,`,`,`,d,`,` ,`,`,,s,`,`,s,`,`,s,`,`,s,,`,`,~,`,`,,s,`,`,s,`,`,s,`,`,s,,`,` ,`,`,d,`,`,`,`,`,`,`,`,`,`,d,`,`,`,`,`,d,`,`,`,`,`,`,`,`,`,`,d,`,` -,`,`,,,,d,,,,,,d,,,,`,`,`,,,,d,,,,,,d,,,,`,` -,`,`,,a,r,`,`,h,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,h,`,`,r,a,,`,` -,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,h,`,`,`,`,,`,` -,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,` -,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,` -,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,n,`,s,`,t,,`,` -,`,`,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,`,` -,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,`,`,,t,`,s,`,n,,n,`,s,`,t,,`,` -,`,`,,c,`,`,`,f,,f,`,`,`,c,,`,s,`,,c,`,`,`,f,,f,`,`,`,c,,`,` -,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,`,`,b,`,`,d,`,` -,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,`,`,,`,`,`,`,h,,h,`,`,`,`,,`,` -,`,`,,a,r,`,`,h,,h,`,`,r,a,,`,s,`,,a,r,`,`,h,,h,`,`,r,a,,`,` -,`,`,,,,d,,,,,,d,,,,d,,d,,,,d,,,,,,d,,,,`,` -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` +,`,,,,d,,,,,,,d,,,,`,`,`,,,,d,,,,,,,d,,,,` +,`,,a,r,`,`,h,,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,,h,`,`,r,a,,` +,`,,`,`,`,`,h,,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,,h,`,`,`,`,,` +,`,d,`,`,b,`,`,,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,,`,`,b,`,`,d,` +,`,,c,`,`,`,f,,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,,f,`,`,`,c,,` +,`,,t,`,s,`,n,,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,,n,`,s,`,t,,` +,`,,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,,` +,`,,,,,,,,,,,,,,,`,`,`,,,,,,,,,,,,,,,` +,`,,t,`,s,`,n,,,n,`,s,`,t,,`,s,`,,t,`,s,`,n,,,n,`,s,`,t,,` +,`,,c,`,`,`,f,,,f,`,`,`,c,,`,`,`,,c,`,`,`,f,,,f,`,`,`,c,,` +,`,d,`,`,b,`,`,,,`,`,b,`,`,d,`,`,`,d,`,`,b,`,`,,,`,`,b,`,`,d,` +,`,,`,`,`,`,h,,,h,`,`,`,`,,`,s,`,,`,`,`,`,h,,,h,`,`,`,`,,` +,`,,a,r,`,`,h,,,h,`,`,r,a,,d,,d,,a,r,`,`,h,,,h,`,`,r,a,,` +,`,,,,d,,,,,,,d,,,,`,`,`,,,,d,,,,,,,d,,,,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` "#dig label(apartments1) start(18; 18; central stairs) message(Once the area is dug out, continue with /apartments2.) apartment complex" @@ -2700,13 +2769,13 @@ b(4x5),,,b(4x5),,,b(4x5),,,b(4x5),,,b(4x5),,,,`,`,`,b(4x5),,,b(4x5),,,b(4x5),,,b ,,,,,,,,,,,,,,,,`,`,` ,h,f,,h,f,,h,f,,h,f,,h,f,,`,`,`,,f,h,,f,h,,f,h,,f,h,,f,h ,`,`,,`,`,,`,`,,`,`,,`,`,,`,`,`,,`,`,,`,`,,`,`,,`,`,,`,` -,`,b,,`,b,,`,b,,`,b,,`,b,,`,,`,,b,`,,b,`,,b,`,,b,`,,b,` +,`,b,,`,b,,`,b,,`,b,,`,b,,d,,d,,b,`,,b,`,,b,`,,b,`,,b,` ,d,,,d,,,d,,,d,,,d,,,`,`,`,,,d,,,d,,,d,,,d,,,d -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,`,`,d,`,`,`,`,`,d,`,`,`,`,`,`,`,`,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,,`,`,~,`,`,,`,`,`,`,`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,`,`,d,`,`,`,`,`,d,`,`,`,`,`,`,`,`,`,`,`,`,` ,d,,,d,,,d,,,d,,,d,,,`,`,`,,,d,,,d,,,d,,,d,,,d -,`,b,,`,b,,`,b,,`,b,,`,b,,`,,`,,b,`,,b,`,,b,`,,b,`,,b,` +,`,b,,`,b,,`,b,,`,b,,`,b,,d,,d,,b,`,,b,`,,b,`,,b,`,,b,` ,`,`,,`,`,,`,`,,`,`,,`,`,,`,`,`,,`,`,,`,`,,`,`,,`,`,,`,` ,h,f,,h,f,,h,f,,h,f,,h,f,,`,`,`,,f,h,,f,h,,f,h,,f,h,,f,h ,,,,,,,,,,,,,,,,`,`,` diff --git a/data/dfhack-config/autonick.txt b/data/dfhack-config/autonick.txt index 12fbe65e6..bdd40beed 100644 --- a/data/dfhack-config/autonick.txt +++ b/data/dfhack-config/autonick.txt @@ -7,6 +7,332 @@ Toady One Threetoe +#Dwarven single names taken from Classic Fantasy works +Balin +Dwalin +Fili +Kili +Gloin +Oin +Bifur +Bofur +Bombur +Ori +Gimli +Thrain +Thror +Fundin +Frerin +Gror +Ibun +Khim +Fimbrethil +Floi +Nali +Thor +Vili +Regin +Fafnir +Brokkr +Sindri +Nordri +Sudri +Austri +Doriath +Thingol +Eol +Mim +Telchar +Narvi +Gundabad +Muhrak +Skorri +Draupnir +Alaric +Grimr +Eitri +Svidurr +Thorgar +Hrungnir +Galar +Skirni +Hreinn +Dori +Hreimr +Hreinir +Hroaldr +Groin +Vestri +Nori +Durin +Dvalin +Eikinskjaldi +Bafur +Snorri +Fimafeng +Vitr +Dvali +Dain +Nain +Nipingr +Hrodvitnir +Sveinn +Ivaldi +Svein +Sveinbjorn +Havardr +Haki +Hakon +Mouri +Motsognir +Eberk +Thromar +Kragar +Borgar +Throk +Orvald +Berric +Rogar +Urgen +Morgrim +Keldar +Ingvar +Frandar +Grimsi +Hrokur +Orik +Rundar +Bjornar +Throki +Dworin +Thranduil +Faldar +Galdor +Thorkel +Dorrin +Borkan +Gundrik +Throkir +Raldor +Helgrim +Throgar +Borin +Ragnir +Orvar +Skalf +Baldir +Fror +Thorgil +Ulfar +Grimbold +Faldur +Varrin +Dornir +Halgrim +Gundin +Ulfgar +Skalfar +Yngvarr +Kaldur +Thrandar +Keldin +Rundin +Skaldur +Borgin +Haldur +Bjornulf +Orkarn +Ragnor +Baldrick +Thorlin +Graldor +Ulfrik +Fornir +Egil +Grimnor +Roldor +Ulfgard +Borgrim +Faldrik +Rognir +Balfor +Volmar +Thormund +Brynhild + +#Dwarven composite names taken from Classic Fantasy works +Gorin Stonehammer +Brundar Ironfoot +Haldrek Battlebeard +Orin Stonefist +Frida Stormaxe +Torvald Rockjaw +Einar Blackforge +Thorgar Granitebeard +Ragnir Hammerhelm +Hilda Ironbrow +Grimnar Deepdelver +Ulfrik Ironmane +Freya Thunderstone +Ragnar Firebeard +Gunnar Ironpeak +Astrid Ironheart +Bjorn Steelbreaker +Hrolf Thunderhammer +Sigrun Stonebreaker +Eirik Rockbeard +Helga Frostbeard +Skaldur Stormguard +Agnar Stonehand +Ingrid Mountainmace +Hjalmar Blackstone +Solveig Steelhelm +Rurik Stonegrip +Freyja Silveraxe +Thordur Goldbeard +Gudrun Ironfoot +Vali Fireforge +Thora Frostbeard +Vargr Stoneborn +Astrid Ironbrow +Einar Blackstone +Hilda Hammerheart +Leif Ironshaper +Thrain Stormbrow +Sigrid Steelheart +Haldor Boulderbreaker +Ragnhild Strongarm +Brynjar Ironmantle +Sigrun Thunderbeard +Valgard Steelbeard +Gunnhild Stonefist +Ingrid Ironrock +Eirik Frostbane +Helga Deepforge +Skaldur Ironshield +Agnar Stonemace +Solveig Stormgrip +Hjalmar Mountainheart +Gudrun Firebeard +Thora Thunderstrike +Vargr Ironhand +Freyja Stoneguard +Thordur Blackstone +Rurik Hammerbeard +Solveig Ironbreaker +Astrid Goldhand +Einar Stormbrew +Hilda Steelbeard +Thrain Ironmane +Sigrid Fireheart +Haldor Thunderstone +Ragnhild Ironfoot +Brynjar Blackhelm +Sigrun Frostbeard +Valgard Stoneshield +Gunnhild Ironheart +Bjorn Deepdelver +Ingrid Ironpeak +Eirik Thunderhammer +Gormund Stoneforge +Eovar Broadshield +Thrunir Hammerstone +Brunhild Steelbraid +Garrik Frostbeard +Haldrek Ironhand +Astrid Rockrider +Dagmar Stonefury +Borgar Thunderhelm +Ingrid Ironstrike +Rurik Blackmane +Fjorn Stoneborn +Siv Ironbreaker +Gudrik Stormbeard +Ulfgar Emberforge +Eilif Silverstone +Hilda Stormwarden +Ormar Ironjaw +Vali Steelshaper +Eira Frostbeard +Torgar Graniteheart +Brunhild Firebrand +Haldrek Ironmantle +Solveig Rockbreaker +Thrain Thunderaxe +Brynjar Stoneclaw +Asa Ironhide +Grimnar Blackmane +Ragnvald Hammerfall +Gudbrand Ironhand +Astrid Flamebeard +Ormur Steelbender +Hjalmar Rockjaw +Inga Thunderheart +Valgard Ironbeard +Eirik Swiftstrike +Sylvi Stoneguard +Helge Hammerfist +Jorunn Fireforge +Solveig Ironroot +Thora Stormbeard +Baldur Stonemane +Freydis Ironshaper +Gunnvald Deepstone +Bjorn Blackstone +Ingrid Frostmane +Agnar Steelhammer +Thordur Ironbeard +Ylva Goldhand +Greta Firestone +Rurik Rockhelm +Gunnhild Ironsong +Vali Steelgrip +Brynhild Stormblade +Astrid Ironmantle +Einar Stoneshield +Hilda Frostbeard +Ormr Ironheart +Inga Steelbreaker +Ulfrik Thunderaxe +Freyja Stonebeard +Sigrun Frostfury +Sylvi Blackmane +Thorvald Ironhelm +Eirik Stormstone +Haldora Deepdelver +Sigrid Steelshaper +Gunnar Thunderheart +Bjorn Ironbrow +Ingrid Goldmantle +Agnar Stormforge +Solveig Ironclaw +Thora Rockguard +Grimur Emberstone +Ragnhild Hammerstrike +Vali Ironfist +Brynjar Blackbraid +Astrid Flameforge +Einar Stonestorm +Hilda Frostbane +Ormur Ironhelm +Inga Steelshaper +Gudbrand Thunderbeard +Freya Stonefist +Gunnvald Stormmane +Bjorn Ironhelm +Ingrid Frostforge +Agnar Steelgrip +Thordur Ironhand +Ylva Flameheart +Greta Stonemane +Rurik Ironroot +Gunnhild Steelbeard +Vali Thunderstrike +Thorin Oakenshield +Dain Ironfoot +Gamil Zirak + + # animals Mouse Otter diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index f677ca301..ce4a8a490 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -38,7 +38,7 @@ keybinding add Ctrl-V@dwarfmode digv keybinding add Ctrl-Shift-V@dwarfmode "digv x" # clean the selected tile of blood etc -keybinding add Ctrl-C spotclean +keybinding add Ctrl-C@dwarfmode spotclean # destroy the selected item keybinding add Ctrl-K@dwarfmode autodump-destroy-item @@ -157,7 +157,7 @@ keybinding add Alt-K@dwarfmode toggle-kbd-cursor #keybinding add Ctrl-Shift-T@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit "gui/rename unit-profession" # gui/design -keybinding add Ctrl-D@dwarfmode gui/design +keybinding add Ctrl-D@dwarfmode/Default gui/design diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index 38221883a..02169b526 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -80,6 +80,7 @@ # Enable system services enable buildingplan +enable burrow enable confirm enable logistics enable overlay diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 15ff52488..d3cfbb415 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -4,11 +4,19 @@ add_subdirectory(lua) add_subdirectory(md5) add_subdirectory(protobuf) +if(UNIX) + set_target_properties(lua PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-deprecated-enum-enum-conversion") + set_target_properties(protoc PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-restrict") + set_target_properties(protoc-bin PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-restrict") + set_target_properties(protobuf-lite PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-restrict") + set_target_properties(protobuf PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-restrict") +endif() + if(UNIX AND NOT APPLE) # remove this once our MSVC build env has been updated option(INSTALL_GTEST "Enable installation of googletest. (Projects embedding googletest may want to turn this OFF.)" OFF) add_subdirectory(googletest) if(UNIX) - set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized -Wno-sign-compare") + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized -Wno-sign-compare -Wno-restrict") endif() endif() @@ -31,8 +39,6 @@ option(CLSOCKET_DEP_ONLY "Build for use inside other CMake projects as dependenc add_subdirectory(clsocket) ide_folder(clsocket "Depends") -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/luacov/src/luacov/ DESTINATION ${DFHACK_DATA_DESTINATION}/lua/luacov) - # set the default values of libexpat options - the descriptions are left empty # because later option() calls *do* override those set(EXPAT_BUILD_EXAMPLES OFF CACHE BOOL "") diff --git a/depends/clsocket b/depends/clsocket index d5e17c601..8cf949340 160000 --- a/depends/clsocket +++ b/depends/clsocket @@ -1 +1 @@ -Subproject commit d5e17c6012e7eefb0cbe3e130a56c24bd11f0094 +Subproject commit 8cf949340e22001bee1ca25c9d6c1d6a89e8faf2 diff --git a/depends/libzip b/depends/libzip index 081249cce..fc4a77ea2 160000 --- a/depends/libzip +++ b/depends/libzip @@ -1 +1 @@ -Subproject commit 081249cceb59adc857a72d67e60c32047680f787 +Subproject commit fc4a77ea28eb5eba0ecd11443f291335ec3d2aa0 diff --git a/depends/protobuf/google/protobuf/repeated_field.h b/depends/protobuf/google/protobuf/repeated_field.h index aed4ce9f2..637708254 100644 --- a/depends/protobuf/google/protobuf/repeated_field.h +++ b/depends/protobuf/google/protobuf/repeated_field.h @@ -46,6 +46,10 @@ #ifndef GOOGLE_PROTOBUF_REPEATED_FIELD_H__ #define GOOGLE_PROTOBUF_REPEATED_FIELD_H__ +#ifdef __GNUC__ +#pragma GCC system_header +#endif + #include #include #include diff --git a/docs/Core.rst b/docs/Core.rst index 763858b61..a8991147f 100644 --- a/docs/Core.rst +++ b/docs/Core.rst @@ -377,6 +377,23 @@ Other (non-DFHack-specific) variables that affect DFHack: sensitive), ``DF2CONSOLE()`` will produce UTF-8-encoded text. Note that this should be the case in most UTF-8-capable \*nix terminal emulators already. +Core preferences +================ + +There are a few settings that can be changed dynamically via +`gui/control-panel` to affect runtime behavior. You can also toggle these from +the commandline using the `lua` command, e.g. +``lua dfhack.HIDE_ARMOK_TOOLS=true`` or by editing the generated +``dfhack-config/init/dfhack.control-panel-preferences.init`` file and +restarting DF. + +- ``dfhack.HIDE_CONSOLE_ON_STARTUP``: Whether to hide the external DFHack + terminal window on startup. This, of course, is not useful to change + dynamically. You'll have to use `gui/control-panel` or edit the init file + directly and restart DF for it to have an effect. + +- ``dfhack.HIDE_ARMOK_TOOLS``: Whether to hide "armok" tools in command lists. + Miscellaneous notes =================== This section is for odd but important notes that don't fit anywhere else. diff --git a/docs/Installing.rst b/docs/Installing.rst index 8a04b7cb9..14b5a0f93 100644 --- a/docs/Installing.rst +++ b/docs/Installing.rst @@ -48,10 +48,6 @@ DF version - see `above ` for details. For example: * ``dfhack-50.07-r1-Windows-64bit.zip`` supports 64-bit DF on Windows -In between stable releases, we may create beta releases to test new features. -These are available via the beta release channel on Steam or from our regular -Github page as a pre-release tagged with a "beta" suffix. - .. warning:: Do *not* download the source code from GitHub, either from the releases page @@ -60,6 +56,31 @@ Github page as a pre-release tagged with a "beta" suffix. you want to compile DFHack instead of using a pre-built release, see `building-dfhack-index` for instructions.) +Beta releases +------------- + +In between stable releases, we may create beta releases to test new features. +These are available via the ``beta`` release channel on Steam or from our +regular Github page as a pre-release tagged with a "beta" or "rc" suffix. + +Development builds +------------------ + +If you are actively working with the DFHack team on testing a feature, you may +want to download and install a development build. They are available via the +``testing`` release channel on Steam or can be downloaded from the build +artifact list on GitHub for specific repository commits. + +To download a development build from GitHub: + +- Ensure you are logged into your GitHub account +- Go to https://github.com/DFHack/dfhack/actions/workflows/build.yml?query=branch%3Adevelop+event%3Apush +- Click on the first entry that has a green checkmark +- Click the number under "Artifacts" (or scroll down) +- Click on the "dfhack-*-build-*" artifact for your platform to download + +You can extract this package the same as if you are doing a manual install (see the next section). + Installing DFHack ================= diff --git a/docs/Quickstart.rst b/docs/Quickstart.rst index f4a022a9c..10349a65f 100644 --- a/docs/Quickstart.rst +++ b/docs/Quickstart.rst @@ -45,67 +45,75 @@ Here are some common tasks people use DFHack tools to accomplish: - Quickly scan the map for visible ores of specific types so you can focus your mining efforts -Some tools are one-shot commands. For example, you can run `unforbid all ` -to claim all (reachable) items on the map after a messy siege. - -Other tools must be `enabled ` and then they will run in the background. -For example, `enable seedwatch ` will start monitoring your stocks of -seeds and prevent your chefs from cooking seeds that you need for planting. -Tools that are enabled in the context of a fort will save their state with that -fort, and they will remember that they are enabled the next time you load your save. - -A third class of tools add information to the screen or provide new integrated -functionality via the DFHack `overlay` framework. For example, the `unsuspend` -tool, in addition to its basic function of unsuspending all building construction -jobs, can also overlay a marker on suspended buildings to indicate that they are -suspended (and will use different markers to tell you whether this is a problem). +Some tools are one-shot commands. For example, you can run +`unforbid all ` to claim all (reachable) items on the map after a +messy siege. + +Other tools must be `enabled ` once and then they will run in the +background. For example, once enabled, `seedwatch` will start monitoring your +stocks of seeds and prevent your chefs from cooking seeds that you need for +planting. Tools that are enabled in the context of a fort will save their state +with that fort, and they will remember that they are enabled the next time you +load your save. + +A third class of tools adds information to the screen or provides new integrated +functionality via the DFHack `overlay` framework. For example, the `sort` tool +adds widgets to the squad member selection screen that allow you to search, +sort, and filter the list of military candidates. You don't have to run any +command to get the benefits of the tool, it appears automatically when you're +on the relevant screen. How can I figure out which commands to run? ------------------------------------------- -There are several ways to scan DFHack tools and find the ones you need right now. +There are several ways to scan DFHack tools and find the ones you need right +now. -The first place to check is the DFHack logo hover hotspot. It's in the upper -left corner of the screen by default, though you can move it anywhere you want -with the `gui/overlay` configuration UI. +The first place to check is the DFHack logo menu. It's in the upper left corner +of the screen by default, though you can move it anywhere you want with the +`gui/overlay` configuration UI. -When you hover the mouse over the logo (or hit the Ctrl-Shift-C keyboard shortcut) -a list of DFHack tools relevant to the current context comes up. For example, when -you have a unit selected, the hotspot will show a list of tools that inspect -units, allow you to edit them, or maybe even teleport them. Next to each tool, -you'll see the hotkey you can hit to invoke the command without even opening the -hover list. +When you click on the logo (or hit the Ctrl-Shift-C keyboard shortcut), a short +list of popular, relevant DFHack tools comes up. These are the tools that have +been assigned hotkeys that are active in the current context. For example, when +you're looking at a fort map, the list will contain fortress design tools like +`gui/quickfort` and `gui/design`. You can click on the tools in the list, or +note the hotkeys listed next to them and maybe use them to launch the tool next +time without even opening the logo menu. The second place to check is the DFHack control panel: `gui/control-panel`. It will give you an overview of which tools are currently enabled, and will allow you to toggle them on or off, see help text for them, or launch their dedicated configuration UIs. You can open the control panel from anywhere with the -Ctrl-Shift-E hotkey or by selecting it from the logo hover list. +Ctrl-Shift-E hotkey or by selecting it from the logo menu list. In the control panel, you can also select which tools you'd like to be -automatically enabled when you start a new fort. There are also system settings -you can change, like whether DFHack windows will pause the game when they come -up. - -Finally, you can explore the full extent of the DFHack catalog in `gui/launcher`, -which is always listed first in the DFHack logo hover list. You can also bring up -the launcher by tapping the backtick key (\`) or hitting Ctrl-Shift-D. In the -launcher, you can quickly autocomplete any command name by selecting it in the -list on the right side of the window. Commands are ordered by how often you run -them, so your favorite commands will always be on top. You can also pull full -commandlines out of your history with Alt-S or by clicking on the "history search" -hotkey hint. - -Once you have typed (or autocompleted, or searched for) a command, other commands -related to the one you have selected will appear in the right-hand panel. Scanning -through that list is a great way to learn about new tools that you might find -useful. You can also see how commands are grouped by running the `tags` command. +automatically enabled and popular commands you'd like to run when you start a +new fort. On the "Preferences" tab, there are settings you can change, like +whether you want to limit DFHack functionality to interface improvements, +bugfixes, and productivity tools, hiding the god-mode tools ("mortal mode") or +whether you want DFHack windows to pause the game when they come up. + +Finally, you can explore the full extent of the DFHack catalog in +`gui/launcher`, which is always listed first in the DFHack logo menu list. You +can also bring up the launcher by tapping the backtick key (\`) or hitting +Ctrl-Shift-D. In the launcher, you can quickly autocomplete any command name by +selecting it in the list on the right side of the window. Commands are ordered +by how often you run them, so your favorite commands will always be on top. You +can also pull full commandlines out of your history with Alt-S or by clicking +on the "history search" hotkey hint. + +Once you have typed (or autocompleted, or searched for) a command, other +commands related to the one you have selected will appear in the right-hand +panel. Scanning through that list is a great way to learn about new tools that +you might find useful. You can also see how commands are grouped by running the +`tags` command. The bottom panel will show the full help text for the command you are running, -allowing you to refer to the usage documentation and examples when you are typing -your command. After you run a command, the bottom panel switches to command output -mode, but you can get back to the help text by hitting Ctrl-T or clicking on the -``Help`` tab. +allowing you to refer to the usage documentation and examples when you are +typing your command. After you run a command, the bottom panel switches to +command output mode, but you can get back to the help text by hitting Ctrl-T or +clicking on the ``Help`` tab. How do DFHack in-game windows work? ----------------------------------- @@ -122,84 +130,88 @@ you type at the keyboard. Hit Esc or right click to close the window or cancel the current action. You can click anywhere on the screen that is not a DFHack window to unfocus the window and let it just sit in the background. It won't respond to key presses or mouse clicks until you click on it again to give it -focus. If no DFHack windows are focused, you can right click directly on a window -to close it without left clicking to focus it first. +focus. If no DFHack windows are focused, you can right click directly on a +window to close it without left clicking to focus it first. DFHack windows are draggable from the title bar or from anywhere on the window that doesn't have a mouse-clickable widget on it. Many are resizable as well (if the tool window has components that can reasonably be resized). -You can generally use DFHack tools without interrupting the game. That is, if the -game is unpaused, it can continue to run while a DFHack window is open. If configured -to do so in `gui/control-panel`, tools will initially pause the game to let you -focus on the task at hand, but you can unpause like normal if you want. You can -also interact with the map, scrolling it with the keyboard or mouse and selecting -units, buildings, and items. Some tools will intercept all mouse clicks to allow -you to select regions on the map. When these tools have focus, you will not be able -to use the mouse to interact with map elements or pause/unpause the game. Therefore, -these tools will pause the game when they open, regardless of your settings in -`gui/control-panel`. You can still unpause with the keyboard (spacebar by default), -though. +You can generally use DFHack tools without interrupting the game. That is, if +the game is unpaused, it can continue to run while a DFHack window is open. If +configured to do so in `gui/control-panel`, tools will initially pause the game +to let you focus on the task at hand, but you can unpause like normal if you +want. You can also interact with the map, scrolling it with the keyboard or +mouse and selecting units, buildings, and items. Some tools will intercept all +mouse clicks to allow you to select regions of the map. When these tools have +focus, you will not be able to use the mouse to interact with map elements or +pause/unpause the game. Therefore, these tools will pause the game when they +open, regardless of your settings in `gui/control-panel`. You can still unpause +with the keyboard (spacebar by default), though. Where do I go next? ------------------- To recap: -You can get to popular, relevant tools for the current context by hovering -the mouse over the DFHack logo or by hitting Ctrl-Shift-C. +You can get to popular, relevant tools for the current context by clicking on +the DFHack logo or by hitting Ctrl-Shift-C. You can enable DFHack tools and configure settings with `gui/control-panel`, -which you can access directly with the Ctrl-Shift-E hotkey. +which you can open from the DFHack logo or access directly with the +Ctrl-Shift-E hotkey. You can get to the launcher and its integrated autocomplete, history search, and help text by hitting backtick (\`) or Ctrl-Shift-D, or, of course, by -running it from the logo hover list. +running it from the logo menu list. With those three interfaces, you have the complete DFHack tool suite at your -fingertips. So what to run first? Here are a few commands to get you started. -You can run them all from the launcher. +fingertips. So what to run first? Here are a few examples to get you started. First, let's import some useful manager orders to keep your fort stocked with basic necessities. Run ``orders import library/basic``. If you go to your -manager orders screen, you can see all the orders that have been created for you. -Note that you could have imported the orders directly from this screen as well, -using the DFHack `overlay` widget at the bottom of the manager orders panel. - -Next, try setting up `autochop` to automatically designate trees for chopping when -you get low on usable logs. Run `gui/control-panel` and select ``autochop`` in the -``Fort`` list. Click on the button to the left of the name or hit Enter to enable -it. You can then click on the configure button (the gear icon) to launch -`gui/autochop` if you'd like to customize its settings. If you have the extra -screen space, you can go ahead and set the `gui/autochop` window to minimal mode -(click on the hint near the upper right corner of the window or hit Alt-M) and -click on the map so the window loses keyboard focus. As you play the game, you can -glance at the live status panel to check on your stocks of wood. - -Finally, let's do some fort design copy-pasting. Go to some bedrooms that you have -set up in your fort. Run `gui/blueprint`, set a name for your blueprint by -clicking on the name field (or hitting the 'n' hotkey), typing "rooms" (or whatever) -and hitting Enter to set. Then draw a box around the target area by clicking with -the mouse. When you select the second corner, the blueprint will be saved to your -``blueprints`` subfolder. - -Now open up `gui/quickfort`. You can search for the blueprint you just created by -typing its name, but it should be up near the top already. If you copied a dug-out -area with furniture in it, your blueprint will have two labels: "/dig" and "/build". -Click on the "/dig" blueprint or select it with the keyboard arrow keys and hit Enter. -You can rotate or flip the blueprint around if you need to with the transform hotkeys. -You'll see a preview of where the blueprint will be applied as you move the mouse -cursor around the map. Red outlines mean that the blueprint may fail to fully apply -at that location, so be sure to choose a spot where all the preview tiles are shown -with green diamonds. Click the mouse or hit Enter to apply the blueprint and -designate the tiles for digging. Your dwarves will come and dig it out as if you -had designated the tiles yourself. - -Once the area is dug out, run `gui/quickfort` again and select the "/build" blueprint -this time. Apply the blueprint in the dug-out area, and your furniture will be -designated. It's just that easy! Note that `quickfort` uses `buildingplan` to place -buildings, so you don't even need to have the relevant furniture or building -materials in stock. The planned furniture/buildings will get built whenever you are -able to produce the building materials. +manager orders screen, you can see all the orders that have been created for +you. Note that you could have imported the orders directly from this screen as +well, using the DFHack `overlay` widget at the bottom of the manager orders +panel. + +Next, try setting up `autochop` to automatically designate trees for chopping +when you get low on usable logs. Run `gui/control-panel` and select +``autochop`` in the ``Fort`` list. Click on the button to the left of the name +or hit Enter to enable it. You can then click on the configure button (the gear +icon) to launch `gui/autochop` if you'd like to customize its settings. If you +have the extra screen space, you can go ahead and set the `gui/autochop` window +to minimal mode (click on the hint near the upper right corner of the window or +hit Alt-M) and click on the map so the window loses keyboard focus. As you play +the game, you can glance at the live status panel to check on your stocks of +wood. + +Finally, let's do some fort design copy-pasting. Go to some bedrooms that you +have set up in your fort. Run `gui/blueprint`, set a name for your blueprint by +clicking on the name field (or hitting the 'n' hotkey), typing "rooms" (or +whatever) and hitting Enter to set. Then draw a box around the target area by +clicking with the mouse. When you select the second corner, the blueprint will +be saved to your ``dfhack-config/blueprints`` subfolder. + +Now open up `gui/quickfort`. You can search for the blueprint you just created +by typing its name, but it should be up near the top already. If you copied a +dug-out area with furniture in it, your blueprint will have two labels: "/dig" +and "/build". Click on the "/dig" blueprint or select it with the keyboard +arrow keys and hit Enter. You can rotate or flip the blueprint around if you +need to with the transform hotkeys. You'll see a preview of where the blueprint +will be applied as you move the mouse cursor around the map. Red outlines mean +that the blueprint may fail to fully apply at that location, so be sure to +choose a spot where all the preview tiles are shown with green diamonds. Click +the mouse or hit Enter to apply the blueprint and designate the tiles for +digging. Your dwarves will come and dig it out as if you had designated the +tiles yourself. + +Once the area is dug out, run `gui/quickfort` again and select your "/build" +blueprint this time. Hit ``o`` to generate manager orders for the required +furniture. Apply the blueprint in the dug-out area, and your furniture will be +designated. It's just that easy! Note that `quickfort` uses `buildingplan` to +place buildings, so you don't even need to have the relevant furniture or +building materials in stock yet. The planned furniture/buildings will get built +whenever you are able to produce the building materials. There are many, many more tools to explore. Have fun! diff --git a/docs/Tools.rst b/docs/Tools.rst index 401276d7f..67d7edc54 100644 --- a/docs/Tools.rst +++ b/docs/Tools.rst @@ -3,8 +3,8 @@ DFHack tools ============ -DFHack has **a lot** of tools. This page attempts to make it clearer what they -are, how they work, and how to find the ones you want. +DFHack comes with **a lot** of tools. This page attempts to make it clearer +what they are, how they work, and how to find the ones you want. .. contents:: Contents :local: @@ -36,6 +36,12 @@ more than one category. If you already know what you're looking for, try the `search` or Ctrl-F on this page. If you'd like to see the full list of tools in one flat list, please refer to the `annotated index `. +Some tools are part of our back catalog and haven't been updated yet for v50 of +Dwarf Fortress. These tools are tagged as +`unavailable `. They will still appear in the +alphabetical list at the bottom of this page, but unavailable tools will not +listed in any of the indices. + DFHack tools by game mode ------------------------- diff --git a/docs/about/Authors.rst b/docs/about/Authors.rst index cf74c6412..27b896f89 100644 --- a/docs/about/Authors.rst +++ b/docs/about/Authors.rst @@ -3,7 +3,7 @@ List of authors The following is a list of people who have contributed to DFHack, in alphabetical order. -If you should be here and aren't, please get in touch on IRC or the forums, +If you should be here and aren't, please get in touch on Discord or the forums, or make a pull request! ======================= ======================= =========================== @@ -83,6 +83,7 @@ Herwig Hochleitner bendlas Hevlikn Hevlikn Ian S kremlin- IndigoFenix +Jacek Konieczny Jajcus James 20k James Gilles kazimuth James Logsdon jlogsdon @@ -129,6 +130,7 @@ Michael Crouch creidieki Michon van Dooren MaienM miffedmap miffedmap Mike Stewart thewonderidiot +Mikhail Panov Halifay Mikko Juola Noeda Adeon Milo Christiansen milochristiansen MithrilTuxedo MithrilTuxedo @@ -137,6 +139,7 @@ moversti moversti mrrho mrrho Murad Beybalaev Erquint Myk Taylor myk002 +Najeeb Al-Shabibi master-spike napagokc napagokc Neil Little nmlittle Nick Rart nickrart comestible @@ -203,6 +206,7 @@ Sebastian Wolfertz Enkrod SeerSkye SeerSkye seishuuu seishuuu Seth Woodworth sethwoodworth +shevernitskiy shevernitskiy Shim Panze Shim-Panze Silver silverflyone simon diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 34d44ab0d..da7d42d37 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -10,6 +10,13 @@ work (e.g. links from the `changelog`). :local: :depth: 1 +.. _workorder-recheck: + +workorder-recheck +================= +Tool to set 'Checking' status of the selected work order, allowing conditions to be +reevaluated. Merged into `orders`. + .. _autohauler: autohauler @@ -184,6 +191,12 @@ gui/hack-wish ============= Replaced by `gui/create-item`. +.. _gui/mechanisms: + +gui/mechanisms +============== +Linked building interface has been added to the vanilla UI. + .. _gui/no-dfhack-init: gui/no-dfhack-init @@ -192,13 +205,6 @@ 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. -.. _gui/stockpiles: - -gui/stockpiles -============== -Provided import/export dialogs. Converted to an `overlay` that displays when -a stockpile is selected. - .. _masspit: masspit @@ -220,6 +226,12 @@ ruby Support for the Ruby language in DFHack scripts was removed due to the issues the Ruby library causes when used as an embedded language. +.. _search-plugin: + +search +====== +Functionality was merged into `sort`. + .. _show-unit-syndromes: show-unit-syndromes diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index c9665a048..c6553e48c 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -33,6 +33,11 @@ The ```` parameter above has the following **case-sensitive** syntax:: where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote optional parts. +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 +it is not "applicable" in the current context if a unit is not actively +selected. + When multiple commands are bound to the same key combination, DFHack selects the first applicable one. Later ``add`` commands, and earlier entries within one ``add`` command have priority. Commands that are not specifically intended for diff --git a/docs/changelog.txt b/docs/changelog.txt index 54c2ba361..6f2e0d8f7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -22,6 +22,24 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: 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. +Template for new versions: + +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Documentation + +## API + +## Lua + +## Removed + ===end ]]] @@ -33,22 +51,234 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future +## New Tools +- `burrow`: (reinstated) automatically expand burrows as you dig + +## New Features +- `prospect`: can now give you an estimate of resources from the embark screen. hover the mouse over a potential embark area and run `prospect`. +- `burrow`: integrated 3d box fill and 2d/3d flood fill extensions for burrow painting mode +- `buildingplan`: allow specific mechanisms to be selected when linking levers +- `sort`: military and burrow membership filters for the burrow assignment screen + +## Fixes +- `stockpiles`: hide configure and help buttons when the overlay panel is minimized +- `caravan`: price of vermin swarms correctly adjusted down. a stack of 10000 bees is worth 10, not 10000 +- `sort`: when filtering out already-established temples in the location assignment screen, also filter out the "No specific deity" option if a non-denominational temple has already been established +- RemoteServer: continue to accept connections as long as the listening socket is valid instead of closing the socket after the first disconnect + +## Misc Improvements +- `buildingplan`: display how many items are available on the planner panel +- `buildingplan`: clarify interface when building single-tile staircases +- `sort`: allow searching by profession on the squad assignment page +- `sort`: add search for work animal assignment screen; allow filtering by miltary/squad/civilian/burrow +- `sort`: on the squad assignment screen, make effectiveness and potential ratings use the same scale so effectiveness is always less than or equal to potential for a unit and so you can tell when units are approaching their maximum potential +- `sort`: new overlay on the animal assignment screen that shows how many work animals each visible unit already has assigned to them +- `dreamfort`: Inside+ and Clearcutting burrows now automatically created and managed + +## Documentation + +## API +- ``Gui::revealInDwarfmodeMap``: gained ``highlight`` parameter to control setting the tile highlight on the zoom target +- ``Maps::getWalkableGroup``: get the walkability group of a tile +- ``Units::getReadableName``: now returns the *untranslated* name +- ``Burrows::setAssignedUnit``: now properly handles inactive burrows +- ``Gui::getMousePos``: now takes an optional ``allow_out_of_bounds`` parameter so coordinates can be returned for mouse positions outside of the game map (i.e. in the blank space around the map) + +## Lua +- ``dfhack.gui.revealInDwarfmodeMap``: gained ``highlight`` parameter to control setting the tile highlight on the zoom target +- ``dfhack.maps.getWalkableGroup``: get the walkability group of a tile +- ``dfhack.gui.getMousePos``: support new optional ``allow_out_of_bounds`` parameter +- ``gui.FRAME_THIN``: a panel frame suitable for floating tooltips + +## Removed + +# 50.11-r2 + +## New Tools +- `spectate`: (reinstated) automatically follow dwarves, cycling among interesting ones +- `preserve-tombs`: keep tombs assigned to units when they die + +## New Features +- `logistics`: ``automelt`` now optionally supports melting masterworks; click on gear icon on `stockpiles` overlay frame +- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", candidate assignment on the "Noble" subtab, and the "Work details" subtab under "Labor" +- `sort`: new search and filter widgets for the "Interrogate" and "Convict" screens under "Justice" +- `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) +- `sort`: new search widgets for burrow assignment screen and other unit assignment dialogs +- `sort`: new search widgets for artifacts on the world/raid screen +- `sort`: new search widgets for slab engraving menu; can filter for only units that need a slab to prevent rising as a ghost +- `stocks`: hotkey for collapsing all categories on stocks screen + +## Fixes +- `buildingplan`: remove bars of ash, coal, and soap as valid building materials to match v50 rules +- `buildingplan`: fix incorrect required items being displayed sometimes when switching the planner overlay on and off +- `zone`: races without specific child or baby names will now get generic child/baby names instead of an empty string +- `zone`: don't show animal assignment link for cages and restraints linked to dungeon zones (which aren't normally assignable) +- `sort`: don't count mercenaries as appointed officials in the squad assignment screen +- `dwarfvet`: fix invalid job id assigned to ``Rest`` job, which could cause crashes on reload + +## Misc Improvements +- `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates +- Help icons added to several complex overlays. clicking the icon runs `gui/launcher` with the help text in the help area +- `orders`: ``recheck`` command now only resets orders that have conditions that can be rechecked +- `sort`: added help button for squad assignment search/filter/sort +- `zone`: animals trained for war or hunting are now labeled as such in animal assignment screens +- `buildingplan`: support filtering cages by whether they are occupied +- `buildingplan`: show how many items you need to make when planning buildings +- `tailor`: now adds to existing orders if possilbe instead of creating new ones + +## Documentation +- unavailable tools are no longer listed in the tag indices in the online docs + +## API +- added ``Items::getCapacity``, returns the capacity of an item as a container (reverse-engineered), needed for `combine` + +## Lua +- added ``GRAY`` color aliases for ``GREY`` colors +- added ``dfhack.items.getCapacity`` to expose the new module API +- ``utils.search_text``: text search routine (generalized from internal ``widgets.FilteredList`` logic) + +## Removed +- ``FILTER_FULL_TEXT``: moved from ``gui.widgets`` to ``utils``; if your full text search preference is lost, please reset it in `gui/control-panel` + +# 50.11-r1 + +## New Tools +- `tubefill`: (reinstated) replenishes mined-out adamantine + +## Fixes +- `autolabor`: ensure vanilla work details are reinstated when the fort or the plugin is unloaded +- ``dfhack.TranslateName()``: fixed crash on certain invalid names, which affected `warn-starving` +- EventManager: Unit death event no longer misfires on units leaving the map + +## Misc Improvements +- `digtype`: designate only visible tiles by default, and use "auto" dig mode for following veins +- `digtype`: added options for designating only current z-level, this z-level and above, and this z-level and below +- `hotkeys`: make the DFHack logo brighten on hover in ascii mode to indicate that it is clickable +- `hotkeys`: use vertical bars instead of "!" symbols for the DFHack logo in ascii mode to make it easier to read +- EventManager: guard against potential iterator invalidation if one of the event listeners were to modify the global data structure being iterated over +- EventManager: for ``onBuildingCreatedDestroyed`` events, changed firing order of events so destroyed events come before created events + +## Lua +- mouse key events are now aligned with internal DF semantics: ``_MOUSE_L`` indicates that the left mouse button has just been pressed and ``_MOUSE_L_DOWN`` indicates that the left mouse button is being held down. similarly for ``_MOUSE_R`` and ``_MOUSE_M``. 3rd party scripts may have to adjust. + +# 50.10-r1 + +## Fixes +- Linux launcher: allow Steam Overlay and game streaming to function +- `autobutcher`: don't ignore semi-wild units when marking units for slaughter + +## Misc Improvements +- 'sort': Improve combat skill scale thresholds + +# 50.09-r4 + +## New Features +- `dig`: new overlay for ASCII mode that visualizes designations for smoothing, engraving, carving tracks, and carving fortifications + +## Fixes +- `buildingplan`: make the construction dimensions readout visible again +- `seedwatch`: fix a crash when reading data saved by very very old versions of the plugin +- `gui/mod-manager`: don't continue to display overlay after the raws loading progress bar appears + +## Misc Improvements +- `sort`: add sort option for training need on squad assignment screen +- `sort`: filter mothers with infants, units with weak mental fortitude, and critically injured units on the squad assignment screen +- `sort`: display a rating relative to the current sort order next to the visible units on the squad assignment screen + +## Documentation +- add instructions for downloading development builds to the ``Installing`` page + +## API +- `overlay`: overlay widgets can now declare a ``version`` attribute. changing the version of a widget will reset its settings to defaults. this is useful when changing the overlay layout and old saved positions will no longer be valid. + +## Lua +- ``argparse.boolean``: convert arguments to lua boolean values. + +# 50.09-r3 + +## New Features +- `sort`: search, sort, and filter for squad assignment screen +- `zone`: advanced unit assignment screens for cages, restraints, and pits/ponds +- `buildingplan`: one-click magma/fire safety filter for planned buildings + +## Fixes +- Core: reload scripts in mods when a world is unloaded and immediately loaded again +- Core: fix text getting added to DFHack text entry widgets when Alt- or Ctrl- keys are hit +- `buildingplan`: ensure selected barrels and buckets are empty (or at least free of lye and milk) as per the requirements of the building +- `orders`: prevent import/export overlay from appearing on the create workorder screen +- `caravan`: corrected prices for cages that have units inside of them +- `tailor`: remove crash caused by clothing items with an invalid ``maker_race`` +- ``dialogs.MessageBox``: fix spacing around scrollable text +- `seedwatch`: ignore unplantable tree seeds +- `autobutcher`: fix ``ticks`` commandline option incorrectly rejecting positive integers as valid values + +## Misc Improvements +- Surround DFHack-specific UI elements with square brackets instead of red-yellow blocks for better readability +- `autobutcher`: don't mark animals for butchering if they are already marked for some kind of training (war, hunt) +- `hotkeys`: don't display DFHack logo in legends mode since it covers up important interface elements. the Ctrl-Shift-C hotkey to bring up the menu and the mouseover hotspot still function, though. +- `sort`: animals are now sortable by race on the assignment screens +- `createitem`: support creating items inside of bags + +## API +- ``Items::getValue()``: remove ``caravan_buying`` parameter since the identity of the selling party doesn't actually affect the item value +- `RemoteFortressReader`: add a ``force_reload`` option to the GetBlockList RPC API to return blocks regardless of whether they have changed since the last request +- ``Units``: new animal propery check functions ``isMarkedForTraining(unit)``, ``isMarkedForTaming(unit)``, ``isMarkedForWarTraining(unit)``, and ``isMarkedForHuntTraining(unit)`` +- ``Gui``: ``getAnyStockpile`` and ``getAnyCivzone`` (along with their ``getSelected`` variants) now work through layers of ZScreens. This means that they will still return valid results even if a DFHack tool window is in the foereground. + +## Lua +- ``new()``: improved error handling so that certain errors that were previously uncatchable (creating objects with members with unknown vtables) are now catchable with ``pcall()`` +- ``dfhack.items.getValue()``: remove ``caravan_buying`` param as per C++ API change +- ``widgets.BannerPanel``: panel with distinctive border for marking DFHack UI elements on otherwise vanilla screens +- ``widgets.Panel``: new functions to override instead of setting corresponding properties (useful when subclassing instead of just setting attributes): ``onDragBegin``, ``onDragEnd``, ``onResizeBegin``, ``onResizeEnd`` +- ``dfhack.screen.readTile()``: now populates extended tile property fields (like ``top_of_text``) in the returned ``Pen`` object +- ``dfhack.units``: new animal propery check functions ``isMarkedForTraining(unit)``, ``isMarkedForTaming(unit)``, ``isMarkedForWarTraining(unit)``, and ``isMarkedForHuntTraining(unit)`` +- ``dfhack.gui``: new ``getAnyCivZone`` and ``getAnyStockpile`` functions; also behavior of ``getSelectedCivZone`` and ``getSelectedStockpile`` functions has changes as per the related API notes + +# 50.09-r2 + ## New Plugins +- `3dveins`: reinstated for v50, this plugin replaces vanilla DF's blobby vein generation with veins that flow smoothly and naturally between z-levels +- `zone`: new searchable, sortable, filterable screen for assigning units to pastures +- `dwarfvet`: reinstated and updated for v50's new hospital mechanics; allow your animals to have their wounds treated at hospitals +- `dig`: new ``dig.asciiwarmdamp`` overlay that highlights warm and damp tiles when in ASCII mode. there is no effect in graphics mode since the tiles are already highlighted there ## Fixes -- RemoteServer: fix accept continue to accept connections as long as the listening socket is valid and logs errors +- Fix extra keys appearing in DFHack text boxes when shift (or any other modifier) is released before the other key you were pressing +- `logistics`: don't autotrain domestic animals brought by invaders (they'll get attacked by friendly creatures as soon as you let them out of their cage) +- `logistics`: don't bring trade goods to depot if the only caravans present are tribute caravans +- `gui/create-item`: when choosing a citizen to create the chosen items, avoid choosing a dead citizen +- `logistics`: fix potential crash when removing stockpiles or turning off stockpile features ## Misc Improvements +- `stockpiles`: include exotic pets in the "tameable" filter +- `logistics`: bring an autotraded bin to the depot if any item inside is tradeable instead of marking all items within the bin as untradeable if any individual item is untradeable +- `autonick`: add more variety to nicknames based on famous literary dwarves +- ``widgets.EditField``: DFHack edit fields now support cut/copy/paste with the system clipboard with Ctrl-X/Ctrl-C/Ctrl-V +- Suppress DF keyboard events when a DFHack keybinding is matched. This prevents, for example, a backtick from appearing in a textbox as text when you launch `gui/launcher` from the backtick keybinding. +- Dreamfort: give noble suites double-thick walls and add apartment doors ## Documentation +- `misery`: rewrite the documentation to clarify the actual effects of the plugin ## API +- ``Units::getUnitByNobleRole``, ``Units::getUnitsByNobleRole``: unit lookup API by role +- ``Items::markForTrade()``, ``Items::isRequestedTradeGood()``, ``Items::getValue``: see Lua notes below ## Internals +- Price calculations fixed for many item types ## Lua +- ``dfhack.units.getUnitByNobleRole``, ``dfhack.units.getUnitsByNobleRole``: unit lookup API by role +- ``dfhack.items.markForTrade``: mark items for trade +- ``dfhack.items.isRequestedTradeGood``: discover whether an item is named in a trade agreement with an active caravan +- ``dfhack.items.getValue``: gained optional ``caravan`` and ``caravan_buying`` parameters for prices that take trader races and agreements into account +- ``widgets.TextButton``: wraps a ``HotkeyLabel`` and decorates it to look more like a button -## Removed +# 50.09-r1 + +## Internals + +- Core: update SDL interface from SDL1 to SDL2 # 50.08-r4 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index d07cb045e..fa7094b40 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -306,7 +306,32 @@ All types and the global object have the following features: * ``type._identity`` Contains a lightuserdata pointing to the underlying - ``DFHack::type_instance`` object. + ``DFHack::type_identity`` object. + +All compound types (structs, classes, unions, and the global object) support: + +* ``type._fields`` + + Contains a table mapping field names to descriptions of the type's fields, + including data members and functions. Iterating with ``pairs()`` returns data + fields in the order they are defined in the type. Functions and globals may + appear in an arbitrary order. + + Each entry contains the following fields: + + * ``name``: the name of the field (matches the ``_fields`` table key) + * ``offset``: for data members, the position of the field relative to the start of the type, in bytes + * ``count``: for arrays, the number of elements + * ``mode``: implementation detail. See ``struct_field_info::Mode`` in ``DataDefs.h``. + + Each entry may also contain the following fields, depending on its type: + + * ``type_name``: present for most fields; a string representation of the field's type + * ``type``: the type object matching the field's type; present if such an object exists + (e.g. present for DF types, absent for primitive types) + * ``type_identity``: present for most fields; a lightuserdata pointing to the field's underlying ``DFHack::type_identity`` object + * ``index_enum``, ``ref_target``: the type object corresponding to the field's similarly-named XML attribute, if present + * ``union_tag_field``, ``union_tag_attr``, ``original_name``: the string value of the field's similarly-named XML attribute, if present Types excluding the global object also support: @@ -659,7 +684,7 @@ Persistent configuration storage -------------------------------- This api is intended for storing configuration options in the world itself. -It probably should be restricted to data that is world-dependent. +It is intended for data that is world-dependent. Entries are identified by a string ``key``, but it is also possible to manage multiple entries with the same key; their identity is determined by ``entry_id``. @@ -692,10 +717,8 @@ Every entry has a mutable string ``value``, and an array of 7 mutable ``ints``. otherwise the existing one is simply updated. Returns *entry, did_create_new* -Since the data is hidden in data structures owned by the DF world, -and automatically stored in the save game, these save and retrieval -functions can just copy values in memory without doing any actual I/O. -However, currently every entry has a 180+-byte dead-weight overhead. +The data is kept in memory, so no I/O occurs when getting or saving keys. It is +all written to a json file in the game save directory when the game is saved. It is also possible to associate one bit per map tile with an entry, using these two methods: @@ -985,41 +1008,26 @@ General-purpose selections ~~~~~~~~~~~~~~~~~~~~~~~~~~ * ``dfhack.gui.getSelectedWorkshopJob([silent])`` - - When a job is selected in :kbd:`q` mode, returns the job, else - prints error unless silent and returns *nil*. - * ``dfhack.gui.getSelectedJob([silent])`` - - Returns the job selected in a workshop or unit/jobs screen. - * ``dfhack.gui.getSelectedUnit([silent])`` - - Returns the unit selected via :kbd:`v`, :kbd:`k`, unit/jobs, or - a full-screen item view of a cage or suchlike. - * ``dfhack.gui.getSelectedItem([silent])`` - - Returns the item selected via :kbd:`v` ->inventory, :kbd:`k`, :kbd:`t`, or - a full-screen item view of a container. Note that in the - last case, the highlighted *contained item* is returned, not - the container itself. - * ``dfhack.gui.getSelectedBuilding([silent])`` - - Returns the building selected via :kbd:`q`, :kbd:`t`, :kbd:`k` or :kbd:`i`. - * ``dfhack.gui.getSelectedCivZone([silent])`` - - Returns the zone currently selected via :kbd:`z` - +* ``dfhack.gui.getSelectedStockpile([silent])`` * ``dfhack.gui.getSelectedPlant([silent])`` - Returns the plant selected via :kbd:`k`. + Returns the currently selected in-game object or the indicated thing + associated with the selected in-game object. For example, Calling + ``getSelectedJob`` when a building is selected will return the job associated + with the building (e.g. the ``ConstructBuilding`` job). If ``silent`` is + ommitted or set to ``false`` and a selected object cannot be found, then an + error is printed to the console. * ``dfhack.gui.getAnyUnit(screen)`` * ``dfhack.gui.getAnyItem(screen)`` * ``dfhack.gui.getAnyBuilding(screen)`` +* ``dfhack.gui.getAnyCivZone(screen)`` +* ``dfhack.gui.getAnyStockpile(screen)`` * ``dfhack.gui.getAnyPlant(screen)`` Similar to the corresponding ``getSelected`` functions, but operate on the @@ -1045,11 +1053,13 @@ Fortress mode Same as ``resetDwarfmodeView``, but also recenter if position is valid. If ``pause`` is false, skip pausing. Respects ``RECENTER_INTERFACE_SHUTDOWN_MS`` in DF's ``init.txt`` (the delay before input is recognized when a recenter occurs.) -* ``dfhack.gui.revealInDwarfmodeMap(pos[,center])`` - ``dfhack.gui.revealInDwarfmodeMap(x,y,z[,center])`` +* ``dfhack.gui.revealInDwarfmodeMap(pos[,center[,highlight]])`` + ``dfhack.gui.revealInDwarfmodeMap(x,y,z[,center[,highlight]])`` - Centers the view on the given coordinates. If ``center`` is true, make sure the - position is in the exact center of the view, else just bring it on screen. + Centers the view on the given coordinates. If ``center`` is true, make sure + the position is in the exact center of the view, else just bring it on screen. + If ``highlight`` is true, then mark the target tile with a pulsing highlight + until the player clicks somewhere else. ``pos`` can be a ``df.coord`` instance or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`), e.g.:: @@ -1130,10 +1140,12 @@ Announcements If you want a guaranteed announcement without parsing, use ``dfhack.gui.showAutoAnnouncement`` instead. -* ``dfhack.gui.getMousePos()`` +* ``dfhack.gui.getMousePos([allow_out_of_bounds])`` Returns the map coordinates of the map tile the mouse is over as a table of - ``{x, y, z}``. If the cursor is not over the map, returns ``nil``. + ``{x, y, z}``. If the cursor is not over a valid tile, returns ``nil``. To + allow the function to return coordinates outside of the map, set + ``allow_out_of_bounds`` to ``true``. Other ~~~~~ @@ -1355,11 +1367,16 @@ Units module * ``dfhack.units.isTame(unit)`` * ``dfhack.units.isTamable(unit)`` * ``dfhack.units.isDomesticated(unit)`` +* ``dfhack.units.isMarkedForTraining(unit)`` +* ``dfhack.units.isMarkedForTaming(unit)`` +* ``dfhack.units.isMarkedForWarTraining(unit)`` +* ``dfhack.units.isMarkedForHuntTraining(unit)`` * ``dfhack.units.isMarkedForSlaughter(unit)`` * ``dfhack.units.isMarkedForGelding(unit)`` * ``dfhack.units.isGeldable(unit)`` * ``dfhack.units.isGelded(unit)`` * ``dfhack.units.isEggLayer(unit)`` +* ``dfhack.units.isEggLayerRace(unit)`` * ``dfhack.units.isGrazer(unit)`` * ``dfhack.units.isMilkable(unit)`` @@ -1438,10 +1455,22 @@ Units module Note that ``pos2xyz()`` cannot currently be used to convert coordinate objects to the arguments required by this function. +* ``dfhack.units.getUnitByNobleRole(role_name)`` + + Returns the unit assigned to the given noble role, if any. ``role_name`` must + be one of the position codes associated with the active fort or civilization + government. For example: ``CAPTAIN_OF_THE_GUARD``, ``MAYOR``, or ``BARON``. + Note that if more than one unit has the role, only the first will be + returned. See ``getUnitsByNobleRole`` below for retrieving all units with a + particular role. + +* ``dfhack.units.getUnitsByNobleRole(role_name)`` + + Returns a list of units (possibly empty) assigned to the given noble role. + * ``dfhack.units.getCitizens([ignore_sanity])`` - Returns a table (list) of all citizens, which you would otherwise have to loop over all - units in world and test against ``isCitizen()`` to discover. + Returns a list of all living citizens. * ``dfhack.units.teleport(unit, pos)`` @@ -1487,6 +1516,11 @@ Units module Computes the effective attribute value, including curse effect. +* ``dfhack.units.casteFlagSet(race, caste, flag)`` + + Returns whether the given ``df.caste_raw_flags`` flag is set for the given + race and caste. + * ``dfhack.units.getMiscTrait(unit, type[, create])`` Finds (or creates if requested) a misc trait object with the given id. @@ -1576,6 +1610,12 @@ Units module Currently only one dream per unit is supported by Dwarf Fortress. Support for multiple dreams may be added in future versions of Dwarf Fortress. +* ``dfhack.units.getReadableName(unit)`` + + Returns a string that includes the language name of the unit (if any), the + race of the unit, whether it is trained for war or hunting, and any + syndrome-given descriptions (such as "necromancer"). + * ``dfhack.units.getStressCategory(unit)`` Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. @@ -1755,9 +1795,17 @@ Items module Calculates the base value for an item of the specified type and material. -* ``dfhack.items.getValue(item)`` +* ``dfhack.items.getValue(item[, caravan_state])`` + + Calculates the value of an item. If a ``df.caravan_state`` object is given + (from ``df.global.plotinfo.caravans`` or + ``df.global.main_interface.trade.mer``), then the value is modified by civ + properties and any trade agreements that might be in effect. - Calculates the Basic Value of an item, as seen in the View Item screen. +* ``dfhack.items.isRequestedTradeGood(item[, caravan_state])`` + + Returns whether a caravan will pay extra for the given item. If caravan_state + is not given, checks all active caravans. * ``dfhack.items.createItem(item_type, item_subtype, mat_type, mat_index, unit)`` @@ -1773,7 +1821,16 @@ Items module * ``dfhack.items.canTradeWithContents(item)`` - Checks whether the item and all items it contains, if any, can be traded. + Returns false if the item or any contained items cannot be traded. + +* ``canTradeAnyWithContents(item)`` + + Returns true if the item is empty and can be traded or if the item contains + any item that can be traded. + +* ``dfhack.items.markForTrade(item, depot)`` + + Marks the given item for trade at the given depot. * ``dfhack.items.isRouteVehicle(item)`` @@ -1857,10 +1914,11 @@ Maps module Returns the plant struct that owns the tile at the specified position. -* ``dfhack.maps.canWalkBetween(pos1, pos2)`` +* ``dfhack.maps.getWalkableGroup(pos)`` - Checks if a dwarf may be able to walk between the two tiles, - using a pathfinding cache maintained by the game. + Returns the walkability group for the given tile position. A return value of + ``0`` indicates that the tile is not walkable. The data comes from a + pathfinding cache maintained by DF. .. note:: This cache is only updated when the game is unpaused, and thus @@ -1869,6 +1927,10 @@ Maps module take into account anything that depends on the actual units, like burrows, or the presence of invaders. +* ``dfhack.maps.canWalkBetween(pos1, pos2)`` + + Checks if both positions are walkable and also share a walkability group. + * ``dfhack.maps.hasTileAssignment(tilemask)`` Checks if the tile_bitmask object is not *nil* and contains any set bits; returns *true* or *false*. @@ -1889,9 +1951,11 @@ Maps module Burrows module -------------- -* ``dfhack.burrows.findByName(name)`` +* ``dfhack.burrows.findByName(name[, ignore_final_plus])`` - Returns the burrow pointer or *nil*. + Returns the burrow pointer or *nil*. if ``ignore_final_plus`` is ``true``, + then ``+`` characters at the end of the names are ignored, both for the + specified ``name`` and the names of the burrows that it matches against. * ``dfhack.burrows.clearUnits(burrow)`` @@ -2474,10 +2538,10 @@ Supported callbacks and fields are: Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience. ``_MOUSE_L, _MOUSE_R, _MOUSE_M`` - If the left, right, and/or middle mouse button is being pressed. + If the left, right, and/or middle mouse button was just pressed. ``_MOUSE_L_DOWN, _MOUSE_R_DOWN, _MOUSE_M_DOWN`` - If the left, right, and/or middle mouse button was just pressed. + If the left, right, and/or middle mouse button is being held down. If this method is omitted, the screen is dismissed on reception of the ``LEAVESCREEN`` key. @@ -2527,6 +2591,67 @@ a ``dfhack.penarray`` instance to cache their output. ``bufferx`` and ``buffery`` default to 0. + +Textures module +--------------- + +In order for the game to render a particular tile (graphic), it needs to know the +``texpos`` - the position in the vector of the registered game textures (also the +graphical tile id passed as the ``tile`` field in a `Pen `). +Adding new textures to the vector is not difficult, but the game periodically +deletes textures that are in the vector, and that's a problem since it +invalidates the ``texpos`` value that used to point to that texture. +The ``textures`` module solves this problem by providing a stable handle instead of a +raw ``texpos``. When we need to draw a particular tile, we can look up the current +``texpos`` value via the handle. +Texture module can register textures in two ways: to reserved and dynamic ranges. +Reserved range is a limit buffer in a game texture vector, that will never be wiped. +It is good for static assets, which need to be loaded at the very beginning and will be used during the process running. +In other cases, it is better to use dynamic range. +If reserved range buffer limit has been reached, dynamic range will be used by default. + +* ``loadTileset(file, tile_px_w, tile_px_h[, reserved])`` + + Loads a tileset from the image ``file`` with give tile dimensions in pixels. The + image will be sliced in row major order. Returns an array of ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). + + Example usage:: + + local logo_textures = dfhack.textures.loadTileset('hack/data/art/dfhack.png', 8, 12) + local first_texposhandle = logo_textures[1] + +* ``getTexposByHandle(handle)`` + + Get the current ``texpos`` for the given ``TexposHandle``. Always use this method to + get the ``texpos`` for your texture. ``texpos`` can change when game textures are + reset, but the handle will be the same. + +* ``createTile(pixels, tile_px_w, tile_px_h[, reserved])`` + + Create and register a new texture with the given tile dimensions and an array of + ``pixels`` in row major order. Each pixel is an integer representing color in packed + RBGA format (for example, #0022FF11). Returns a ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). + +* ``createTileset(pixels, texture_px_w, texture_px_h, tile_px_w, tile_px_h[, reserved])`` + + Create and register a new texture with the given texture dimensions and an array of + ``pixels`` in row major order. Then slice it into tiles with the given tile + dimensions. Each pixel is an integer representing color in packed RBGA format (for + example #0022FF11). Returns an array of ``TexposHandle``. + ``reserved`` is optional boolean argument, which indicates texpos range. + ``true`` - reserved, ``false`` - dynamic (default). + +* ``deleteHandle(handle)`` + + ``handle`` here can be single ``TexposHandle`` or an array of ``TexposHandle``. + Deletes all metadata and texture(s) related to the given handle(s). The handles + become invalid after this call. + + Filesystem module ----------------- @@ -2697,6 +2822,11 @@ and are only documented here for completeness: The oldval, newval or delta arguments may be used to specify additional constraints. Returns: *found_index*, or *nil* if end reached. +* ``dfhack.internal.cxxDemangle(mangled_name)`` + + Decodes a mangled C++ symbol name. Returns the demangled name on success, or + ``nil, error_message`` on failure. + * ``dfhack.internal.getDir(path)`` Lists files/directories in a directory. @@ -2815,6 +2945,19 @@ and are only documented here for completeness: Returns 0 if the address is not found. Requires a heap snapshot. +* ``dfhack.internal.getClipboardTextCp437()`` + + Gets the system clipboard text (and converts text to CP437 encoding). + +* ``dfhack.internal.setClipboardTextCp437(text)`` + + Sets the system clipboard text from a CP437 string. + +* ``dfhack.internal.getSuppressDuplicateKeyboardEvents()`` +* ``dfhack.internal.setSuppressDuplicateKeyboardEvents(suppress)`` + + Gets and sets the flag for whether to suppress DF key events when a DFHack + keybinding is matched and a command is launched. .. _lua-core-context: @@ -2950,6 +3093,9 @@ environment by the mandatory init file dfhack.lua: COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN, COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE + ``COLOR_GREY`` and ``COLOR_DARKGREY`` can also be spelled ``COLOR_GRAY`` and + ``COLOR_DARKGRAY``. + * State change event codes, used by ``dfhack.onStateChange`` Available only in the `core context `, as is the event itself: @@ -3206,6 +3352,20 @@ utils Exactly like ``erase_sorted_key``, but if field is specified, takes the key from ``item[field]``. +* ``utils.search_text(text,search_tokens)`` + + Returns true if all the search tokens are found within ``text``. The text and + search tokens are normalized to lower case and special characters (e.g. ``A`` + with a circle on it) are converted to their "basic" forms (e.g. ``a``). + ``search_tokens`` can be a string or a table of strings. If it is a string, + it is split into space-separated tokens before matching. The search tokens + are treated literally, so any special regular expression characters do not + need to be escaped. If ``utils.FILTER_FULL_TEXT`` is ``true``, then the + search tokens can match any part of ``text``. If it is ``false``, then the + matches must happen at the beginning of words within ``text``. You can change + the value of ``utils.FILTER_FULL_TEXT`` in `gui/control-panel` on the + "Preferences" tab. + * ``utils.call_with_string(obj,methodname,...)`` Allocates a temporary string object, calls ``obj:method(tmp,...)``, and @@ -3416,6 +3576,13 @@ parameters. ``tonumber(arg)``. If ``arg_name`` is specified, it is used to make error messages more useful. +* ``argparse.boolean(arg, arg_name)`` + + Converts ``string.lower(arg)`` from "yes/no/on/off/true/false/etc..." to a lua + boolean. Throws if the value can't be converted, otherwise returns + ``true``/``false``. If ``arg_name`` is specified, it is used to make error + messages more useful. + dumper ====== @@ -3812,6 +3979,14 @@ Misc of keycodes to *true* or *false*. For instance, it is possible to use the table passed as argument to ``onInput``. + You can send mouse clicks as will by setting the ``_MOUSE_L`` key or other + mouse-related pseudo-keys documented with the ``screen:onInput(keys)`` + function above. Note that if you are simulating a click at a specific spot on + the screen, you must set ``df.global.gps.mouse_x`` and + ``df.global.gps.mouse_y`` if you are clicking on the interface layer or + ``df.global.gps.precise_mouse_x`` and ``df.global.gps.precise_mouse_y`` if + you are clicking on the map. + * ``mkdims_xy(x1,y1,x2,y2)`` Returns a table containing the arguments as fields, and also ``width`` and @@ -4412,6 +4587,10 @@ There are the following predefined frame style tables: A frame suitable for overlay widget panels. +* ``FRAME_THIN`` + + A frame suitable for floating tooltip panels that need the DFHack signature. + * ``FRAME_BOLD`` A frame suitable for a non-draggable panel meant to capture the user's focus, @@ -4511,7 +4690,7 @@ Has attributes: * ``drag_anchors = {}`` (default: ``{title=true, frame=false/true, body=true}``) * ``drag_bound = 'frame' or 'body'`` (default: ``'frame'``) * ``on_drag_begin = function()`` (default: ``nil``) -* ``on_drag_end = function(bool)`` (default: ``nil``) +* ``on_drag_end = function(success, new_frame)`` (default: ``nil``) If ``draggable`` is set to ``true``, then the above attributes come into play when the panel is dragged around the screen, either with the mouse or the @@ -4525,13 +4704,15 @@ Has attributes: otherwise. Dragging can be canceled by right clicking while dragging with the mouse, hitting :kbd:`Esc` (while dragging with the mouse or keyboard), or by calling ``Panel:setKeyboaredDragEnabled(false)`` (while dragging with the - keyboard). + keyboard). If it is more convenient to do so, you can choose to override the + ``panel:onDragBegin`` and/or the ``panel:onDragEnd`` methods instead of + setting the ``on_drag_begin`` and/or ``on_drag_end`` attributes. * ``resizable = bool`` (default: ``false``) * ``resize_anchors = {}`` (default: ``{t=false, l=true, r=true, b=true}`` * ``resize_min = {}`` (default: w and h from the ``frame``, or ``{w=5, h=5}``) * ``on_resize_begin = function()`` (default: ``nil``) -* ``on_resize_end = function(bool)`` (default: ``nil``) +* ``on_resize_end = function(success, new_frame)`` (default: ``nil``) If ``resizable`` is set to ``true``, then the player can click the mouse on any edge specified in ``resize_anchors`` and drag the border to resize the @@ -4545,6 +4726,9 @@ Has attributes: Dragging can be canceled by right clicking while resizing with the mouse, hitting :kbd:`Esc` (while resizing with the mouse or keyboard), or by calling ``Panel:setKeyboardResizeEnabled(false)`` (while resizing with the keyboard). + If it is more convenient to do so, you can choose to override the + ``panel:onResizeBegin`` and/or the ``panel:onResizeEnd`` methods instead of + setting the ``on_resize_begin`` and/or ``on_resize_end`` attributes. * ``autoarrange_subviews = bool`` (default: ``false``) * ``autoarrange_gap = int`` (default: ``0``) @@ -4589,6 +4773,15 @@ Has functions: commit the new window size or :kbd:`Esc` to cancel. If resizing is canceled, then the window size from before the resize operation is restored. +* ``panel:onDragBegin()`` +* ``panel:onDragEnd(success, new_frame)`` +* ``panel:onResizeBegin()`` +* ``panel:onResizeEnd(success, new_frame)`` + +The default implementations of these methods call the associated attribute (if +set). You can override them in a subclass if that is more convenient than +setting the attributes. + Double clicking: If the panel is resizable and the user double-clicks on the top edge (the frame @@ -4688,6 +4881,12 @@ following keyboard hotkeys: - Ctrl-B/Ctrl-F: move the cursor one word back or forward. - Ctrl-A/Ctrl-E: move the cursor to the beginning/end of the text. +The widget also supports integration with the system clipboard: + +- Ctrl-C: copy current text to the system clipboard +- Ctrl-X: copy current text to the system clipboard and clear text in widget +- Ctrl-V: paste text from the system clipboard (text is converted to cp437) + The ``EditField`` class also provides the following functions: * ``editfield:setCursor([cursor_pos])`` @@ -5016,13 +5215,50 @@ The CycleHotkeyLabel widget implements the following methods: selected option if no index is given. If an option was defined as just a string, then this function will return ``nil`` for that option. -ToggleHotkeyLabel ------------------ +ToggleHotkeyLabel class +----------------------- This is a specialized subclass of CycleHotkeyLabel that has two options: ``On`` (with a value of ``true``) and ``Off`` (with a value of ``false``). The ``On`` option is rendered in green. +HelpButton class +---------------- + +A 3x1 tile button with a question mark on it, intended to represent a help +icon. Clicking on the icon will launch `gui/launcher` with a given command +string, showing the help text for that command. + +It has the following attributes: + +:command: The command to load in `gui/launcher`. + +ConfigureButton class +--------------------- + +A 3x1 tile button with a gear mark on it, intended to represent a configure +icon. Clicking on the icon will run the given callback. + +It has the following attributes: + +:on_click: The function on run when the icon is clicked. + +BannerPanel class +----------------- + +This is a Panel subclass that prints a distinctive banner along the far left +and right columns of the widget frame. Note that this is not a "proper" frame +since it doesn't have top or bottom borders. Subviews of this panel should +inset their frames one tile from the left and right edges. + +TextButton class +---------------- + +This is a BannerPanel subclass that wraps a HotkeyLabel with some decorators on +the sides to make it look more like a button, suitable for both graphics and +ASCII modes. All HotkeyLabel parameters passed to the constructor are passed +through to the wrapped HotkeyLabel. + List class ---------- @@ -5109,12 +5345,11 @@ FilteredList class ------------------ This widget combines List, EditField and Label into a combo-box like -construction that allows filtering the list by subwords of its items. +construction that allows filtering the list. In addition to passing through all attributes supported by List, it supports: -:case_sensitive: If ``true``, matching is case sensitive. Defaults to ``false``. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_below: If true, the edit field is placed below the list instead of above. :edit_key: If specified, the edit field is disabled until this key is pressed. @@ -5163,9 +5398,9 @@ Filter behavior: By default, the filter matches substrings that start at the beginning of a word (or after any punctuation). You can instead configure filters to match any -substring with a command like:: +substring across the full text with a command like:: - :lua require('gui.widgets').FILTER_FULL_TEXT=true + :lua require('utils').FILTER_FULL_TEXT=true TabBar class ------------ @@ -5219,6 +5454,31 @@ The parent widget owns the range values, and can control them independently (e.g :on_left_change: Callback executed when moving the left handle. :on_right_change: Callback executed when moving the right handle. + +gui.textures +============ + +This module contains convenience methods for accessing default DFHack graphic assets. +Pass the ``offset`` in tiles (in row major position) to get a particular tile from the +asset. ``offset`` 0 is the first tile. + +* ``tp_green_pin(offset)`` tileset: ``hack/data/art/green-pin.png`` +* ``tp_red_pin(offset)`` tileset: ``hack/data/art/red-pin.png`` +* ``tp_icons(offset)`` tileset: ``hack/data/art/icons.png`` +* ``tp_on_off(offset)`` tileset: ``hack/data/art/on-off.png`` +* ``tp_control_panel(offset)`` tileset: ``hack/data/art/control-panel.png`` +* ``tp_border_thin(offset)`` tileset: ``hack/data/art/border-thin.png`` +* ``tp_border_medium(offset)`` tileset: ``hack/data/art/border-medium.png`` +* ``tp_border_bold(offset)`` tileset: ``hack/data/art/border-bold.png`` +* ``tp_border_panel(offset)`` tileset: ``hack/data/art/border-panel.png`` +* ``tp_border_window(offset)`` tileset: ``hack/data/art/order-window.png`` + +Example usage:: + + local textures = require('gui.textures') + local first_border_texpos = textures.tp_border_thin(1) + + .. _lua-plugins: ======= @@ -5356,51 +5616,6 @@ Native functions provided by the `buildingplan` plugin: * ``void doCycle()`` runs a check for whether buildings in the monitor list can be assigned items and unsuspended. This method runs automatically twice a game day, so you only need to call it directly if you want buildingplan to do a check right now. * ``void scheduleCycle()`` schedules a cycle to be run during the next non-paused game frame. Can be called multiple times while the game is paused and only one cycle will be scheduled. -burrows -======= - -The `burrows` plugin implements extended burrow manipulations. - -Events: - -* ``onBurrowRename.foo = function(burrow)`` - - Emitted when a burrow might have been renamed either through - the game UI, or ``renameBurrow()``. - -* ``onDigComplete.foo = function(job_type,pos,old_tiletype,new_tiletype,worker)`` - - Emitted when a tile might have been dug out. Only tracked if the - auto-growing burrows feature is enabled. - -Native functions: - -* ``renameBurrow(burrow,name)`` - - Renames the burrow, emitting ``onBurrowRename`` and updating auto-grow state properly. - -* ``findByName(burrow,name)`` - - Finds a burrow by name, using the same rules as the plugin command line interface. - Namely, trailing ``'+'`` characters marking auto-grow burrows are ignored. - -* ``copyUnits(target,source,enable)`` - - Applies units from ``source`` burrow to ``target``. The ``enable`` - parameter specifies if they are to be added or removed. - -* ``copyTiles(target,source,enable)`` - - Applies tiles from ``source`` burrow to ``target``. The ``enable`` - parameter specifies if they are to be added or removed. - -* ``setTilesByKeyword(target,keyword,enable)`` - - Adds or removes tiles matching a predefined keyword. The keyword - set is the same as used by the command line. - -The lua module file also re-exports functions from ``dfhack.burrows``. - .. _cxxrandom-api: cxxrandom diff --git a/docs/dev/compile/Compile.rst b/docs/dev/compile/Compile.rst index 22b9a7b1a..5e605e391 100644 --- a/docs/dev/compile/Compile.rst +++ b/docs/dev/compile/Compile.rst @@ -88,9 +88,9 @@ assistance. All Platforms ============= -Before you can compile the code you'll need to configure your build with cmake. Some IDEs can do this, -but from command line is the usual way to do this; thought the Windows section below points out some -Windows batch files that can be used to avoid opening a terminal/command-prompt. +Before you can compile the code you'll need to configure your build with cmake. Some IDEs can do this +for you, but it's more common to do it from the command line. Windows developers can refer to the +Windows section below for batch files that can be used to avoid opening a terminal/command-prompt. You should seek cmake's documentation online or via ``cmake --help`` to see how the command works. See the `build-options` page for help finding the DFHack build options relevant to you. diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 4ec226d30..b5b6cf0e3 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -90,6 +90,10 @@ The ``overlay.OverlayWidget`` superclass defines the following class attributes: This will be filled in with the display name of your widget, in case you have multiple widgets with the same implementation but different configurations. +- ``version`` + You can set this to any string. If the version string of a loaded widget + does not match the saved settings for that widget, then the configuration + for the widget (position, enabled status) will be reset to defaults. - ``default_pos`` (default: ``{x=-2, y=-2}``) Override this attribute with your desired default widget position. See the `overlay` docs for information on what positive and negative numbers @@ -131,7 +135,10 @@ The ``overlay.OverlayWidget`` superclass defines the following class attributes: seconds) that your widget can take to react to changes in information and not annoy the player. Set to 0 to be called at the maximum rate. Be aware that running more often than you really need to will impact game FPS, - especially if your widget can run while the game is unpaused. + especially if your widget can run while the game is unpaused. If you change + the value of this attribute dynamically, it may not be noticed until the + previous timeout expires. However, if you need a burst of high-frequency + updates, set it to ``0`` and it will be noticed immediately. Registering a widget with the overlay framework *********************************************** diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst index 38117503c..297e8482a 100644 --- a/docs/guides/modding-guide.rst +++ b/docs/guides/modding-guide.rst @@ -45,6 +45,7 @@ this:: info.txt graphics/... objects/... + blueprints/... scripts_modactive/example-mod.lua scripts_modactive/internal/example-mod/... scripts_modinstalled/... @@ -58,6 +59,9 @@ Let's go through that line by line. - Modifications to the game raws (potentially with custom raw tokens) go in the :file:`graphics/` and :file:`objects/` folders. You can read more about the files that go in these directories on the :wiki:`Modding` wiki page. +- Any `quickfort` blueprints included with your mod go in the + :file:`blueprints` folder. Note that your mod can *just* be blueprints and + nothing else if you like. - A control script in :file:`scripts_modactive/` directory that handles system-level event hooks (e.g. reloading state when a world is loaded), registering `overlays `, and diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 9dda52454..e4b337cc4 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -84,7 +84,7 @@ Feature summary - Configurable zone/location settings, such as the pit/pond toggle or hospital supply quantities -- Build mode +- Build mode - Integrated with DFHack `buildingplan`: you can place buildings before manufacturing building materials and you can use the `buildingplan` UI @@ -108,6 +108,10 @@ Feature summary - Set building properties (such as a name) - Can attach and configure track stops as part of hauling routes +- Burrow mode + + - Supports creating, adding to, and subtracting from burrows. + Introduction to blueprints -------------------------- @@ -866,6 +870,36 @@ names an existing route, the stop will be added to that route:: These two track stops (which do not dump their contents) simply exist on a common route at the ends of a connected carved track. +#burrow mode +------------ + +``#burrow`` mode can create, extend, and remove tiles from burrows. + +Burrow designation syntax +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The syntax should look familiar by now:: + + symbol{properties}(expansion) + +See the `#burrow mode reference`_ for symbol and property definitions. + +Here's how to create (or add to, if a burrow by that name already exists) a +5x5 burrow named ``Inside+``. It will also register this burrow with +`gui/civ-alert` if no burrow has yet been registered:: + + #burrow + a{create=true name=Inside+ civalert=true}(5x5) + +Why the trailing ``+``? That's to indicate to the `burrow` plugin that the +burrow should grow as adjacent tiles are dug out. + +Similarly, here is how to erase a tile from all burrows that currently include +it:: + + #burrow + e + .. _quickfort-modeline: Modeline markers @@ -1992,10 +2026,10 @@ All stockpiles support the following properties: Property Description ================ =========== ``name`` the name of the stockpile -``take_from`` comma-separated list of names of stockpiles or workshops that - the stockpile takes from -``give_to`` comma-separated list of names of stockpiles or workshops that - the stockpile gives to +``take_from`` comma-separated list of names or building ids of stockpiles + or workshops that the stockpile takes from +``give_to`` comma-separated list of names or building ids of stockpiles + or workshops that the stockpile gives to ``links_only`` if set to ``true`` then the stockpile will only take from links ``barrels`` the number of desired barrels @@ -2014,6 +2048,12 @@ Property Description feature ================ =========== +Note that specifying building IDs in ``take_from`` or ``give_to`` lists is +primarily useful when dynamically generating `quickfort` blueprints and +applying them via the API. You will not generally know the ID of a stockpile or +building when writing a blueprint by hand or when preparing a blueprint to +apply in a different fort. + #build mode reference ~~~~~~~~~~~~~~~~~~~~~ @@ -2023,8 +2063,8 @@ accept the ``name`` property. Moreover, all workshops and furnaces accept the ``max_general_orders`` property, which sets the maximum number of general workorders that the building can accept, and the ``take_from`` and ``give_to`` properties, which are -comma-separated lists of names (the same as the correponding stockpile -properties above). +comma-separated lists of names or building ids (the same as the correponding +stockpile properties above). ================= ============================= ========== Symbol Type Properties @@ -2141,7 +2181,8 @@ Symbol Type Properties route stop on this track stop and make it take from the given comma-separated list of - stockpile names. ``route``: add + stockpile names or stockpile + building ids. ``route``: add this route stop to the named route. if no route of this name exists, it will be created. If @@ -2192,3 +2233,25 @@ Symbol Type Properties ``trackrampSEW`` track ramp tee to the S, E, W ``trackrampNSEW`` track ramp cross ================= ============================= ========== + +#burrow mode reference +~~~~~~~~~~~~~~~~~~~~~~ + +====== ======= ========== +Symbol Meaning Properties +====== ======= ========== +``a`` add ``name``: if set, will add to an existing burrow of this name. + ``create``: if set to ``true``, will create a burrow with the + specified ``name`` if it doesn't already exist. + ``civalert``: if set to ``true``, will register this burrow with + `gui/civ-alert` if no burrow has already been registered. + ``autochop_clear``: if set to ``true``, register the burrow with + `autochop` so that all trees in the burrow are immediately + chopped down, regardless of how many logs are in stock. + ``autochop_chop``: if set to ``true``, register the burrow with + ``autochop`` so that woodcutting activity is constrained to this + burrow (and others marked for ``chop``). +``e`` erase ``name``: if set, will only affect the first burrow of the given + name. if not set, will affect all burrows that cover the given + tiles. +====== ======= ========== diff --git a/docs/images/gm-editor.png b/docs/images/gm-editor.png index 24546a710..117cb7031 100644 Binary files a/docs/images/gm-editor.png and b/docs/images/gm-editor.png differ diff --git a/docs/images/search-stockpile.png b/docs/images/search-stockpile.png deleted file mode 100644 index a0e837875..000000000 Binary files a/docs/images/search-stockpile.png and /dev/null differ diff --git a/docs/images/search.png b/docs/images/search.png deleted file mode 100644 index 384c3c533..000000000 Binary files a/docs/images/search.png and /dev/null differ diff --git a/docs/plugins/3dveins.rst b/docs/plugins/3dveins.rst index 3112934ab..96fbcf836 100644 --- a/docs/plugins/3dveins.rst +++ b/docs/plugins/3dveins.rst @@ -3,7 +3,7 @@ .. dfhack-tool:: :summary: Rewrite layer veins to expand in 3D space. - :tags: unavailable fort gameplay map + :tags: fort gameplay map Existing, flat veins are removed and new 3D veins that naturally span z-levels are generated in their place. The transformation preserves the mineral counts diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index 7e5b6b024..4a0d64ae1 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -70,7 +70,7 @@ Usage ``autobutcher list_export`` Print commands required to set the current settings in another fort. -To see a list of all races, run this command: +To see a list of all races, run this command:: devel/query --table df.global.world.raws.creatures.all --search ^creature_id --maxdepth 1 diff --git a/docs/plugins/autogems.rst b/docs/plugins/autogems.rst index 6135b39d2..5beb7260e 100644 --- a/docs/plugins/autogems.rst +++ b/docs/plugins/autogems.rst @@ -3,7 +3,7 @@ autogems .. dfhack-tool:: :summary: Automatically cut rough gems. - :tags: unavailable fort auto workorders + :tags: unavailable :no-command: .. dfhack-command:: autogems-reload diff --git a/docs/plugins/building-hacks.rst b/docs/plugins/building-hacks.rst index b45b23be4..ae38cb3d9 100644 --- a/docs/plugins/building-hacks.rst +++ b/docs/plugins/building-hacks.rst @@ -3,7 +3,7 @@ building-hacks .. dfhack-tool:: :summary: Provides a Lua API for creating powered workshops. - :tags: unavailable fort gameplay buildings + :tags: unavailable :no-command: See `building-hacks-api` for more details. diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 9cc7e68a2..b1e77a80d 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -19,11 +19,11 @@ periodically scan for appropriate items and attach them to the planned building. Once all items are attached, the construction job will be unsuspended and a dwarf will come and build the building. If you have the `unsuspend` overlay enabled (it is enabled by default), then buildingplan-suspended -buildings will appear with a ``P`` marker on the main map, as opposed to the -usual ``x`` marker for "regular" suspended buildings. If you have -`suspendmanager` running, then buildings will be left suspended when their -items are all attached and ``suspendmanager`` will unsuspend them for -construction when it is safe to do so. +buildings will be tagged with a clock graphic in graphics mode or a ``P`` +marker in ASCII mode, as opposed to the ``x`` marker for "regular" suspended +buildings. If you have `suspendmanager` running, then buildings will be left +suspended when their items are all attached and ``suspendmanager`` will +unsuspend them for construction when it is safe to do so. If you want to impose restrictions on which items are chosen for the buildings, buildingplan has full support for quality and material filters (see `below @@ -45,10 +45,10 @@ from the buildingplan placement UI. One way to integrate buildingplan into your gameplay is to create manager workorders to ensure you always have a few blocks/doors/beds/etc. available. You -can then place as many of each building as you like. Produced items will be used -to build the planned buildings as they are produced, with minimal space -dedicated to stockpiles. The DFHack `orders` library can help with setting -these manager workorders up for you. +can then place as many of each building as you like. Items will be used to +build the planned buildings as they are produced, with minimal space dedicated +to stockpiles. The DFHack `orders` library can help with setting these manager +workorders up for you. If you don't want to use the ``buildingplan`` interface for the building you're currently trying to place, you can hit :kbd:`Alt`:kbd:`M` or click on the @@ -125,16 +125,17 @@ tiles selected in the construction area are not appropriate for building. For example, if you want to fill an area with flooring, you can select the entire area, and any tiles with existing buildings or walls will simply be skipped. -For weapon and spike traps, you can choose how many weapons will be included -on this panel. +Some building types will have other options available as well, such as a +selector for how many weapons you want in weapon traps or whether you want your +built cages to not have any occupants. Setting quality and material filters ++++++++++++++++++++++++++++++++++++ If you want to set restrictions on the items chosen to complete the planned -building, you can click on the "filter" button next to the item name or select -the item with the :kbd:`*` and :kbd:`/` keys and hit :kbd:`f` to bring up the -filter dialog. +building, you can click on the "[any material]" link next to the item name or +select the item with the :kbd:`q` or :kbd:`Q` keys and hit :kbd:`f` to bring up +the filter dialog. You can select whether the item must be decorated, and you can drag the ends of the "Item quality" slider to set your desired quality range. Note that blocks, @@ -147,32 +148,33 @@ You can click on specific materials to allow only items of those materials when building the current type of building. You can also allow or disallow entire categories of materials by clicking on the "Type" options on the left. Note that it is perfectly fine to choose materials that currently show zero quantity. -`buildingplan` will patiently watch for items made of materials you have -selected. +`buildingplan` will patiently wait for items made of materials you have +selected to become available. Choosing specific items +++++++++++++++++++++++ -If you want to choose specific items, click on the "Choose from items" toggle -or hit :kbd:`i` before placing the building. When you click to place the -building, a dialog will come up that allows you choose which items to use. The -list is sorted by most recently used materials for that building type by -default, but you can change to sort by name or by available quantity by -clicking on the "Sort by" selector or hitting :kbd:`R`. The configuration for -whether you would like to choose specific items is saved per building type and -will be restored when you plan more of that building type. +If you want to choose specific items instead of using the filters, click on the +"Choose items" selector or hit :kbd:`z` before placing the building. You can +choose to be prompted for every item ("Manually") or you can have it +automatically select the type of item that you last chose for this building +type. The list you are prompted with is sorted by most recently used materials +for that building type by default, but you can change to sort by name or by +available quantity by clicking on the "Sort by" selector or hitting :kbd:`R`. +The configuration for whether you would like to choose specific items is saved +per building type and will be restored when you plan more of that building type. You can select the maximum quantity of a specified item by clicking on the item name or selecting it with the arrow keys and hitting :kbd:`Enter`. You can instead select items one at a time by Ctrl-clicking (:kbd:`Shift`:kbd:`Right`) to increment or Ctrl-Shift-clicking (:kbd:`Shift`:kbd:`Left`) to decrement. -Once you are satisfied with your choices, click on the "Confirm" button or hit +Once you are satisfied with your choices, click on the large green button or hit :kbd:`C` to continue building. Note that you don't have to select all the items that the building needs. Any remaining items will be automatically chosen from -other available items (or future items if not all items are available yet). If -there are multiple item types to choose for the current building, one dialog -will appear per item type. +other available items (or from items produced in the future if not all items +are available yet). If there are multiple item types to choose for the current +building, one dialog will appear per item type. Building status --------------- @@ -180,10 +182,18 @@ Building status When viewing a planned building, a separate `overlay` widget appears on the building info sheet, showing you which items have been attached and which items are still pending. For a pending item, you can see its position in the -fulfillment queue. If there is a particular building that you need built ASAP, +fulfillment queue. You need to manufacture these items for them to be attached +to the building. If there is a particular building that you need built ASAP, you can click on the "make top priority" button (or hit :kbd:`Ctrl`:kbd:`T`) to bump the items for this building to the front of their respective queues. Note that each item type and filter configuration has its own queue, so even if an item is in queue position 1, there may be other queues that snag the needed item first. + +Lever linking +------------- + +When linking levers, `buildingplan` extends the vanilla panel by offering +control over which mechanisms are chosen for installation at the lever and at +the target. Heat safety filters are provided for convenience. diff --git a/docs/plugins/burrow.rst b/docs/plugins/burrow.rst new file mode 100644 index 000000000..f84f02989 --- /dev/null +++ b/docs/plugins/burrow.rst @@ -0,0 +1,115 @@ +burrow +====== + +.. dfhack-tool:: + :summary: Quickly adjust burrow tiles and units. + :tags: fort auto design productivity units + +This tool has two modes. When enabled, it monitors burrows with names that end +in ``+``. If a wall at the edge of such a burrow is dug out, the burrow will be +automatically extended to include the newly-revealed adjacent walls. If a miner +digs into an open space, such as a cavern, the open space will *not* be +included in the burrow. + +When run as a command, it can quickly adjust which tiles and/or units are +associated with the burrow. + +Usage +----- + +:: + + enable burrow + burrow tiles|units clear [ ...] [] + burrow tiles|units set|add|remove [...] [] + burrow tiles box-add|box-remove [] [] [] + burrow tiles flood-add|flood-remove [] + +The burrows can be referenced by name or by the internal numeric burrow ID. If +referenced by name, the first burrow that matches the name (case sensitive) +will be targeted. If a burrow name ends in ``+`` (to indicate that it should be +auto-expanded), the final ``+`` does not need to be specified on the +commandline. + +For ``set``, ``add``, or ``remove`` commands, instead of a burrow, you can +specify one of the following all-caps keywords: + +- ``ABOVE_GROUND`` +- ``SUBTERRANEAN`` +- ``INSIDE`` +- ``OUTSIDE`` +- ``LIGHT`` +- ``DARK`` +- ``HIDDEN`` +- ``REVEALED`` + +to add or remove tiles with the corresponding properties. + +Flood fill selects tiles spreading out from a starting tile if they: + +- match the inside/outside and hidden/revealed properties of the starting tile +- match the walkability group of the starting tile OR (if the starting tile is + walkable) is adjacent to a tile with the same walkability group as the + starting tile + +When flood adding, the flood fill will also stop at any tiles that have already +been added to the burrow. Similarly for flood removing, the flood will also +stop at tiles that are not in the burrow. + +Examples +-------- + +``enable burrow`` + Start monitoring burrows that have names ending in '+' and automatically + expand them when walls that border the burrows are dug out. +``burrow tiles clear Safety`` + Remove all tiles from the burrow named ``Safety`` (in preparation for + adding new tiles elsewhere, presumably). +``burrow units clear Farmhouse Workshops`` + Remove all units from the burrows named ``Farmhouse`` and ``Workshops``. +``multicmd burrow tiles set Inside INSIDE; burrow tiles remove Inside HIDDEN`` + Reset the burrow named ``Inside`` to include all the currently revealed, + interior tiles. +``burrow units set "Core Fort" Peasants Skilled`` + Clear all units from the burrow named ``Core Fort``, then add units + currently assigned to the ``Peasants`` and ``Skilled`` burrows. +``burrow tiles box-add Safety 0,0,0`` + Add all tiles to the burrow named ``Safety`` that are within the volume of + the box starting at coordinate 0, 0, 0 (the upper left corner of the bottom + level) and ending at the current location of the keyboard cursor. +``burrow tiles flood-add Safety --cur-zlevel`` + Flood-add the tiles on the current z-level with the same properties as the + tile under the keyboard cursor to the burrow named ``Safety``. + +Options +------- + +``-c``, ``--cursor `` + Indicate the starting position of the box or flood fill. If not specified, + the position of the keyboard cursor is used. +``-z``, ``--cur-zlevel`` + Restricts a flood fill operation to the currently visible z-level. + +Note +---- + +If you are auto-expanding a burrow (whose name ends in a ``+``) and the miner +who is digging to expand the burrow is assigned to that burrow, then 1-wide +corridors that expand the burrow will have very slow progress. This is because +the burrow is expanded to include the next dig job only after the miner has +chosen a next tile to dig, which may be far away. 2-wide cooridors are much +more efficient when expanding a burrow since the "next" tile to dig will still +be nearby. + +Overlay +------- + +When painting burrows in the vanilla UI, a few extra mouse operations are +supported. If you box select across multiple z-levels, you will be able to +select the entire volume instead of just the selected area on the z-level that +you are currently looking at. + +In addition, double-clicking will start a flood fill from the target tile. + +The box and flood fill actions respect the UI setting for whether the burrow is +being added to or erased. diff --git a/docs/plugins/burrows.rst b/docs/plugins/burrows.rst deleted file mode 100644 index 299656cf5..000000000 --- a/docs/plugins/burrows.rst +++ /dev/null @@ -1,54 +0,0 @@ -burrows -======= - -.. dfhack-tool:: - :summary: Auto-expand burrows as you dig. - :tags: unavailable fort auto design productivity map units - :no-command: - -.. dfhack-command:: burrow - :summary: Quickly add units/tiles to burrows. - -When a wall inside a burrow with a name ending in ``+`` is dug out, the burrow -will be extended to newly-revealed adjacent walls. - -Usage ------ - -``burrow enable auto-grow`` - When a wall inside a burrow with a name ending in '+' is dug out, the burrow - will be extended to newly-revealed adjacent walls. This final '+' may be - omitted in burrow name args of other ``burrow`` commands. Note that digging - 1-wide corridors with the miner inside the burrow is SLOW. -``burrow disable auto-grow`` - Disables auto-grow processing. -``burrow clear-unit [ ...]`` - Remove all units from the named burrows. -``burrow clear-tiles [ ...]`` - Remove all tiles from the named burrows. -``burrow set-units target-burrow [ ...]`` - Clear all units from the target burrow, then add units from the named source - burrows. -``burrow add-units target-burrow [ ...]`` - Add units from the source burrows to the target. -``burrow remove-units target-burrow [ ...]`` - Remove units in source burrows from the target. -``burrow set-tiles target-burrow [ ...]`` - Clear target burrow tiles and add tiles from the names source burrows. -``burrow add-tiles target-burrow [ ...]`` - Add tiles from the source burrows to the target. -``burrow remove-tiles target-burrow [ ...]`` - Remove tiles in source burrows from the target. - -In place of a source burrow, you can use one of the following keywords: - -- ``ABOVE_GROUND`` -- ``SUBTERRANEAN`` -- ``INSIDE`` -- ``OUTSIDE`` -- ``LIGHT`` -- ``DARK`` -- ``HIDDEN`` -- ``REVEALED`` - -to add tiles with the given properties. diff --git a/docs/plugins/design.rst b/docs/plugins/design.rst new file mode 100644 index 000000000..3cb48343c --- /dev/null +++ b/docs/plugins/design.rst @@ -0,0 +1,10 @@ +design +====== + +.. dfhack-tool:: + :summary: Draws designations in shapes. + :tags: fort design dev map + :no-command: + +This plugin provides a Lua API, but no direct commands. See `gui/design` for +the user interface. diff --git a/docs/plugins/dig.rst b/docs/plugins/dig.rst index a10db97df..2055a7572 100644 --- a/docs/plugins/dig.rst +++ b/docs/plugins/dig.rst @@ -50,7 +50,7 @@ Usage Designate circles. The diameter is the number of tiles across the center of the circle that you want to dig. See the `digcircle`_ section below for options. -``digtype [] [-p] [-z]`` +``digtype [] [-p] [--zup|-u] [--zdown|-zu] [--cur-zlevel|-z] [--hidden|-h] [--no-auto|-a]`` Designate all vein tiles of the same type as the selected tile. See the `digtype`_ section below for options. ``digexp [] [] [-p]`` @@ -119,9 +119,11 @@ the last selected parameters. digtype ------- -For every tile on the map of the same vein type as the selected tile, this -command designates it to have the same designation as the selected tile. If the -selected tile has no designation, they will be dig designated. +For every tile on the map of the same vein type as the selected tile, this command +designates it to have the same designation as the selected tile. If the selected +tile has no designation, they will be dig designated. By default, only designates +visible tiles, and in the case of dig designation, applies automatic mining to them +(designates uncovered neighbouring tiles of the same type to be dug). If an argument is given, the designation of the selected tile is ignored, and all appropriate tiles are set to the specified designation. @@ -143,9 +145,18 @@ Designation options: ``clear`` Clear any designations. -You can also pass a ``-z`` option, which restricts designations to the current -z-level and down. This is useful when you don't want to designate tiles on the -same z-levels as your carefully dug fort above. +Other options: + +``-d``, ``--zdown`` + Only designates tiles on the cursor's z-level and below. +``-u``, ``--zup`` + Only designates tiles on the cursor's z-level and above. +``-z``, ``--cur-zlevel`` + Only designates tiles on the same z-level as the cursor. +``-h``, ``--hidden`` + Allows designation of hidden tiles, and picking a hidden tile as the target type. +``-a``, ``--no-auto`` + No automatic mining mode designation - useful if you want to avoid dwarves digging where you don't want them. digexp ------ @@ -179,3 +190,23 @@ Filters: Take current designation and apply the selected pattern to it. After you have a pattern set, you can use ``expdig`` to apply it again. + +Overlay +------- + +This tool also provides two overlays that are managed by the `overlay` +framework. Both have no effect when in graphics mode, but when in ASCII mode, +they display useful highlights that are otherwise missing from the ASCII mode +interface. + +The ``dig.asciiwarmdamp`` overlay highlights warm tiles red and damp tiles in +blue. Box selection characters and the keyboard cursor will also +change color as appropriate when over the warm or damp tile. + +The ``dig.asciicarve`` overlay highlights tiles that are designated for +smoothing, engraving, track carving, or fortification carving. The designations +blink so you can still see what is underneath them. + +Note that due to the limitations of the ASCII mode screen buffer, the +designation highlights may show through other interface elements that overlap +the designated area. diff --git a/docs/plugins/digFlood.rst b/docs/plugins/digFlood.rst index ee502371a..e98b65382 100644 --- a/docs/plugins/digFlood.rst +++ b/docs/plugins/digFlood.rst @@ -3,7 +3,7 @@ digFlood .. dfhack-tool:: :summary: Digs out veins as they are discovered. - :tags: unavailable fort auto map + :tags: unavailable Once you register specific vein types, this tool will automatically designate tiles of those types of veins for digging as your miners complete adjacent diff --git a/docs/plugins/diggingInvaders.rst b/docs/plugins/diggingInvaders.rst index 001636709..46db313c6 100644 --- a/docs/plugins/diggingInvaders.rst +++ b/docs/plugins/diggingInvaders.rst @@ -3,7 +3,7 @@ diggingInvaders .. dfhack-tool:: :summary: Invaders dig and destroy to get to your dwarves. - :tags: unavailable fort gameplay military units + :tags: unavailable Usage ----- diff --git a/docs/plugins/dwarfmonitor.rst b/docs/plugins/dwarfmonitor.rst index 45a5d8f85..cd8867ee5 100644 --- a/docs/plugins/dwarfmonitor.rst +++ b/docs/plugins/dwarfmonitor.rst @@ -3,7 +3,7 @@ dwarfmonitor .. dfhack-tool:: :summary: Report on dwarf preferences and efficiency. - :tags: unavailable fort inspection jobs units + :tags: unavailable It can also show heads-up display widgets with live fort statistics. diff --git a/docs/plugins/dwarfvet.rst b/docs/plugins/dwarfvet.rst index b4dfe0ada..12d77fef8 100644 --- a/docs/plugins/dwarfvet.rst +++ b/docs/plugins/dwarfvet.rst @@ -2,22 +2,34 @@ dwarfvet ======== .. dfhack-tool:: - :summary: Allows animals to be treated at animal hospitals. - :tags: unavailable fort gameplay animals + :summary: Allow animals to be treated at hospitals. + :tags: fort gameplay animals Annoyed that your dragons become useless after a minor injury? Well, with -dwarfvet, injured animals will be treated at an animal hospital, which is simply -a hospital that is also an animal training zone. Dwarfs with the Animal -Caretaker labor enabled will come to the hospital to treat animals. Normal +dwarfvet, injured animals will be treated at a hospital. Dwarfs with the Animal +Caretaker labor enabled will come to the hospital to treat the animals. Normal medical skills are used (and trained), but no experience is given to the Animal Caretaker skill itself. +You can enable ``dwarfvet`` in `gui/control-panel`, and you can choose to start +``dwarfvet`` automatically in new forts in the ``Autostart`` tab. + Usage ----- -``enable dwarfvet`` - Enables the plugin. -``dwarfvet report`` - Reports all zones that the game considers animal hospitals. -``dwarfvet report-usage`` - Reports on animals currently being treated. +:: + + enable dwarfvet + dwarfvet [status] + dwarfvet now + +Examples +-------- + +``dwarfvet`` + Report on how many animals are being treated and how many are in need of + treatment. + +``dwarfvet now`` + Assign injured animals to a free floor spot in a nearby hospital, + regardless of whether the plugin is enabled. diff --git a/docs/plugins/embark-assistant.rst b/docs/plugins/embark-assistant.rst index 282d4b122..88c4ffccd 100644 --- a/docs/plugins/embark-assistant.rst +++ b/docs/plugins/embark-assistant.rst @@ -3,7 +3,7 @@ embark-assistant .. dfhack-tool:: :summary: Embark site selection support. - :tags: unavailable embark fort interface + :tags: unavailable Run this command while the pre-embark screen is displayed to show extended (and reasonably correct) resource information for the embark rectangle as well as diff --git a/docs/plugins/embark-tools.rst b/docs/plugins/embark-tools.rst index f320706ff..37a7bf43e 100644 --- a/docs/plugins/embark-tools.rst +++ b/docs/plugins/embark-tools.rst @@ -3,7 +3,7 @@ embark-tools .. dfhack-tool:: :summary: Extend the embark screen functionality. - + :tags: unavailable Usage ----- diff --git a/docs/plugins/fix-unit-occupancy.rst b/docs/plugins/fix-unit-occupancy.rst index 6ba01b712..5bf504fdc 100644 --- a/docs/plugins/fix-unit-occupancy.rst +++ b/docs/plugins/fix-unit-occupancy.rst @@ -3,7 +3,7 @@ fix-unit-occupancy .. dfhack-tool:: :summary: Fix phantom unit occupancy issues. - :tags: unavailable fort bugfix map + :tags: unavailable If you see "unit blocking tile" messages that you can't account for (:bug:`3499`), this tool can help. diff --git a/docs/plugins/fixveins.rst b/docs/plugins/fixveins.rst index 552c5f8c3..49b2046b6 100644 --- a/docs/plugins/fixveins.rst +++ b/docs/plugins/fixveins.rst @@ -3,7 +3,7 @@ fixveins .. dfhack-tool:: :summary: Restore missing mineral inclusions. - :tags: unavailable fort bugfix map + :tags: unavailable This tool can also remove invalid references to mineral inclusions if you broke your embark with tools like `tiletypes`. diff --git a/docs/plugins/follow.rst b/docs/plugins/follow.rst index e72f79ce0..21b3411a9 100644 --- a/docs/plugins/follow.rst +++ b/docs/plugins/follow.rst @@ -3,7 +3,7 @@ follow .. dfhack-tool:: :summary: Make the screen follow the selected unit. - :tags: unavailable fort interface units + :tags: unavailable Once you exit from the current menu or cursor mode, the screen will stay centered on the unit. Handy for watching dwarves running around. Deactivated by diff --git a/docs/plugins/forceequip.rst b/docs/plugins/forceequip.rst index 2565d12c3..18661195a 100644 --- a/docs/plugins/forceequip.rst +++ b/docs/plugins/forceequip.rst @@ -3,7 +3,7 @@ forceequip .. dfhack-tool:: :summary: Move items into a unit's inventory. - :tags: unavailable adventure fort animals items military units + :tags: unavailable This tool is typically used to equip specific clothing/armor items onto a dwarf, but can also be used to put armor onto a war animal or to add unusual items diff --git a/docs/plugins/generated-creature-renamer.rst b/docs/plugins/generated-creature-renamer.rst index ea386eacf..fc2734d07 100644 --- a/docs/plugins/generated-creature-renamer.rst +++ b/docs/plugins/generated-creature-renamer.rst @@ -3,7 +3,7 @@ generated-creature-renamer .. dfhack-tool:: :summary: Automatically renames generated creatures. - :tags: unavailable adventure fort legends units + :tags: unavailable :no-command: .. dfhack-command:: list-generated diff --git a/docs/plugins/hotkeys.rst b/docs/plugins/hotkeys.rst index 6b2ef3f0c..561a34382 100644 --- a/docs/plugins/hotkeys.rst +++ b/docs/plugins/hotkeys.rst @@ -19,14 +19,17 @@ Usage Menu overlay widget ------------------- -The in-game hotkeys menu is registered with the `overlay` framework and can be -enabled as a hotspot in the upper-left corner of the screen. You can bring up -the menu by hovering the mouse cursor over the hotspot and can select a command -to run from the list by clicking on it with the mouse or by using the keyboard -to select a command with the arrow keys and hitting :kbd:`Enter`. +The in-game hotkeys menu is registered with the `overlay` framework and appears +as a DFHack logo in the upper-left corner of the screen. You can bring up the +menu by clicking on the logo or by hitting the global :kbd:`Ctrl`:kbd:`Shift`:kbd:`c` hotkey. You can select a command to run from +the list by clicking on it with the mouse or by using the keyboard to select a +command with the arrow keys and hitting :kbd:`Enter`. + +The menu closes automatically when an action is taken or when you click or +right click anywhere else on the screen. A short description of the command will appear in a nearby textbox. If you'd like to see the full help text for the command or edit the command before -running, you can open it for editing in `gui/launcher` by right clicking on the +running, you can open it for editing in `gui/launcher` by shift clicking on the command, left clicking on the arrow to the left of the command, or by pressing the right arrow key while the command is selected. diff --git a/docs/plugins/infiniteSky.rst b/docs/plugins/infiniteSky.rst index 789709c9f..0c14804af 100644 --- a/docs/plugins/infiniteSky.rst +++ b/docs/plugins/infiniteSky.rst @@ -3,7 +3,7 @@ infiniteSky .. dfhack-tool:: :summary: Automatically allocate new z-levels of sky - :tags: unavailable fort auto design map + :tags: unavailable If enabled, this plugin will automatically allocate new z-levels of sky at the top of the map as you build up. Or it can allocate one or many additional levels diff --git a/docs/plugins/isoworldremote.rst b/docs/plugins/isoworldremote.rst index b792cb649..82748e36e 100644 --- a/docs/plugins/isoworldremote.rst +++ b/docs/plugins/isoworldremote.rst @@ -3,7 +3,7 @@ isoworldremote .. dfhack-tool:: :summary: Provides a remote API used by Isoworld. - :tags: unavailable dev graphics + :tags: unavailable :no-command: See `remote` for related remote APIs. diff --git a/docs/plugins/jobutils.rst b/docs/plugins/jobutils.rst index 674c6897a..5e8125585 100644 --- a/docs/plugins/jobutils.rst +++ b/docs/plugins/jobutils.rst @@ -5,7 +5,7 @@ jobutils .. dfhack-tool:: :summary: Provides commands for interacting with jobs. - :tags: unavailable fort inspection jobs + :tags: unavailable :no-command: .. dfhack-command:: job diff --git a/docs/plugins/labormanager.rst b/docs/plugins/labormanager.rst index 731f706b8..255de05c4 100644 --- a/docs/plugins/labormanager.rst +++ b/docs/plugins/labormanager.rst @@ -3,7 +3,7 @@ labormanager .. dfhack-tool:: :summary: Automatically manage dwarf labors. - :tags: unavailable fort auto labors + :tags: unavailable Labormanager is derived from `autolabor` but uses a completely different approach to assigning jobs to dwarves. While autolabor tries to keep as many diff --git a/docs/plugins/logistics.rst b/docs/plugins/logistics.rst index dff142390..d9aeee067 100644 --- a/docs/plugins/logistics.rst +++ b/docs/plugins/logistics.rst @@ -17,6 +17,9 @@ For autotrade, items will be marked for trading only when a caravan is approaching or is already at the trade depot. Items (or bins that contain items) of which a noble has forbidden export will not be marked for trade. +Stockpiles can be registered for ``logistics`` features by toggling the options +in the `stockpiles` overlay that comes up when you select a stockpile in the UI. + Usage ----- @@ -69,3 +72,7 @@ Options Causes the command to act upon stockpiles with the given names or numbers instead of the stockpile that is currently selected in the UI. Note that the numbers are the stockpile numbers, not the building ids. +``-m``, ``--melt-masterworks`` + If specified with a ``logistics add melt`` command, will configure the + stockpile to allow melting of masterworks. By default, masterworks are not + marked for melting, even if they are in an automelt stockpile. diff --git a/docs/plugins/manipulator.rst b/docs/plugins/manipulator.rst index 734800e77..c1268bda5 100644 --- a/docs/plugins/manipulator.rst +++ b/docs/plugins/manipulator.rst @@ -3,7 +3,7 @@ manipulator .. dfhack-tool:: :summary: An in-game labor management interface. - :tags: unavailable fort productivity labors + :tags: unavailable :no-command: It is equivalent to the popular Dwarf Therapist utility. diff --git a/docs/plugins/map-render.rst b/docs/plugins/map-render.rst index 4f3c6ba72..5dc07c45e 100644 --- a/docs/plugins/map-render.rst +++ b/docs/plugins/map-render.rst @@ -3,7 +3,7 @@ map-render .. dfhack-tool:: :summary: Provides a Lua API for re-rendering portions of the map. - :tags: unavailable dev graphics + :tags: unavailable :no-command: See `map-render-api` for details. diff --git a/docs/plugins/misery.rst b/docs/plugins/misery.rst index 8a65d8419..3bcd5588a 100644 --- a/docs/plugins/misery.rst +++ b/docs/plugins/misery.rst @@ -2,11 +2,12 @@ misery ====== .. dfhack-tool:: - :summary: Increase the intensity of your citizens' negative thoughts. + :summary: Make citizens more miserable. :tags: fort gameplay units -When enabled, negative thoughts that your citizens have will multiply by the -specified factor. This makes it more challenging to keep them happy. +When enabled, all of your citizens receive a negative thought about a +particularly nasty soapy bath. You can vary the strength of this negative +thought to increase or decrease the difficulty of keeping your citizens happy. Usage ----- @@ -18,18 +19,18 @@ Usage misery misery clear -The default misery factor is ``2``, meaning that your dwarves will become -miserable twice as fast. +The default misery factor is ``2``, which will result in a moderate hit to your +dwarves' happiness. Larger numbers increase the challenge. Examples -------- ``enable misery`` - Start multiplying bad thoughts for your citizens! + Start adding bad thoughts about nasty soapy baths to your citizens! ``misery 5`` - Make dwarves become unhappy 5 times faster than normal -- this is quite - challenging to handle! + Change the strength of the soapy bath negative thought to something quite + large -- this is very challenging to handle! ``misery clear`` Clear away negative thoughts added by ``misery``. Note that this will not diff --git a/docs/plugins/mode.rst b/docs/plugins/mode.rst index 061e3a548..35964b783 100644 --- a/docs/plugins/mode.rst +++ b/docs/plugins/mode.rst @@ -3,7 +3,7 @@ mode .. dfhack-tool:: :summary: See and change the game mode. - :tags: unavailable armok dev gameplay + :tags: unavailable .. warning:: diff --git a/docs/plugins/mousequery.rst b/docs/plugins/mousequery.rst index bff110f0e..38e9266e3 100644 --- a/docs/plugins/mousequery.rst +++ b/docs/plugins/mousequery.rst @@ -3,7 +3,7 @@ mousequery .. dfhack-tool:: :summary: Adds mouse controls to the DF interface. - :tags: unavailable fort productivity interface + :tags: unavailable Adds mouse controls to the DF interface. For example, with ``mousequery`` you can click on buildings to configure them, hold the mouse button to draw dig diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 46a004081..32708b4c8 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -17,6 +17,14 @@ Usage manager orders. It will not clear the orders that already exist. ``orders clear`` Deletes all manager orders in the current embark. +``orders recheck [this]`` + Sets the status to ``Checking`` (from ``Active``) for all work orders that + have conditions that can be re-checked. If the "this" option is passed, + only sets the status for the workorder whose condition details page is + open. This makes the manager reevaluate its conditions. This is especially + useful for an order that had its conditions met when it was started, but + the requisite items have since disappeared and the workorder is now + generating job cancellation spam. ``orders sort`` Sorts current manager orders by repeat frequency so repeating orders don't prevent one-time orders from ever being completed. The sorting order is: diff --git a/docs/plugins/petcapRemover.rst b/docs/plugins/petcapRemover.rst index 4f6ea4160..2aef993d0 100644 --- a/docs/plugins/petcapRemover.rst +++ b/docs/plugins/petcapRemover.rst @@ -3,7 +3,7 @@ petcapRemover .. dfhack-tool:: :summary: Modify the pet population cap. - :tags: unavailable fort auto animals + :tags: unavailable In vanilla DF, pets will not reproduce unless the population is below 50 and the number of children of that species is below a certain percentage. This plugin diff --git a/docs/plugins/plants.rst b/docs/plugins/plants.rst index 281b295cf..d35579e1d 100644 --- a/docs/plugins/plants.rst +++ b/docs/plugins/plants.rst @@ -5,7 +5,7 @@ plants .. dfhack-tool:: :summary: Provides commands that interact with plants. - :tags: unavailable adventure fort armok map plants + :tags: unavailable :no-command: .. dfhack-command:: plant diff --git a/docs/plugins/power-meter.rst b/docs/plugins/power-meter.rst index f3a76c60a..d6d4c8272 100644 --- a/docs/plugins/power-meter.rst +++ b/docs/plugins/power-meter.rst @@ -3,7 +3,7 @@ power-meter .. dfhack-tool:: :summary: Allow pressure plates to measure power. - :tags: unavailable fort gameplay buildings + :tags: unavailable :no-command: If you run `gui/power-meter` while building a pressure plate, the pressure diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst new file mode 100644 index 000000000..2f01162c6 --- /dev/null +++ b/docs/plugins/preserve-tombs.rst @@ -0,0 +1,23 @@ +preserve-tombs +============== + +.. dfhack-tool:: + :summary: Preserve tomb assignments when assigned units die. + :tags: fort bugfix + +If you find that the tombs you assign to units get unassigned from them when +they die (e.g. your nobles), this tool can help fix that. + +Usage +----- + +``enable preserve-tombs`` + enable the plugin +``preserve-tombs [status]`` + check the status of the plugin, and if the plugin is enabled, + lists all currently tracked tomb assignments +``preserve-tombs now`` + forces an immediate update of the tomb assignments. This plugin + automatically updates the tomb assignments once every 100 ticks. + +This tool runs in the background. diff --git a/docs/plugins/prospector.rst b/docs/plugins/prospector.rst index 4628d11c8..68ec3d9b1 100644 --- a/docs/plugins/prospector.rst +++ b/docs/plugins/prospector.rst @@ -11,8 +11,8 @@ prospector .. dfhack-command:: prospect :summary: Shows a summary of resources that exist on the map. -It can also calculate an estimate of resources available in the selected embark -area. +It can also calculate an estimate of resources available in the currently +highlighted embark area. Usage ----- diff --git a/docs/plugins/rename.rst b/docs/plugins/rename.rst index 903a1a1d4..a002fc4e4 100644 --- a/docs/plugins/rename.rst +++ b/docs/plugins/rename.rst @@ -3,7 +3,7 @@ rename .. dfhack-tool:: :summary: Easily rename things. - :tags: unavailable adventure fort productivity buildings stockpiles units + :tags: unavailable Use `gui/rename` for an in-game interface. diff --git a/docs/plugins/rendermax.rst b/docs/plugins/rendermax.rst index 91faa6f5c..d09014cb2 100644 --- a/docs/plugins/rendermax.rst +++ b/docs/plugins/rendermax.rst @@ -3,7 +3,7 @@ rendermax .. dfhack-tool:: :summary: Modify the map lighting. - :tags: unavailable adventure fort gameplay graphics + :tags: unavailable This plugin provides a collection of OpenGL lighting filters that affect how the map is drawn to the screen. diff --git a/docs/plugins/search.rst b/docs/plugins/search.rst deleted file mode 100644 index c1493f992..000000000 --- a/docs/plugins/search.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. _search-plugin: - -search -====== - -.. dfhack-tool:: - :summary: Adds search capabilities to the UI. - :tags: unavailable fort productivity interface - :no-command: - -Search options are added to the Stocks, Animals, Trading, Stockpile, Noble -assignment candidates), Military (position candidates), Burrows (unit list), -Rooms, Announcements, Job List, and Unit List screens all get hotkeys that allow -you to dynamically filter the displayed lists. - -Usage ------ - -:: - - enable search - -.. image:: ../images/search.png - -Searching works the same way as the search option in :guilabel:`Move to Depot`. -You will see the Search option displayed on screen with a hotkey -(usually :kbd:`s`). Pressing it lets you start typing a query and the relevant -list will start filtering automatically. - -Pressing :kbd:`Enter`, :kbd:`Esc` or the arrow keys will return you to browsing -the now filtered list, which still functions as normal. You can clear the filter -by either going back into search mode and backspacing to delete it, or pressing -the "shifted" version of the search hotkey while browsing the list (e.g. if the -hotkey is :kbd:`s`, then hitting :kbd:`Shift`:kbd:`s` will clear any filter). - -Leaving any screen automatically clears the filter. - -In the Trade screen, the actual trade will always only act on items that are -actually visible in the list; the same effect applies to the Trade Value numbers -displayed by the screen. Because of this, the :kbd:`t` key is blocked while -search is active, so you have to reset the filters first. Pressing -:kbd:`Alt`:kbd:`C` will clear both search strings. - -In the stockpile screen the option only appears if the cursor is in the -rightmost list: - -.. image:: ../images/search-stockpile.png - -Note that the 'Permit XXX'/'Forbid XXX' keys conveniently operate only on items -actually shown in the rightmost list, so it is possible to select only fat or -tallow by forbidding fats, then searching for fat/tallow, and using Permit Fats -again while the list is filtered. diff --git a/docs/plugins/siege-engine.rst b/docs/plugins/siege-engine.rst index 57693fa11..4707ad5e8 100644 --- a/docs/plugins/siege-engine.rst +++ b/docs/plugins/siege-engine.rst @@ -3,7 +3,7 @@ siege-engine .. dfhack-tool:: :summary: Extend the functionality and usability of siege engines. - :tags: unavailable fort gameplay buildings + :tags: unavailable :no-command: Siege engines in DF haven't been updated since the game was 2D, and can only aim diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 067e188fd..eb3e20663 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -2,59 +2,159 @@ sort ==== .. dfhack-tool:: - :summary: Sort lists shown in the DF interface. - :tags: unavailable fort productivity interface + :summary: Search and sort lists shown in the DF interface. + :tags: fort productivity interface :no-command: -.. dfhack-command:: sort-items - :summary: Sort the visible item list. +The ``sort`` tool provides search and sort functionality for lists displayed in +the DF interface. -.. dfhack-command:: sort-units - :summary: Sort the visible unit list. +Searching and sorting functionality is provided by `overlay` widgets, and +widgets for individual lists can be moved via `gui/overlay` or turned on or off +via `gui/control-panel`. -Usage ------ +Squad assignment overlay +------------------------ -:: +You can search for a dwarf by name by typing in the Search field. The search +field is always focused, so any lowercase letter you type will appear there. - sort-items [ ...] - sort-units [ ...] +The squad assignment screen can be sorted by name, by arrival order, by stress, +by various military-related skills, or by long-term military potential. -Both commands sort the visible list using the given sequence of comparisons. -Each property can be prefixed with a ``<`` or ``>`` character to indicate -whether elements that don't have the given property defined go first or last -(respectively) in the sorted list. +If sorted by "melee effectiveness" (the default), then the citizens are sorted +according to how well they will perform in battle when using the weapon they +have the most skill in. The effectiveness rating also takes into account +physical and mental attributes as well as general fighting (non-weapon) skills. -Examples --------- +The "ranged effectiveness" sort order does a similar sort for expected +effectiveness with a crossbow. This sort also takes into account relevant +physical and mental attributes. -``sort-items material type quality`` - Sort a list of items by material, then by type, then by quality -``sort-units profession name`` - Sort a list of units by profession, then by name +The "effectiveness" sorts are the ones you should be using if you need the best +squad you can make right now. The numbers to the left of the unit list indicate +exactly how effective that dwarf is expected to be. Light green numbers +indicate the best of the best, while red numbers indicate dwarves that will not +be effective in the military in their current state (though see "melee +potential" and "ranged potential" sorts below for predictions about future +effectiveness). -Properties ----------- +The "arrival order" sorts your citizens according to the most recent time they +entered your map. The numbers on the left indicate the relative arrival order, +and the numbers for the group of dwarves that most recently entered the map +will be at the top and be colored bright green. If you run this sort after you +get a new group of migrants, the migrant wave will be colored bright green. +Dwarves that arrived earlier will have numbers in yellow, and your original +dwarves (if any still survive and have never left and re-entered the map) will +have numbers in red. -Items can be sorted by the following properties: +The "stress" sort order will bring your most stressed dwarves to the top, ready +for addition to a :wiki:`therapy squad ` to +help improve their mood. -- ``type`` -- ``description`` -- ``base_quality`` -- ``quality`` -- ``improvement`` -- ``wear`` -- ``material`` +Similarly, sorting by "need for training" will show you the dwarves that are +feeling the most unfocused because they are having their military training +needs unmet. -Units can be sorted by the following properties: +Both "stress" and "need for training" sorts use the dwarf happiness indicators +to show how dire the dwarf's situation is and how much their mood might be +improved if you add them to an appropriate squad. -- ``name`` -- ``age`` -- ``arrival`` -- ``noble`` -- ``profession`` -- ``profession_class`` -- ``race`` -- ``squad`` -- ``squad_position`` -- ``happiness`` +If sorting is done by "melee potential", then citizens are arranged based on +genetic predispositions in physical and mental attributes, as well as body +size. Dwarves (and other humanoid creatures) with higher ratings are expected +to be more effective in melee combat if they train their attributes to their +genetic maximum. + +Similarly, the "ranged potential" sort orders citizens by genetic +predispositions in physical and mental attributes that are relevant to ranged +combat. Dwarves (and other humanoid creatures) with higher rating are expected +to be more effective in ranged combat if they train their attributes to the +maximum. + +The squad assignment panel also offers options for filtering which dwarves are +shown. Each filter option can by cycled through "Include", "Only", and +"Exclude" settings. "Include" does no filtering, "Only" shows only units that +match the filter, and "Exclude" shows only units that do *not* match the filter. + +The following filters are provided: + +- Units that are assigned to other squads +- Elected and appointed officials (e.g. mayor, priests, tavern keepers, etc.) +- Nobility (e.g. monarch, barons, counts, etc.) +- Mothers with infants (you may not want mothers using their babies as shields) +- Weak mental fortitude (units that have facets and values that indicate that + they will react poorly to the stresses of battle) +- Critically injured (units that have lost their ability to see, grasp weapons, + or walk) + +"Melee skill effectiveness", "ranged skill effectiveness", "melee combat potential" +and "ranged combat potential" are explained in detail here: +https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candidate_selection_part_3/ +"Mental stability" is explained here: +https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/ + +Info tabs overlay +----------------- + +The Info overlay adds search support to many of the fort-wide "Info" panels +(e.g. "Creatures", "Tasks", etc.). When searching for units, you can search by +name (with either English or native language last names), profession, or +special status (like "necromancer"). If there is text in the second column, you +can search for that text as well. This is often a job name or a status, like +"caged". The work animal assignment page can also filter by squad or burrow +membership. + +Work animals overlay +-------------------- + +In addition to the search and filter widgets provided by the Info tabs overlay, +the work animal assignment screen has an additional overlay that annotates each +visible unit with the number of work animals that unit already has. + +Interrogation overlay +--------------------- + +In the interrogation and conviction screens under the "Justice" tab, you can +search for units by name. You can also filter by the classification of the +unit. The classification groups are ordered by how likely a member of that +group is to be involved in a plot. The groups are: All, Risky visitors, Other +visitors, Residents, Citizens, Animals, Deceased, and Others. "Risky" visitors are those who are especially likely to be involved in plots, such as criminals, +necromancers, necromancer experiments, and intelligent undead. + +On the interrogations screen, you can also filter units by whether they have +already been interrogated. + +Candidates overlay +------------------ + +When you select the button to choose a candidate to assign to a noble role on +the nobles screen, you can search for units by name, profession, or any of the +skills in which they have achieved at least "novice" level. For example, when +assigning a broker, you can search for "appraisal" to find candidates that have +at least some appraisal skill. + +Location selection overlay +-------------------------- + +When choosing the type of guildhall or temple to dedicate, you can search for +the relevant profession, religion, or deity by name. For temples, you can also +search for the "spheres" associated with the deity or religion, such as +"wealth" or "lies". + +You can also choose whether to filter out temple or guildhall types that you +have already established. + +Slab engraving overlay +---------------------- + +When choosing a unit to engrave a slab for, you can search for units by name, +either in their native language or in English (though only their native name +will be displayed). This overlay also adds a filter for showing only units that +would need a slab in order to prevent them rising as a ghost. + +World overlay +------------- + +Searching is supported for the Artifacts list when viewing the world map (where +you can initiate raids). diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 95ce852ae..f54d68142 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -3,7 +3,7 @@ spectate .. dfhack-tool:: :summary: Automatically follow productive dwarves. - :tags: unavailable fort interface + :tags: fort interface Usage ----- diff --git a/docs/plugins/steam-engine.rst b/docs/plugins/steam-engine.rst index 532b311d0..5ef4ab873 100644 --- a/docs/plugins/steam-engine.rst +++ b/docs/plugins/steam-engine.rst @@ -3,7 +3,7 @@ steam-engine .. dfhack-tool:: :summary: Allow modded steam engine buildings to function. - :tags: unavailable fort gameplay buildings + :tags: unavailable :no-command: The steam-engine plugin detects custom workshops with the string diff --git a/docs/plugins/stockflow.rst b/docs/plugins/stockflow.rst index 29b7838fc..95eba213c 100644 --- a/docs/plugins/stockflow.rst +++ b/docs/plugins/stockflow.rst @@ -3,7 +3,7 @@ stockflow .. dfhack-tool:: :summary: Queue manager jobs based on free space in stockpiles. - :tags: unavailable fort auto stockpiles workorders + :tags: unavailable With this plugin, the fortress bookkeeper can tally up free space in specific stockpiles and queue jobs through the manager to produce items to fill the free diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index da9b77dc5..7d4ec050b 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -95,6 +95,19 @@ file are: specific item types, and any toggles the category might have (like Prepared meals for the Food category). +Overlay +------- + +This plugin provides a panel that appears when you select a stockpile via an +`overlay` widget. You can use it to easily toggle `logistics` plugin features +like autotrade, automelt, or autotrain. There are also buttons along the top frame for: + +- minimizing the panel (if it is in the way of the vanilla stockpile + configuration widgets) +- showing help for the overlay widget in `gui/launcher` (this page) +- configuring advanced settings for the stockpile, such as whether automelt + will melt masterworks + .. _stockpiles-library: The stockpiles settings library diff --git a/docs/plugins/stocks.rst b/docs/plugins/stocks.rst index f8db4ee67..af5228b12 100644 --- a/docs/plugins/stocks.rst +++ b/docs/plugins/stocks.rst @@ -3,7 +3,7 @@ stocks .. dfhack-tool:: :summary: Enhanced fortress stock management interface. - :tags: unavailable fort productivity items + :tags: unavailable When the plugin is enabled, two new hotkeys become available: diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index 12e814c55..f99be26eb 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -12,10 +12,10 @@ Usage strangemood [] -Examples --------- +Example +------- -``strangemood -force -unit -type secretive -skill armorsmith`` +``strangemood --force --unit --type secretive --skill armorsmith`` Trigger a strange mood for the selected unit that will cause them to become a legendary armorsmith. diff --git a/docs/plugins/title-folder.rst b/docs/plugins/title-folder.rst index ee4068547..b6a250e73 100644 --- a/docs/plugins/title-folder.rst +++ b/docs/plugins/title-folder.rst @@ -3,7 +3,7 @@ title-folder .. dfhack-tool:: :summary: Displays the DF folder name in the window title bar. - :tags: unavailable interface + :tags: unavailable :no-command: Usage diff --git a/docs/plugins/trackstop.rst b/docs/plugins/trackstop.rst index 041bcac2c..d2011387e 100644 --- a/docs/plugins/trackstop.rst +++ b/docs/plugins/trackstop.rst @@ -3,7 +3,7 @@ trackstop .. dfhack-tool:: :summary: Add dynamic configuration options for track stops. - :tags: unavailable fort gameplay buildings + :tags: unavailable :no-command: When enabled, this plugin adds a :kbd:`q` menu for track stops, which is diff --git a/docs/plugins/tubefill.rst b/docs/plugins/tubefill.rst index a8684c765..ff668f8df 100644 --- a/docs/plugins/tubefill.rst +++ b/docs/plugins/tubefill.rst @@ -3,9 +3,13 @@ tubefill .. dfhack-tool:: :summary: Replenishes mined-out adamantine. - :tags: unavailable fort armok map + :tags: fort armok map -Veins that were originally hollow will be left alone. +This tool replaces mined-out tiles of adamantine spires with fresh, undug +adamantine walls, ready to be re-harvested. Empty tiles within the spire that +used to contain special gemstones, obsidian, water, or magma will also be +replaced with fresh adamantine. Adamantine spires that were originally hollow +will be left hollow. See below for more details. Usage ----- diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index f9467e333..69c93bb60 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -3,7 +3,7 @@ tweak .. dfhack-tool:: :summary: A collection of tweaks and bugfixes. - :tags: unavailable adventure fort armok bugfix fps interface + :tags: unavailable Usage ----- diff --git a/docs/plugins/workflow.rst b/docs/plugins/workflow.rst index c95054c0e..323dd6ed4 100644 --- a/docs/plugins/workflow.rst +++ b/docs/plugins/workflow.rst @@ -3,7 +3,7 @@ workflow .. dfhack-tool:: :summary: Manage automated item production rules. - :tags: unavailable fort auto jobs + :tags: unavailable Manage repeat jobs according to stock levels. `gui/workflow` provides a simple front-end integrated in the game UI. diff --git a/docs/plugins/zone.rst b/docs/plugins/zone.rst index af6ee2b5f..26e170a58 100644 --- a/docs/plugins/zone.rst +++ b/docs/plugins/zone.rst @@ -3,7 +3,7 @@ zone .. dfhack-tool:: :summary: Manage activity zones, cages, and the animals therein. - :tags: unavailable fort productivity animals buildings + :tags: unavailable Usage ----- @@ -157,3 +157,32 @@ cages and then place one pen/pasture activity zone above them, covering all cages you want to use. Then use ``zone set`` (like with ``assign``) and run ``zone tocages ``. ``tocages`` can be used together with ``nick`` or ``remnick`` to adjust nicknames while assigning to cages. + +Overlay +------- + +Advanced unit selection is available via an `overlay` widget that appears when +you select a cage, restraint, pasture zone, or pit/pond zone. + +In the window that pops up when you click the hotkey hint or hit the hotkey on your keyboard, you can: + +- search for units by name +- sort or filter by status (Assigned here, Pastured elsewhere, On restraint, On + display in cage, In movable cage, or Roaming) +- sort or filter by disposition (Pet, Domesticated, Partially trained, Wild + (trainable), Wild (untrainable), or Hostile) +- sort by gender +- sort by name +- filter by whether the unit lays eggs +- filter by whether the unit needs a grazing area + +The window is fully navigatable via keyboard or mouse. Hit Enter or click on a +unit to assign/unassign it to the currently selected zone or building. Shift +click to assign/unassign a range of units. + +You can also keep the window open and click around on different cages, +restraints, pastures, or pit/ponds, so you can manage multiple buildings/zones +without having to close and reopen the window. + +Just like all other overlays, you can disable this one in `gui/control-panel` on +the Overlays tab if you don't want the option of using it. diff --git a/docs/sphinx_extensions/dfhack/changelog.py b/docs/sphinx_extensions/dfhack/changelog.py index dd59fa9d5..a981bf984 100644 --- a/docs/sphinx_extensions/dfhack/changelog.py +++ b/docs/sphinx_extensions/dfhack/changelog.py @@ -16,11 +16,12 @@ CHANGELOG_PATHS = ( CHANGELOG_PATHS = (os.path.join(DFHACK_ROOT, p) for p in CHANGELOG_PATHS) CHANGELOG_SECTIONS = [ - 'New Plugins', - 'New Scripts', - 'New Tweaks', + 'New Tools', + 'New Plugins', # deprecated + 'New Scripts', # deprecated + 'New Tweaks', # deprecated 'New Features', - 'New Internal Commands', + 'New Internal Commands', # deprecated 'Fixes', 'Misc Improvements', 'Removed', diff --git a/docs/sphinx_extensions/dfhack/tool_docs.py b/docs/sphinx_extensions/dfhack/tool_docs.py index 836bab217..6c65e5918 100644 --- a/docs/sphinx_extensions/dfhack/tool_docs.py +++ b/docs/sphinx_extensions/dfhack/tool_docs.py @@ -140,7 +140,8 @@ class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription): anchor = to_anchor(self.get_tool_name_from_docname()) tags = self.env.domaindata['tag-repo']['doctags'][docname] indexdata = (name, self.options.get('summary', ''), '', docname, anchor, 0) - self.env.domaindata['all']['objects'].append(indexdata) + if 'unavailable' not in tags: + self.env.domaindata['all']['objects'].append(indexdata) for tag in tags: self.env.domaindata[tag]['objects'].append(indexdata) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 8681c9c90..e9ec8f717 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -9,8 +9,11 @@ if(UNIX) option(CONSOLE_NO_CATCH "Make the console not catch 'CTRL+C' events for easier debugging." OFF) endif() -include_directories(proto) -include_directories(include) +# Generation +set(CODEGEN_OUT ${dfapi_SOURCE_DIR}/include/df/codegen.out.xml) + +file(GLOB GENERATE_INPUT_SCRIPTS ${dfapi_SOURCE_DIR}/xml/*.pm ${dfapi_SOURCE_DIR}/xml/*.xslt) +file(GLOB GENERATE_INPUT_XMLS ${dfapi_SOURCE_DIR}/xml/df.*.xml) execute_process(COMMAND ${PERL_EXECUTABLE} xml/list.pl xml ${dfapi_SOURCE_DIR}/include/df ";" WORKING_DIRECTORY ${dfapi_SOURCE_DIR} @@ -18,6 +21,32 @@ execute_process(COMMAND ${PERL_EXECUTABLE} xml/list.pl xml ${dfapi_SOURCE_DIR}/i set_source_files_properties(${GENERATED_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE GENERATED TRUE) +add_custom_command( + OUTPUT ${CODEGEN_OUT} + BYPRODUCTS ${GENERATED_HDRS} + COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/xml/codegen.pl + ${CMAKE_CURRENT_SOURCE_DIR}/xml + ${CMAKE_CURRENT_SOURCE_DIR}/include/df + MAIN_DEPENDENCY ${dfapi_SOURCE_DIR}/xml/codegen.pl + COMMENT "Generating codegen.out.xml and df/headers" + DEPENDS ${GENERATE_INPUT_XMLS} ${GENERATE_INPUT_SCRIPTS} +) + +if(NOT("${CMAKE_GENERATOR}" STREQUAL Ninja)) + # use BYPRODUCTS instead under Ninja to avoid rebuilds + list(APPEND CODEGEN_OUT ${GENERATED_HDRS}) +endif() + +add_custom_target(generate_headers DEPENDS ${CODEGEN_OUT}) + +include_directories(include) + +add_subdirectory(xml) + +if(BUILD_LIBRARY) + +include_directories(proto) + set(MAIN_HEADERS include/Internal.h include/DFHack.h @@ -60,6 +89,7 @@ set(MAIN_SOURCES ColorText.cpp CompilerWorkAround.cpp DataDefs.cpp + DataIdentity.cpp Debug.cpp Error.cpp VTableInterpose.cpp @@ -69,7 +99,6 @@ set(MAIN_SOURCES LuaApi.cpp DataStatics.cpp DataStaticsCtor.cpp - DataStaticsFields.cpp MiscUtils.cpp Types.cpp PluginManager.cpp @@ -184,8 +213,7 @@ foreach(GROUP other a b c d e f g h i j k l m n o p q r s t u v w x y z) set(STATIC_FIELDS_INC_FILENAME "df/static.fields-${GROUP}.inc") endif() file(WRITE ${STATIC_FIELDS_FILENAME}.tmp - "#define STATIC_FIELDS_GROUP\n" - "#include \"../DataStaticsFields.cpp\"\n" + "#include \"DataStaticsFields.inc\"\n" "#include \"${STATIC_FIELDS_INC_FILENAME}\"\n" ) execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -262,45 +290,20 @@ add_custom_target(generate_proto_core DEPENDS ${PROJECT_PROTO_TMP_FILES}) # Merge headers into sources set_source_files_properties( ${PROJECT_HEADERS} PROPERTIES HEADER_FILE_ONLY TRUE ) list(APPEND PROJECT_SOURCES ${PROJECT_HEADERS}) - -# Generation list(APPEND PROJECT_SOURCES ${GENERATED_HDRS}) -file(GLOB GENERATE_INPUT_SCRIPTS ${dfapi_SOURCE_DIR}/xml/*.pm ${dfapi_SOURCE_DIR}/xml/*.xslt) -file(GLOB GENERATE_INPUT_XMLS ${dfapi_SOURCE_DIR}/xml/df.*.xml) - -set(CODEGEN_OUT ${dfapi_SOURCE_DIR}/include/df/codegen.out.xml) -if(NOT("${CMAKE_GENERATOR}" STREQUAL Ninja)) - # use BYPRODUCTS instead under Ninja to avoid rebuilds - list(APPEND CODEGEN_OUT ${GENERATED_HDRS}) -endif() - -add_custom_command( - OUTPUT ${CODEGEN_OUT} - BYPRODUCTS ${GENERATED_HDRS} - COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/xml/codegen.pl - ${CMAKE_CURRENT_SOURCE_DIR}/xml - ${CMAKE_CURRENT_SOURCE_DIR}/include/df - MAIN_DEPENDENCY ${dfapi_SOURCE_DIR}/xml/codegen.pl - COMMENT "Generating codegen.out.xml and df/headers" - DEPENDS ${GENERATE_INPUT_XMLS} ${GENERATE_INPUT_SCRIPTS} -) - -add_custom_target(generate_headers - DEPENDS ${dfapi_SOURCE_DIR}/include/df/codegen.out.xml) - if(REMOVE_SYMBOLS_FROM_DF_STUBS) if(UNIX) # Don't produce debug info for generated stubs - set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp DataStaticsFields.cpp ${STATIC_FIELDS_FILES} + set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp ${STATIC_FIELDS_FILES} PROPERTIES COMPILE_FLAGS "-g0 -O1") else(WIN32) - set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp DataStaticsFields.cpp ${STATIC_FIELDS_FILES} + set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp ${STATIC_FIELDS_FILES} PROPERTIES COMPILE_FLAGS "/O1 /bigobj") endif() else() if(WIN32) - set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp DataStaticsFields.cpp ${STATIC_FIELDS_FILES} + set_source_files_properties(DataStatics.cpp DataStaticsCtor.cpp ${STATIC_FIELDS_FILES} PROPERTIES COMPILE_FLAGS "/Od /bigobj") endif() endif() @@ -431,10 +434,6 @@ if(UNIX) install(TARGETS dfhooks LIBRARY DESTINATION . RUNTIME DESTINATION .) -else() - # On windows, copy SDL.dll so DF can still run. - install(PROGRAMS ${dfhack_SOURCE_DIR}/package/windows/win${DFHACK_BUILD_ARCH}/SDL.dll - DESTINATION ${DFHACK_LIBRARY_DESTINATION}) endif() # install the main lib @@ -442,20 +441,22 @@ install(TARGETS dfhack LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) -# install the offset file -install(FILES xml/symbols.xml - DESTINATION ${DFHACK_DATA_DESTINATION}) - install(TARGETS dfhack-run dfhack-client binpatch LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) -install(DIRECTORY lua/ - DESTINATION ${DFHACK_LUA_DESTINATION} - FILES_MATCHING PATTERN "*.lua") +endif(BUILD_LIBRARY) -install(DIRECTORY ${dfhack_SOURCE_DIR}/patches - DESTINATION ${DFHACK_DATA_DESTINATION} - FILES_MATCHING PATTERN "*.dif") +# install the offset file +if(INSTALL_DATA_FILES) + install(FILES xml/symbols.xml + DESTINATION ${DFHACK_DATA_DESTINATION}) -add_subdirectory(xml) + install(DIRECTORY lua/ + DESTINATION ${DFHACK_LUA_DESTINATION} + FILES_MATCHING PATTERN "*.lua") + + install(DIRECTORY ${dfhack_SOURCE_DIR}/patches + DESTINATION ${DFHACK_DATA_DESTINATION} + FILES_MATCHING PATTERN "*.dif") +endif() diff --git a/library/Core.cpp b/library/Core.cpp index 431cd2c9f..1d4f52a56 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -84,7 +84,7 @@ using namespace DFHack; #include #include "md5wrapper.h" -#include "SDL_events.h" +#include #ifdef LINUX_BUILD #include @@ -1033,7 +1033,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (first == "die") { +#ifdef WIN32 + TerminateProcess(GetCurrentProcess(),666); +#else std::_Exit(666); +#endif } else if (first == "kill-lua") { @@ -1249,7 +1253,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, s } else if (res == CR_NEEDS_CONSOLE) con.printerr("%s needs an interactive console to work.\n" - "Please run this command from the DFHack terminal.\n", first.c_str()); + "Please run this command from the DFHack console.\n\n" +#ifdef WIN32 + "You can show the console with the 'show' command." +#else + "The console is accessible when you run DF from the commandline\n" + "via the './dfhack' script." +#endif + "\n", first.c_str()); return res; } @@ -1404,7 +1415,7 @@ Core::~Core() } Core::Core() : - d(dts::make_unique()), + d(std::make_unique()), script_path_mutex{}, HotkeyMutex{}, HotkeyCond{}, @@ -1423,6 +1434,7 @@ Core::Core() : 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; @@ -1430,7 +1442,6 @@ Core::Core() : top_viewscreen = NULL; color_ostream::log_errors_to_stderr = true; - }; void Core::fatal (std::string output) @@ -1448,12 +1459,9 @@ void Core::fatal (std::string output) con.print("\n"); } fprintf(stderr, "%s\n", out.str().c_str()); -#ifndef LINUX_BUILD - out << "Check file stderr.log for details\n"; - MessageBox(0,out.str().c_str(),"DFHack error!", MB_OK | MB_ICONERROR); -#else + out << "Check file stderr.log for details.\n"; std::cout << "DFHack fatal error: " << out.str() << std::endl; -#endif + DFSDL::DFSDL_ShowSimpleMessageBox(0x10 /* SDL_MESSAGEBOX_ERROR */, "DFHack error!", out.str().c_str(), NULL); bool is_headless = bool(getenv("DFHACK_HEADLESS")); if (is_headless) @@ -1471,6 +1479,10 @@ std::string Core::getHackPath() #endif } +df::viewscreen * Core::getTopViewscreen() { + return getInstance().top_viewscreen; +} + bool Core::InitMainThread() { Filesystem::init(); @@ -1491,13 +1503,19 @@ bool Core::InitMainThread() { std::cerr << "DFHack build: " << Version::git_description() << "\n" << "Starting with working directory: " << Filesystem::getcwd() << std::endl; + std::cerr << "Binding to SDL.\n"; + if (!DFSDL::init(con)) { + fatal("cannot bind SDL libraries"); + return false; + } + // find out what we are... #ifdef LINUX_BUILD const char * path = "hack/symbols.xml"; #else const char * path = "hack\\symbols.xml"; #endif - auto local_vif = dts::make_unique(); + auto local_vif = std::make_unique(); std::cerr << "Identifying DF version.\n"; try { @@ -1513,7 +1531,7 @@ bool Core::InitMainThread() { return false; } vif = std::move(local_vif); - auto local_p = dts::make_unique(*vif); + auto local_p = std::make_unique(*vif); local_p->ValidateDescriptionOS(); vinfo = local_p->getDescriptor(); @@ -1677,11 +1695,6 @@ bool Core::InitSimulationThread() return false; } - std::cerr << "Binding to SDL.\n"; - if (!DFSDL::init(con)) { - fatal("cannot bind SDL libraries"); - return false; - } if (DFSteam::init(con)) { std::cerr << "Found Steam.\n"; DFSteam::launchSteamDFHackIfNecessary(con); @@ -1855,6 +1868,11 @@ void *Core::GetData( std::string key ) } } +Core& Core::getInstance() { + static Core instance; + return instance; +} + bool Core::isSuspended(void) { return ownerThread.load() == std::this_thread::get_id(); @@ -1887,8 +1905,8 @@ void Core::doUpdate(color_ostream &out) strict_virtual_cast(screen); // save data (do this before updating last_world_data_ptr and triggering unload events) - if ((df::global::game->main_interface.options.do_manual_save && !d->last_manual_save_request) || - (df::global::plotinfo->main.autosave_request && !d->last_autosave_request) || + if ((df::global::game && df::global::game->main_interface.options.do_manual_save && !d->last_manual_save_request) || + (df::global::plotinfo && df::global::plotinfo->main.autosave_request && !d->last_autosave_request) || (is_load_save && !d->was_load_save && strict_virtual_cast(screen))) { doSaveData(out); @@ -1950,8 +1968,10 @@ void Core::doUpdate(color_ostream &out) // Execute per-frame handlers onUpdate(out); - d->last_autosave_request = df::global::plotinfo->main.autosave_request; - d->last_manual_save_request = df::global::game->main_interface.options.do_manual_save; + if (df::global::game && df::global::plotinfo) { + d->last_autosave_request = df::global::plotinfo->main.autosave_request; + d->last_manual_save_request = df::global::game->main_interface.options.do_manual_save; + } d->was_load_save = is_load_save; out << std::flush; @@ -2206,9 +2226,6 @@ void Core::onStateChange(color_ostream &out, state_change_event event) } } break; - case SC_VIEWSCREEN_CHANGED: - Textures::init(out); - break; default: break; } @@ -2347,101 +2364,78 @@ bool Core::DFH_ncurses_key(int key) return ncurses_wgetch(key, dummy); } -int UnicodeAwareSym(const SDL::KeyboardEvent& ke) -{ - // Assume keyboard layouts don't change the order of numbers: - if( '0' <= ke.ksym.sym && ke.ksym.sym <= '9') return ke.ksym.sym; - if(SDL::K_F1 <= ke.ksym.sym && ke.ksym.sym <= SDL::K_F12) return ke.ksym.sym; - - // These keys are mapped to the same control codes as Ctrl-? - switch (ke.ksym.sym) - { - case SDL::K_RETURN: - case SDL::K_KP_ENTER: - case SDL::K_TAB: - case SDL::K_ESCAPE: - case SDL::K_DELETE: - return ke.ksym.sym; - default: - break; - } - - int unicode = ke.ksym.unicode; - - // convert Ctrl characters to their 0x40-0x5F counterparts: - if (unicode < ' ') - { - unicode += 'A' - 1; - } - - // convert A-Z to their a-z counterparts: - if('A' <= unicode && unicode <= 'Z') - { - unicode += 'a' - 'A'; - } - - // convert various other punctuation marks: - if('\"' == unicode) unicode = '\''; - if('+' == unicode) unicode = '='; - if(':' == unicode) unicode = ';'; - if('<' == unicode) unicode = ','; - if('>' == unicode) unicode = '.'; - if('?' == unicode) unicode = '/'; - if('{' == unicode) unicode = '['; - if('|' == unicode) unicode = '\\'; - if('}' == unicode) unicode = ']'; - if('~' == unicode) unicode = '`'; +bool Core::getSuppressDuplicateKeyboardEvents() { + return suppress_duplicate_keyboard_events; +} - return unicode; +void Core::setSuppressDuplicateKeyboardEvents(bool suppress) { + DEBUG(keybinding).print("setting suppress_duplicate_keyboard_events to %s\n", + suppress ? "true" : "false"); + suppress_duplicate_keyboard_events = suppress; } // returns true if the event is handled -bool Core::DFH_SDL_Event(SDL::Event* ev) +bool Core::DFH_SDL_Event(SDL_Event* ev) { + static std::map hotkey_states; + // do NOT process events before we are ready. - if(!started || !ev) + if (!started || !ev) return false; - if(ev->type == SDL::ET_ACTIVEEVENT && ev->active.gain) - { + if (ev->type == SDL_WINDOWEVENT && ev->window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { // clear modstate when gaining focus in case alt-tab was used when // losing focus and modstate is now incorrectly set modstate = 0; return false; } - if(ev->type == SDL::ET_KEYDOWN || ev->type == SDL::ET_KEYUP) - { - auto ke = (SDL::KeyboardEvent *)ev; + if (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) { + auto &ke = ev->key; + auto &sym = ke.keysym.sym; - if (ke->ksym.sym == SDL::K_LSHIFT || ke->ksym.sym == SDL::K_RSHIFT) - modstate = (ev->type == SDL::ET_KEYDOWN) ? modstate | DFH_MOD_SHIFT : modstate & ~DFH_MOD_SHIFT; - else if (ke->ksym.sym == SDL::K_LCTRL || ke->ksym.sym == SDL::K_RCTRL) - modstate = (ev->type == SDL::ET_KEYDOWN) ? modstate | DFH_MOD_CTRL : modstate & ~DFH_MOD_CTRL; - else if (ke->ksym.sym == SDL::K_LALT || ke->ksym.sym == SDL::K_RALT) - modstate = (ev->type == SDL::ET_KEYDOWN) ? modstate | DFH_MOD_ALT : modstate & ~DFH_MOD_ALT; - else if(ke->state == SDL::BTN_PRESSED && !hotkey_states[ke->ksym.sym]) + if (sym == SDLK_LSHIFT || sym == SDLK_RSHIFT) + modstate = (ev->type == SDL_KEYDOWN) ? modstate | DFH_MOD_SHIFT : modstate & ~DFH_MOD_SHIFT; + else if (sym == SDLK_LCTRL || sym == SDLK_RCTRL) + 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 (ke.state == SDL_PRESSED && !hotkey_states[sym]) { - hotkey_states[ke->ksym.sym] = true; - - // Use unicode so Windows gives the correct value for the - // user's Input Language - if(ke->ksym.unicode && ((ke->ksym.unicode & 0xff80) == 0)) - { - int key = UnicodeAwareSym(*ke); - SelectHotkey(key, modstate); - } - else - { - // Pretend non-ascii characters don't happen: - SelectHotkey(ke->ksym.sym, modstate); + // 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); + bool handled = SelectHotkey(sym, modstate); + if (handled) { + 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", + suppress_duplicate_keyboard_events ? "" : "not "); + return suppress_duplicate_keyboard_events; } } - else if(ke->state == SDL::BTN_RELEASED) + else if (ke.state == SDL_RELEASED) { - hotkey_states[ke->ksym.sym] = false; + DEBUG(keybinding).print("key up: sym=%d (%c)\n", sym, sym); + hotkey_states[sym] = false; } } + else if (ev->type == SDL_TEXTINPUT) { + auto &te = ev->text; + DEBUG(keybinding).print("text input: '%s' (modifiers: %s%s%s)\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", + suppress_duplicate_keyboard_events ? "" : "not "); + return suppress_duplicate_keyboard_events; + } + } + return false; } @@ -2455,12 +2449,12 @@ bool Core::SelectHotkey(int sym, int modifiers) while (screen->child) screen = screen->child; - if (sym == SDL::K_KP_ENTER) - sym = SDL::K_RETURN; + if (sym == SDLK_KP_ENTER) + sym = SDLK_RETURN; std::string cmd; - DEBUG(keybinding).print("checking hotkeys for sym=%d, modifiers=%x\n", sym, modifiers); + DEBUG(keybinding).print("checking hotkeys for sym=%d (%c), modifiers=%x\n", sym, sym, modifiers); { std::lock_guard lock(HotkeyMutex); @@ -2497,7 +2491,7 @@ bool Core::SelectHotkey(int sym, int modifiers) if (cmd.empty()) { // Check the hotkey keybindings - int idx = sym - SDL::K_F1; + int idx = sym - SDLK_F1; if(idx >= 0 && idx < 8) { /* TODO: understand how this changes for v50 @@ -2555,22 +2549,22 @@ static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string } if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { - *psym = SDL::K_a + (keyspec[0]-'A'); + *psym = SDLK_a + (keyspec[0]-'A'); return true; } else if (keyspec.size() == 1 && keyspec[0] == '`') { - *psym = SDL::K_BACKQUOTE; + *psym = SDLK_BACKQUOTE; return true; } else if (keyspec.size() == 1 && keyspec[0] >= '0' && keyspec[0] <= '9') { - *psym = SDL::K_0 + (keyspec[0]-'0'); + *psym = SDLK_0 + (keyspec[0]-'0'); return true; } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { - *psym = SDL::K_F1 + (keyspec[1]-'1'); + *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 = SDL::K_F10 + (keyspec[2]-'0'); + *psym = SDLK_F10 + (keyspec[2]-'0'); return true; } else if (keyspec == "Enter") { - *psym = SDL::K_RETURN; + *psym = SDLK_RETURN; return true; } else return false; diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp index f376edc6a..cd261d50f 100644 --- a/library/DataDefs.cpp +++ b/library/DataDefs.cpp @@ -213,12 +213,12 @@ std::string pointer_identity::getFullName() std::string container_identity::getFullName(type_identity *item) { - return "<" + (item ? item->getFullName() : std::string("void")) + ">"; + return '<' + (item ? item->getFullName() : std::string("void")) + '>'; } std::string ptr_container_identity::getFullName(type_identity *item) { - return "<" + (item ? item->getFullName() : std::string("void")) + "*>"; + return '<' + (item ? item->getFullName() : std::string("void")) + std::string("*>"); } std::string bit_container_identity::getFullName(type_identity *) diff --git a/library/DataStaticsFields.cpp b/library/DataIdentity.cpp similarity index 60% rename from library/DataStaticsFields.cpp rename to library/DataIdentity.cpp index 8318b523d..c041b009d 100644 --- a/library/DataStaticsFields.cpp +++ b/library/DataIdentity.cpp @@ -1,24 +1,26 @@ #include +#include #include - -#ifndef STATIC_FIELDS_GROUP -#include "DataDefs.h" -#endif +#include +#include +#include #include "DataFuncs.h" +#include "DataIdentity.h" -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Winvalid-offsetof" -#endif +// the space after the uses of "type" in OPAQUE_IDENTITY_TRAITS_NAME is _required_ +// without it the macro generates a syntax error when type is a template specification namespace df { #define NUMBER_IDENTITY_TRAITS(category, type, name) \ category##_identity identity_traits::identity(name); #define INTEGER_IDENTITY_TRAITS(type, name) NUMBER_IDENTITY_TRAITS(integer, type, name) #define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type, #type) +#define OPAQUE_IDENTITY_TRAITS_NAME(type, name) \ + opaque_identity identity_traits::identity(sizeof(type), allocator_noassign_fn, name) +#define STL_OPAQUE_IDENTITY_TRAITS(type) OPAQUE_IDENTITY_TRAITS_NAME(std::type, #type) -#ifndef STATIC_FIELDS_GROUP INTEGER_IDENTITY_TRAITS(char, "char"); INTEGER_IDENTITY_TRAITS(signed char, "int8_t"); INTEGER_IDENTITY_TRAITS(unsigned char, "uint8_t"); @@ -42,25 +44,12 @@ namespace df { stl_bit_vector_identity identity_traits >::identity; bit_array_identity identity_traits >::identity; - static void *fstream_allocator_fn(void *out, const void *in) { - if (out) { /* *(T*)out = *(const T*)in;*/ return NULL; } - else if (in) { delete (std::fstream*)in; return (std::fstream*)in; } - else return new std::fstream(); - } - opaque_identity identity_traits::identity( - sizeof(std::fstream), fstream_allocator_fn, "fstream"); + STL_OPAQUE_IDENTITY_TRAITS(condition_variable); + STL_OPAQUE_IDENTITY_TRAITS(fstream); + STL_OPAQUE_IDENTITY_TRAITS(mutex); + STL_OPAQUE_IDENTITY_TRAITS(future); + STL_OPAQUE_IDENTITY_TRAITS(function); + STL_OPAQUE_IDENTITY_TRAITS(optional >); buffer_container_identity buffer_container_identity::base_instance; -#endif -#undef NUMBER_IDENTITY_TRAITS -#undef INTEGER_IDENTITY_TRAITS -#undef FLOAT_IDENTITY_TRAITS } - -#define TID(type) (&identity_traits< type >::identity) - -#define FLD(mode, name) struct_field_info::mode, #name, offsetof(CUR_STRUCT, name) -#define GFLD(mode, name) struct_field_info::mode, #name, (size_t)&df::global::name -#define METHOD(mode, name) struct_field_info::mode, #name, 0, wrap_function(&CUR_STRUCT::name) -#define METHOD_N(mode, func, name) struct_field_info::mode, #name, 0, wrap_function(&CUR_STRUCT::func) -#define FLD_END struct_field_info::END diff --git a/library/Debug.cpp b/library/Debug.cpp index 9b13af168..dafbeb5ce 100644 --- a/library/Debug.cpp +++ b/library/Debug.cpp @@ -26,6 +26,7 @@ redistribute it freely, subject to the following restrictions: #include "Debug.h" #include "DebugManager.h" +#include #include #include #include diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 4e339e768..31a8ec749 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -48,7 +48,7 @@ DFhackCExport void dfhooks_prerender() { // called from the main thread for each SDL event. if true is returned, then // the event has been consumed and further processing shouldn't happen -DFhackCExport bool dfhooks_sdl_event(SDL::Event* event) { +DFhackCExport bool dfhooks_sdl_event(SDL_Event* event) { if (disabled) return false; return DFHack::Core::getInstance().DFH_SDL_Event(event); diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 0a737875a..a8e901a6d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -161,6 +161,26 @@ static bool get_int_field(lua_State *L, T *pf, int idx, const char *name, int de return !nil; } +template +static bool get_int_or_closure_field(lua_State *L, T *pf, int idx, const char *name, int defval) +{ + lua_getfield(L, idx, name); + bool nil = lua_isnil(L, -1); + if (nil) { + *pf = T(defval); + } else if (lua_isnumber(L, -1)) { + *pf = T(lua_tointeger(L, -1)); + } else if (lua_isfunction(L, -1)) { + lua_call(L, 0, 1); + *pf = T(lua_tointeger(L, -1)); + lua_pop(L, 1); + } else { + luaL_error(L, "Field %s is not a number or closure function.", name); + } + lua_pop(L, 1); + return !nil; +} + static bool get_char_field(lua_State *L, char *pf, int idx, const char *name, char defval) { lua_getfield(L, idx, name); @@ -207,7 +227,7 @@ static void decode_pen(lua_State *L, Pen &pen, int idx) else pen.bold = lua_toboolean(L, -1); lua_pop(L, 1); - get_int_field(L, &pen.tile, idx, "tile", 0); + get_int_or_closure_field(L, &pen.tile, idx, "tile", 0); bool tcolor = get_int_field(L, &pen.tile_fg, idx, "tile_fg", 7); tcolor = get_int_field(L, &pen.tile_bg, idx, "tile_bg", 0) || tcolor; @@ -844,6 +864,23 @@ static void make_pen_table(lua_State *L, Pen &pen) lua_pushboolean(L, false); lua_setfield(L, -2, "tile_color"); break; } + + if (pen.keep_lower) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "keep_lower"); + } + if (pen.write_to_lower) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "write_to_lower"); + } + if (pen.top_of_text) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "top_of_text"); + } + if (pen.bottom_of_text) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "bottom_of_text"); + } } } @@ -1331,8 +1368,8 @@ static CommandHistory * ensureCommandHistory(std::string id, static int getCommandHistory(lua_State *state) { - std::string id = lua_tostring(state, 1); - std::string src_file = lua_tostring(state, 2); + std::string id = luaL_checkstring(state, 1); + std::string src_file = luaL_checkstring(state, 2); std::vector entries; ensureCommandHistory(id, src_file)->getEntries(entries); Lua::PushVector(state, entries); @@ -1360,6 +1397,13 @@ static void OpenModule(lua_State *state, const char *mname, lua_pop(state, 1); } +static void OpenModule(lua_State *state, const char *mname, const luaL_Reg *reg2) +{ + luaL_getsubtable(state, lua_gettop(state), mname); + luaL_setfuncs(state, reg2, 0); + lua_pop(state, 1); +} + #define WRAPM(module, function) { #function, df::wrap_function(module::function,true) } #define WRAP(function) { #function, df::wrap_function(function,true) } #define WRAPN(name, function) { #name, df::wrap_function(function,true) } @@ -1461,8 +1505,9 @@ static int gui_getDwarfmodeViewDims(lua_State *state) static int gui_getMousePos(lua_State *L) { - auto pos = Gui::getMousePos(); - if (pos.isValid()) + bool allow_out_of_bounds = lua_toboolean(L, 1); + df::coord pos = Gui::getMousePos(allow_out_of_bounds); + if ((allow_out_of_bounds && pos.z >= 0) || pos.isValid()) Lua::Push(L, pos); else lua_pushnil(L); @@ -1483,6 +1528,8 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { WRAPM(Gui, getAnyUnit), WRAPM(Gui, getAnyItem), WRAPM(Gui, getAnyBuilding), + WRAPM(Gui, getAnyCivZone), + WRAPM(Gui, getAnyStockpile), WRAPM(Gui, getAnyPlant), WRAPM(Gui, writeToGamelog), WRAPM(Gui, makeAnnouncement), @@ -1614,11 +1661,19 @@ static int gui_revealInDwarfmodeMap(lua_State *state) switch (lua_gettop(state)) { default: + case 5: + rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4), lua_toboolean(state, 5)); + break; case 4: rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4)); break; case 3: - rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false)); + if (lua_isboolean(state, 3)) { + Lua::CheckDFAssign(state, &p, 1); + rv = Gui::revealInDwarfmodeMap(p, lua_toboolean(state, 2), lua_toboolean(state, 3)); + } + else + rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false)); break; case 2: Lua::CheckDFAssign(state, &p, 1); @@ -1709,18 +1764,76 @@ static const luaL_Reg dfhack_job_funcs[] = { /***** Textures module *****/ -static const LuaWrapper::FunctionReg dfhack_textures_module[] = { - WRAPM(Textures, getDfhackLogoTexposStart), - WRAPM(Textures, getGreenPinTexposStart), - WRAPM(Textures, getRedPinTexposStart), - WRAPM(Textures, getIconsTexposStart), - WRAPM(Textures, getOnOffTexposStart), - WRAPM(Textures, getControlPanelTexposStart), - WRAPM(Textures, getThinBordersTexposStart), - WRAPM(Textures, getMediumBordersTexposStart), - WRAPM(Textures, getBoldBordersTexposStart), - WRAPM(Textures, getPanelBordersTexposStart), - WRAPM(Textures, getWindowBordersTexposStart), +static int textures_loadTileset(lua_State *state) +{ + std::string file = luaL_checkstring(state, 1); + auto tile_w = luaL_checkint(state, 2); + auto tile_h = luaL_checkint(state, 3); + bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false; + auto handles = Textures::loadTileset(file, tile_w, tile_h, reserved); + Lua::PushVector(state, handles); + return 1; +} + +static int textures_getTexposByHandle(lua_State *state) +{ + auto handle = luaL_checkunsigned(state, 1); + auto texpos = Textures::getTexposByHandle(handle); + if (texpos == -1) { + lua_pushnil(state); + } else { + Lua::Push(state, texpos); + } + return 1; +} + +static int textures_deleteHandle(lua_State *state) +{ + if (lua_isinteger(state,1)) { + auto handle = luaL_checkunsigned(state, 1); + Textures::deleteHandle(handle); + } else if (lua_istable(state,1)) { + std::vector handles; + Lua::GetVector(state, handles); + for (auto& handle: handles) { + Textures::deleteHandle(handle); + } + } + return 0; +} + +static int textures_createTile(lua_State *state) +{ + std::vector pixels; + Lua::GetVector(state, pixels); + auto tile_w = luaL_checkint(state, 2); + auto tile_h = luaL_checkint(state, 3); + bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false; + auto handle = Textures::createTile(pixels, tile_w, tile_h, reserved); + Lua::Push(state, handle); + return 1; +} + +static int textures_createTileset(lua_State *state) +{ + std::vector pixels; + Lua::GetVector(state, pixels); + auto texture_w = luaL_checkint(state, 2); + auto texture_h = luaL_checkint(state, 3); + auto tile_w = luaL_checkint(state, 4); + auto tile_h = luaL_checkint(state, 5); + bool reserved = lua_isboolean(state, 6) ? lua_toboolean(state, 6) : false; + auto handles = Textures::createTileset(pixels, texture_w, texture_h, tile_w, tile_h, reserved); + Lua::PushVector(state, handles); + return 1; +} + +static const luaL_Reg dfhack_textures_funcs[] = { + { "loadTileset", textures_loadTileset }, + { "getTexposByHandle", textures_getTexposByHandle }, + { "deleteHandle", textures_deleteHandle }, + { "createTile", textures_createTile }, + { "createTileset", textures_createTileset }, { NULL, NULL } }; @@ -1759,11 +1872,16 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isTame), WRAPM(Units, isTamable), WRAPM(Units, isDomesticated), + WRAPM(Units, isMarkedForTraining), + WRAPM(Units, isMarkedForTaming), + WRAPM(Units, isMarkedForWarTraining), + WRAPM(Units, isMarkedForHuntTraining), WRAPM(Units, isMarkedForSlaughter), WRAPM(Units, isMarkedForGelding), WRAPM(Units, isGeldable), WRAPM(Units, isGelded), WRAPM(Units, isEggLayer), + WRAPM(Units, isEggLayerRace), WRAPM(Units, isGrazer), WRAPM(Units, isMilkable), WRAPM(Units, isForest), @@ -1797,6 +1915,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getNemesis), WRAPM(Units, getPhysicalAttrValue), WRAPM(Units, getMentalAttrValue), + WRAPM(Units, casteFlagSet), WRAPM(Units, getMiscTrait), WRAPM(Units, getAge), WRAPM(Units, getKillCount), @@ -1822,6 +1941,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildNameById), + WRAPM(Units, getReadableName), WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialEvent), WRAPM(Units, getStressCategory), @@ -1832,6 +1952,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, multiplyGroupActionTimers), WRAPM(Units, setActionTimers), WRAPM(Units, setGroupActionTimers), + WRAPM(Units, getUnitByNobleRole), { NULL, NULL } }; @@ -1920,6 +2041,14 @@ static int units_getCitizens(lua_State *L) { return 0; } +static int units_getUnitsByNobleRole(lua_State *L) { + std::string role_name = luaL_checkstring(L, -1); + std::vector units; + Units::getUnitsByNobleRole(units, role_name); + Lua::PushVector(L, units); + return 1; +} + static int units_getStressCutoffs(lua_State *L) { lua_newtable(L); @@ -1934,6 +2063,7 @@ static const luaL_Reg dfhack_units_funcs[] = { { "getNoblePositions", units_getNoblePositions }, { "getUnitsInBox", units_getUnitsInBox }, { "getCitizens", units_getCitizens }, + { "getUnitsByNobleRole", units_getUnitsByNobleRole}, { "getStressCutoffs", units_getStressCutoffs }, { NULL, NULL } }; @@ -2009,12 +2139,16 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = { WRAPM(Items, getSubtypeDef), WRAPM(Items, getItemBaseValue), WRAPM(Items, getValue), + WRAPM(Items, isRequestedTradeGood), WRAPM(Items, createItem), WRAPM(Items, checkMandates), WRAPM(Items, canTrade), WRAPM(Items, canTradeWithContents), + WRAPM(Items, canTradeAnyWithContents), + WRAPM(Items, markForTrade), WRAPM(Items, isRouteVehicle), WRAPM(Items, isSquadEquipment), + WRAPM(Items, getCapacity), WRAPN(moveToGround, items_moveToGround), WRAPN(moveToContainer, items_moveToContainer), WRAPN(moveToInventory, items_moveToInventory), @@ -2106,6 +2240,7 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = { WRAPM(Maps, enableBlockUpdates), WRAPM(Maps, getGlobalInitFeature), WRAPM(Maps, getLocalInitFeature), + WRAPM(Maps, getWalkableGroup), WRAPM(Maps, canWalkBetween), WRAPM(Maps, spawnFlow), WRAPN(hasTileAssignment, hasTileAssignment), @@ -2602,14 +2737,29 @@ static int screen_doSimulateInput(lua_State *L) int sz = lua_rawlen(L, 2); std::set keys; + char str = '\0'; for (int j = 1; j <= sz; j++) { lua_rawgeti(L, 2, j); - keys.insert((df::interface_key)lua_tointeger(L, -1)); + df::interface_key k = (df::interface_key)lua_tointeger(L, -1); + if (!str && k > df::interface_key::STRING_A000 && k <= df::interface_key::STRING_A255) + str = Screen::keyToChar(k); + keys.insert(k); lua_pop(L, 1); } + // if we're injecting a text keybinding, ensure it is reflected in the enabler text buffer + std::string prev_input; + if (str) { + prev_input = (const char *)&df::global::enabler->last_text_input[0]; + df::global::enabler->last_text_input[0] = str; + df::global::enabler->last_text_input[1] = '\0'; + } + screen->feed(&keys); + + if (str) + strcpy((char *)&df::global::enabler->last_text_input[0], prev_input.c_str()); return 0; } @@ -2635,6 +2785,7 @@ static int screen_charToKey(lua_State *L) return 1; } +/* static int screen_zoom(lua_State *L) { using df::global::enabler; @@ -2651,6 +2802,7 @@ static int screen_zoom(lua_State *L) enabler->zoom_display(cmd); return 0; } +*/ } @@ -2671,7 +2823,7 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "_doSimulateInput", screen_doSimulateInput }, { "keyToChar", screen_keyToChar }, { "charToKey", screen_charToKey }, - { "zoom", screen_zoom }, + //{ "zoom", screen_zoom }, { NULL, NULL } }; @@ -3010,6 +3162,8 @@ static const LuaWrapper::FunctionReg dfhack_internal_module[] = { WRAPN(getAddressSizeInHeap, get_address_size_in_heap), WRAPN(getRootAddressOfHeapObject, get_root_address_of_heap_object), WRAPN(msizeAddress, msize_address), + WRAP(getClipboardTextCp437), + WRAP(setClipboardTextCp437), { NULL, NULL } }; @@ -3298,6 +3452,24 @@ static int internal_diffscan(lua_State *L) return 1; } +static int internal_cxxDemangle(lua_State *L) +{ + std::string mangled = luaL_checkstring(L, 1); + std::string status; + std::string demangled = cxx_demangle(mangled, &status); + if (demangled.length()) + { + lua_pushstring(L, demangled.c_str()); + return 1; + } + else + { + lua_pushnil(L); + lua_pushstring(L, status.c_str()); + return 2; + } +} + static int internal_runCommand(lua_State *L) { color_ostream *out = NULL; @@ -3577,13 +3749,23 @@ static int internal_md5file(lua_State *L) } } +static int internal_getSuppressDuplicateKeyboardEvents(lua_State *L) { + Lua::Push(L, Core::getInstance().getSuppressDuplicateKeyboardEvents()); + return 1; +} + +static int internal_setSuppressDuplicateKeyboardEvents(lua_State *L) { + bool suppress = lua_toboolean(L, 1); + Core::getInstance().setSuppressDuplicateKeyboardEvents(suppress); + return 0; +} + static const luaL_Reg dfhack_internal_funcs[] = { { "getPE", internal_getPE }, { "getMD5", internal_getmd5 }, { "getAddress", internal_getAddress }, { "setAddress", internal_setAddress }, { "getVTable", internal_getVTable }, - { "adjustOffset", internal_adjustOffset }, { "getMemRanges", internal_getMemRanges }, { "patchMemory", internal_patchMemory }, @@ -3592,6 +3774,7 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "memcmp", internal_memcmp }, { "memscan", internal_memscan }, { "diffscan", internal_diffscan }, + { "cxxDemangle", internal_cxxDemangle }, { "getDir", filesystem_listdir }, { "runCommand", internal_runCommand }, { "getModifiers", internal_getModifiers }, @@ -3605,6 +3788,8 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "getCommandDescription", internal_getCommandDescription }, { "threadid", internal_threadid }, { "md5File", internal_md5file }, + { "getSuppressDuplicateKeyboardEvents", internal_getSuppressDuplicateKeyboardEvents }, + { "setSuppressDuplicateKeyboardEvents", internal_setSuppressDuplicateKeyboardEvents }, { NULL, NULL } }; @@ -3625,7 +3810,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); - OpenModule(state, "textures", dfhack_textures_module); + OpenModule(state, "textures", dfhack_textures_funcs); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); OpenModule(state, "military", dfhack_military_module); OpenModule(state, "items", dfhack_items_module, dfhack_items_funcs); diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index a1bf855d6..87699641d 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -131,12 +131,12 @@ void DFHack::Lua::GetVector(lua_State *state, std::vector &pvec, in } } -static bool trigger_inhibit_l_down = false; -static bool trigger_inhibit_r_down = false; -static bool trigger_inhibit_m_down = false; -static bool inhibit_l_down = false; -static bool inhibit_r_down = false; -static bool inhibit_m_down = false; +static bool trigger_inhibit_l = false; +static bool trigger_inhibit_r = false; +static bool trigger_inhibit_m = false; +static bool inhibit_l = false; +static bool inhibit_r = false; +static bool inhibit_m = false; void DFHack::Lua::PushInterfaceKeys(lua_State *L, const std::set &keys) { @@ -161,32 +161,32 @@ void DFHack::Lua::PushInterfaceKeys(lua_State *L, } if (df::global::enabler) { - if (!inhibit_l_down && df::global::enabler->mouse_lbut_down) { + if (!inhibit_l && df::global::enabler->mouse_lbut) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_L_DOWN"); - trigger_inhibit_l_down = true; + lua_setfield(L, -2, "_MOUSE_L"); + trigger_inhibit_l = true; } - if (!inhibit_r_down && df::global::enabler->mouse_rbut_down) { + if (!inhibit_r && df::global::enabler->mouse_rbut) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_R_DOWN"); - trigger_inhibit_r_down = true; + lua_setfield(L, -2, "_MOUSE_R"); + trigger_inhibit_r = true; } - if (!inhibit_m_down && df::global::enabler->mouse_mbut_down) { + if (!inhibit_m && df::global::enabler->mouse_mbut) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_M_DOWN"); - trigger_inhibit_m_down = true; + lua_setfield(L, -2, "_MOUSE_M"); + trigger_inhibit_m = true; } - if (df::global::enabler->mouse_lbut) { + if (df::global::enabler->mouse_lbut_down) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_L"); + lua_setfield(L, -2, "_MOUSE_L_DOWN"); } - if (df::global::enabler->mouse_rbut) { + if (df::global::enabler->mouse_rbut_down) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_R"); + lua_setfield(L, -2, "_MOUSE_R_DOWN"); } - if (df::global::enabler->mouse_mbut) { + if (df::global::enabler->mouse_mbut_down) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_M"); + lua_setfield(L, -2, "_MOUSE_M_DOWN"); } } } @@ -2159,23 +2159,25 @@ void DFHack::Lua::Core::Reset(color_ostream &out, const char *where) lua_settop(State, 0); } - if (trigger_inhibit_l_down) { - trigger_inhibit_l_down = false; - inhibit_l_down = true; + if (trigger_inhibit_l) { + trigger_inhibit_l = false; + inhibit_l = true; } - if (trigger_inhibit_r_down) { - trigger_inhibit_r_down = false; - inhibit_r_down = true; + if (trigger_inhibit_r) { + trigger_inhibit_r = false; + inhibit_r = true; } - if (trigger_inhibit_m_down) { - trigger_inhibit_m_down = false; - inhibit_m_down = true; + if (trigger_inhibit_m) { + trigger_inhibit_m = false; + inhibit_m = true; } - if (!df::global::enabler->mouse_lbut) - inhibit_l_down = false; - if (!df::global::enabler->mouse_rbut) - inhibit_r_down = false; - if (!df::global::enabler->mouse_mbut) - inhibit_m_down = false; + if (df::global::enabler) { + if (!df::global::enabler->mouse_lbut_down) + inhibit_l = false; + if (!df::global::enabler->mouse_rbut_down) + inhibit_r = false; + if (!df::global::enabler->mouse_mbut_down) + inhibit_m = false; + } } diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index ef69958e0..407dea265 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -664,6 +664,9 @@ static int meta_global_field_reference(lua_State *state) auto field = (struct_field_info*)find_field(state, 2, "reference"); if (!field) field_error(state, 2, "builtin property or method", "reference"); + void *ptr = *(void**)field->offset; + if (!ptr) + field_error(state, 2, "global address not known", "reference"); field_reference(state, field, *(void**)field->offset); return 1; } @@ -1288,11 +1291,27 @@ void LuaWrapper::SetFunctionWrappers(lua_State *state, const FunctionReg *reg) /** * Add fields in the array to the UPVAL_FIELDTABLE candidates on the stack. + * + * flags: + * GLOBALS: if true, pstruct is a global_identity and fields with addresses of 0 are skipped + * RAW: if true, no fields are skipped (supersedes `GLOBALS` flag) and + * special-case fields like OBJ_METHODs are not added to the metatable + * + * Stack in & out: + * base+1: metatable + * base+2: fields table (to be populated, map of name -> struct_field_info*) + * base+3: field iter table (to be populated, bimap of name <-> integer index) */ -static void IndexFields(lua_State *state, int base, struct_identity *pstruct, bool globals) +namespace IndexFieldsFlags { + enum IndexFieldsFlags { + GLOBALS = 1 << 0, + RAW = 1 << 1, + }; +} +static void IndexFields(lua_State *state, int base, struct_identity *pstruct, int flags) { if (pstruct->getParent()) - IndexFields(state, base, pstruct->getParent(), globals); + IndexFields(state, base, pstruct->getParent(), flags); auto fields = pstruct->getFields(); if (!fields) @@ -1312,6 +1331,7 @@ static void IndexFields(lua_State *state, int base, struct_identity *pstruct, bo bool add_to_enum = true; + if (!(flags & IndexFieldsFlags::RAW)) // Handle the field switch (fields[i].mode) { @@ -1334,10 +1354,10 @@ static void IndexFields(lua_State *state, int base, struct_identity *pstruct, bo } // Do not add invalid globals to the enumeration order - if (globals && !*(void**)fields[i].offset) + if ((flags & IndexFieldsFlags::GLOBALS) && !*(void**)fields[i].offset) add_to_enum = false; - if (add_to_enum) + if (add_to_enum || (flags & IndexFieldsFlags::RAW)) AssociateId(state, base+3, ++cnt, name.c_str()); lua_pushlightuserdata(state, (void*)&fields[i]); @@ -1345,10 +1365,168 @@ static void IndexFields(lua_State *state, int base, struct_identity *pstruct, bo } } +static void PushTypeIdentity(lua_State *state, const type_identity *id) +{ + lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_TYPEID_TABLE_TOKEN); + lua_rawgetp(state, -1, id); + lua_remove(state, -2); // TYPEID_TABLE +} + +static void PushFieldInfoSubTable(lua_State *state, const struct_field_info *field) +{ + if (!field) { + lua_pushnil(state); + return; + } + + lua_newtable(state); // new field info + Lua::TableInsert(state, "mode", field->mode); + Lua::TableInsert(state, "name", field->name); + Lua::TableInsert(state, "offset", field->offset); + Lua::TableInsert(state, "count", field->count); + + if (field->type) { + Lua::TableInsert(state, "type_name", field->type->getFullName()); + + lua_pushlightuserdata(state, field->type); + lua_setfield(state, -2, "type_identity"); + + PushTypeIdentity(state, field->type); + lua_setfield(state, -2, "type"); + } + + if (field->extra) { + if (field->extra->index_enum) { + PushTypeIdentity(state, field->extra->index_enum); + lua_setfield(state, -2, "index_enum"); + } + if (field->extra->ref_target) { + PushTypeIdentity(state, field->extra->ref_target); + lua_setfield(state, -2, "ref_target"); + } + if (field->extra->union_tag_field) { + Lua::TableInsert(state, "union_tag_field", field->extra->union_tag_field); + } + if (field->extra->union_tag_attr) { + Lua::TableInsert(state, "union_tag_attr", field->extra->union_tag_attr); + } + if (field->extra->original_name) { + Lua::TableInsert(state, "original_name", field->extra->original_name); + } + } +} + +/** + * Metamethod: __index for struct._fields + * + * upvalue 1: name -> struct_field_info* table + */ +static int meta_fieldinfo_index(lua_State *state) +{ + luaL_checktype(state, -1, LUA_TSTRING); + + lua_gettable(state, lua_upvalueindex(1)); + auto field = static_cast(lua_touserdata(state, -1)); + lua_pop(state, 1); + PushFieldInfoSubTable(state, field); + + return 1; +} + +/** + * Metamethod: iterator for struct._fields + * + * upvalue 1: name -> struct_field_info* table + * upvalue 3: field table (int <-> name) + */ +static int meta_fieldinfo_next(lua_State *state) +{ + if (lua_gettop(state) < 2) lua_pushnil(state); + + int len = lua_rawlen(state, UPVAL_FIELDTABLE); + int idx = cur_iter_index(state, len+1, 2, 0); + if (idx == len) + return 0; + + lua_rawgeti(state, UPVAL_FIELDTABLE, idx+1); + + // modified from meta_struct_next: + // retrieve the struct_field_info* from the table and convert it + lua_dup(state); + lua_gettable(state, lua_upvalueindex(1)); + auto field = static_cast(lua_touserdata(state, -1)); + lua_pop(state, 1); + PushFieldInfoSubTable(state, field); + + return 2; +} + +static void AddFieldInfoTable(lua_State *state, int ftable_idx, struct_identity *pstruct) +{ + Lua::StackUnwinder base{state}; + + // metatable + lua_newtable(state); + int ix_meta = lua_gettop(state); + + // field info table (name -> struct_field_info*) + lua_newtable(state); + int ix_fieldinfo = lua_gettop(state); + + // field iter table (int <-> name) + lua_newtable(state); + int ix_fielditer = lua_gettop(state); + IndexFields(state, base, pstruct, IndexFieldsFlags::RAW); + + PushStructMethod(state, ix_meta, ix_fielditer, meta_fieldinfo_next); + // change upvalue 1 to the field info table since we don't need the original + lua_pushvalue(state, ix_fieldinfo); + lua_setupvalue(state, -2, 1); + SetPairsMethod(state, ix_meta, "__pairs"); + + // field table (name -> table representation of struct_field_info) + lua_newtable(state); + int ix_fields = lua_gettop(state); + + // wrapper table (empty, indexes into field table with metamethods) + lua_newtable(state); + int ix_wrapper = lua_gettop(state); + + // set up metatable for the wrapper + // use field table for __index + lua_pushstring(state, "__index"); + lua_pushvalue(state, ix_fieldinfo); + lua_pushcclosure(state, meta_fieldinfo_index, 1); + lua_settable(state, ix_meta); + + // use change_error() for __newindex + lua_pushstring(state, "__newindex"); + lua_getfield(state, LUA_REGISTRYINDEX, DFHACK_CHANGEERROR_NAME); + lua_settable(state, ix_meta); + + lua_pushvalue(state, ix_meta); + lua_setmetatable(state, ix_wrapper); + + // convert field info table (struct_field_info) to field table (lua tables) + lua_pushnil(state); // initial key for next() + while (lua_next(state, ix_fieldinfo)) { + auto field = static_cast(lua_touserdata(state, -1)); + lua_pushvalue(state, -2); // field name + PushFieldInfoSubTable(state, field); + lua_settable(state, ix_fields); + lua_pop(state, 1); // struct_field_info + } + + // lua_pushvalue(state, ix_fields); + // freeze_table(state); // TODO: figure out why this creates an __index cycle for nonexistent fields + lua_pushvalue(state, ix_wrapper); + lua_setfield(state, ftable_idx, "_fields"); +} + void LuaWrapper::IndexStatics(lua_State *state, int meta_idx, int ftable_idx, struct_identity *pstruct) { // stack: metatable fieldtable - + AddFieldInfoTable(state, ftable_idx, pstruct); for (struct_identity *p = pstruct; p; p = p->getParent()) { auto fields = p->getFields(); @@ -1384,8 +1562,7 @@ static void MakeFieldMetatable(lua_State *state, struct_identity *pstruct, // Index the fields lua_newtable(state); - - IndexFields(state, base, pstruct, globals); + IndexFields(state, base, pstruct, globals ? IndexFieldsFlags::GLOBALS : 0); // Add the iteration metamethods PushStructMethod(state, base+1, base+3, iterator); diff --git a/library/LuaWrapper.cpp b/library/LuaWrapper.cpp index 59bd96732..9d2357c70 100644 --- a/library/LuaWrapper.cpp +++ b/library/LuaWrapper.cpp @@ -685,7 +685,8 @@ static int meta_new(lua_State *state) type_identity *id = get_object_identity(state, 1, "df.new()", true); - void *ptr; + void *ptr = nullptr; + std::string err_context; // Support arrays of primitive types if (argc == 2) @@ -703,11 +704,22 @@ static int meta_new(lua_State *state) } else { - ptr = id->allocate(); + try { + ptr = id->allocate(); + } + catch (std::exception &e) { + if (e.what()) { + err_context = e.what(); + } + } } if (!ptr) - luaL_error(state, "Cannot allocate %s", id->getFullName().c_str()); + luaL_error(state, "Cannot allocate %s%s%s", + id->getFullName().c_str(), + err_context.empty() ? "" : ": ", + err_context.c_str() + ); if (lua_isuserdata(state, 1)) { @@ -1658,6 +1670,7 @@ static void RenderType(lua_State *state, compound_identity *node) { RenderTypeChildren(state, node->getScopeChildren()); + IndexStatics(state, ix_meta, ftable, (struct_identity*)node); lua_pushlightuserdata(state, node); lua_setfield(state, ftable, "_identity"); diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index b959e756e..a90cf1208 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -27,6 +27,8 @@ distribution. #include "MiscUtils.h" #include "ColorText.h" +#include "modules/DFSDL.h" + #ifndef LINUX_BUILD // We don't want min and max macros #define NOMINMAX @@ -34,6 +36,7 @@ distribution. #else #include #include + #include #endif #include @@ -470,3 +473,29 @@ DFHACK_EXPORT std::string DF2CONSOLE(DFHack::color_ostream &out, const std::stri { return out.is_console() ? DF2CONSOLE(in) : in; } + +DFHACK_EXPORT std::string cxx_demangle(const std::string &mangled_name, std::string *status_out) +{ +#ifdef __GNUC__ + int status; + char *demangled = abi::__cxa_demangle(mangled_name.c_str(), nullptr, nullptr, &status); + std::string out; + if (demangled) { + out = demangled; + free(demangled); + } + if (status_out) { + if (status == 0) *status_out = "success"; + else if (status == -1) *status_out = "memory allocation failure"; + else if (status == -2) *status_out = "invalid mangled name"; + else if (status == -3) *status_out = "invalid arguments"; + else *status_out = "unknown error"; + } + return out; +#else + if (status_out) { + *status_out = "not implemented on this platform"; + } + return ""; +#endif +} diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp index e50750015..0509f5710 100644 --- a/library/Process-linux.cpp +++ b/library/Process-linux.cpp @@ -138,7 +138,7 @@ void Process::getMemRanges( vector & ranges ) { t_memrange temp; temp.name[0] = 0; - sscanf(buffer, "%zx-%zx %s %zx %2zx:%2zx %zu %[^\n]", + sscanf(buffer, "%zx-%zx %s %zx %zx:%zx %zu %[^\n]", &start, &end, (char*)&permissions, diff --git a/library/include/Core.h b/library/include/Core.h index 696be4ead..b470ebbde 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -40,8 +40,6 @@ distribution. #include #include -#include "RemoteClient.h" - #define DFH_MOD_SHIFT 1 #define DFH_MOD_CTRL 2 #define DFH_MOD_ALT 4 @@ -74,6 +72,17 @@ namespace DFHack struct Hide; } + enum command_result + { + CR_LINK_FAILURE = -3, // RPC call failed due to I/O or protocol error + CR_NEEDS_CONSOLE = -2, // Attempt to call interactive command without console + CR_NOT_IMPLEMENTED = -1, // Command not implemented, or plugin not loaded + CR_OK = 0, // Success + CR_FAILURE = 1, // Failure + CR_WRONG_USAGE = 2, // Wrong arguments or ui state + CR_NOT_FOUND = 3 // Target object not found (for RPC mainly) + }; + enum state_change_event { SC_UNKNOWN = -1, @@ -97,10 +106,14 @@ namespace DFHack StateChangeScript(state_change_event event, std::string path, bool save_specific = false) :event(event), path(path), save_specific(save_specific) { } - bool operator==(const StateChangeScript& other) + bool const operator==(const StateChangeScript& other) { return event == other.event && path == other.path && save_specific == other.save_specific; } + bool const operator!=(const StateChangeScript& other) + { + return !(operator==(other)); + } }; // Core is a singleton. Why? Because it is closely tied to SDL calls. It tracks the global state of DF. @@ -112,15 +125,11 @@ namespace DFHack friend void ::dfhooks_shutdown(); friend void ::dfhooks_update(); friend void ::dfhooks_prerender(); - friend bool ::dfhooks_sdl_event(SDL::Event* event); + friend bool ::dfhooks_sdl_event(SDL_Event* event); friend bool ::dfhooks_ncurses_key(int key); public: /// Get the single Core instance or make one. - static Core& getInstance() - { - static Core instance; - return instance; - } + static Core& getInstance(); /// check if the activity lock is owned by this thread bool isSuspended(void); /// Is everything OK? @@ -150,6 +159,9 @@ namespace DFHack std::string findScript(std::string name); void getScriptPaths(std::vector *dest); + bool getSuppressDuplicateKeyboardEvents(); + void setSuppressDuplicateKeyboardEvents(bool suppress); + bool ClearKeyBindings(std::string keyspec); bool AddKeyBinding(std::string keyspec, std::string cmdline); std::vector ListKeyBindings(std::string keyspec); @@ -168,7 +180,7 @@ namespace DFHack bool isWorldLoaded() { return (last_world_data_ptr != NULL); } bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } - static df::viewscreen *getTopViewscreen() { return getInstance().top_viewscreen; } + static df::viewscreen *getTopViewscreen(); DFHack::Console &getConsole() { return con; } @@ -195,7 +207,7 @@ namespace DFHack bool InitSimulationThread(); int Update (void); int Shutdown (void); - bool DFH_SDL_Event(SDL::Event* event); + bool DFH_SDL_Event(SDL_Event* event); bool ncurses_wgetch(int in, int & out); bool DFH_ncurses_key(int key); @@ -240,8 +252,8 @@ namespace DFHack }; int8_t modstate; + bool suppress_duplicate_keyboard_events; std::map > key_bindings; - std::map hotkey_states; std::string hotkey_cmd; enum hotkey_set_t { NO, diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 174156e76..c440c1eb7 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -265,6 +265,7 @@ namespace DFHack type_identity *ref_target; const char *union_tag_field; const char *union_tag_attr; + const char *original_name; }; struct struct_field_info { @@ -497,11 +498,29 @@ 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; + } + + 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; + } + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" template void *allocator_fn(void *out, const void *in) { - if (out) { *(T*)out = *(const T*)in; return out; } + if (out) { return allocator_try_assign(out, in); } else if (in) { delete (T*)in; return (T*)in; } else return new T(); } @@ -514,6 +533,13 @@ namespace df 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(); + } + template struct identity_traits { static compound_identity *get() { return &T::_identity; } diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index fd47429b9..035f59d4b 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -25,13 +25,21 @@ distribution. #pragma once #include -#include +#include +#include +#include #include +#include +#include #include -#include #include "DataDefs.h" +namespace std { + class condition_variable; + class mutex; +}; + /* * Definitions of DFHack namespace structs used by generated headers. */ @@ -541,6 +549,15 @@ namespace df #define INTEGER_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(integer, type) #define FLOAT_IDENTITY_TRAITS(type) NUMBER_IDENTITY_TRAITS(float, type) +// the space after the use of "type" in OPAQUE_IDENTITY_TRAITS is _required_ +// without it the macro generates a syntax error when type is a template specification + +#define OPAQUE_IDENTITY_TRAITS(type) \ + template<> struct DFHACK_EXPORT identity_traits { \ + static opaque_identity identity; \ + static opaque_identity *get() { return &identity; } \ + }; + INTEGER_IDENTITY_TRAITS(char); INTEGER_IDENTITY_TRAITS(signed char); INTEGER_IDENTITY_TRAITS(unsigned char); @@ -554,6 +571,24 @@ namespace df INTEGER_IDENTITY_TRAITS(unsigned long long); FLOAT_IDENTITY_TRAITS(float); FLOAT_IDENTITY_TRAITS(double); + OPAQUE_IDENTITY_TRAITS(std::condition_variable); + OPAQUE_IDENTITY_TRAITS(std::fstream); + OPAQUE_IDENTITY_TRAITS(std::mutex); + OPAQUE_IDENTITY_TRAITS(std::future); + OPAQUE_IDENTITY_TRAITS(std::function); + OPAQUE_IDENTITY_TRAITS(std::optional >); + +#ifdef BUILD_DFHACK_LIB + template + struct DFHACK_EXPORT identity_traits> { + static opaque_identity *get() { + typedef std::shared_ptr type; + static std::string name = std::string("shared_ptr<") + typeid(T).name() + ">"; + static opaque_identity identity(sizeof(type), allocator_noassign_fn, name); + return &identity; + } + }; +#endif template<> struct DFHACK_EXPORT identity_traits { static bool_identity identity; @@ -565,11 +600,6 @@ namespace df static stl_string_identity *get() { return &identity; } }; - template<> struct DFHACK_EXPORT identity_traits { - static opaque_identity identity; - static opaque_identity *get() { return &identity; } - }; - template<> struct DFHACK_EXPORT identity_traits { static ptr_string_identity identity; static ptr_string_identity *get() { return &identity; } @@ -598,6 +628,7 @@ namespace df #undef NUMBER_IDENTITY_TRAITS #undef INTEGER_IDENTITY_TRAITS #undef FLOAT_IDENTITY_TRAITS +#undef OPAQUE_IDENTITY_TRAITS // Container declarations @@ -638,6 +669,10 @@ namespace df static container_identity *get(); }; + template struct identity_traits> { + static container_identity *get(); + }; + template<> struct identity_traits > { static bit_array_identity identity; static bit_container_identity *get() { return &identity; } @@ -715,6 +750,13 @@ namespace df return &identity; } + template + inline container_identity *identity_traits>::get() { + typedef std::unordered_map container; + static ro_stl_assoc_container_identity identity("unordered_map", identity_traits::get(), identity_traits::get()); + return &identity; + } + template inline bit_container_identity *identity_traits >::get() { static bit_array_identity identity(identity_traits::get()); diff --git a/library/include/DataStaticsFields.inc b/library/include/DataStaticsFields.inc new file mode 100644 index 000000000..97a036b58 --- /dev/null +++ b/library/include/DataStaticsFields.inc @@ -0,0 +1,15 @@ +#include + +#include "DataFuncs.h" + +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#endif + +#define TID(type) (&identity_traits< type >::identity) + +#define FLD(mode, name) struct_field_info::mode, #name, offsetof(CUR_STRUCT, name) +#define GFLD(mode, name) struct_field_info::mode, #name, (size_t)&df::global::name +#define METHOD(mode, name) struct_field_info::mode, #name, 0, wrap_function(&CUR_STRUCT::name) +#define METHOD_N(mode, func, name) struct_field_info::mode, #name, 0, wrap_function(&CUR_STRUCT::func) +#define FLD_END struct_field_info::END diff --git a/library/include/Hooks.h b/library/include/Hooks.h index 5856fb44f..fb72fdbe3 100644 --- a/library/include/Hooks.h +++ b/library/include/Hooks.h @@ -24,61 +24,11 @@ distribution. #pragma once -/* - * Some much needed SDL fakery. - */ - -#include "Pragma.h" -#include "Export.h" -#include -#include - -#include "modules/Graphic.h" - -// function and variable pointer... we don't try to understand what SDL does here -typedef void * fPtr; -typedef void * vPtr; -struct WINDOW; -namespace SDL -{ - union Event; -} - -// these functions are here because they call into DFHack::Core and therefore need to -// be declared as friend functions/known -#ifdef _DARWIN -DFhackCExport int DFH_SDL_NumJoysticks(void); -DFhackCExport void DFH_SDL_Quit(void); -DFhackCExport int DFH_SDL_PollEvent(SDL::Event* event); -DFhackCExport int DFH_SDL_Init(uint32_t flags); -DFhackCExport int DFH_wgetch(WINDOW * win); -#endif -DFhackCExport int SDL_NumJoysticks(void); -DFhackCExport void SDL_Quit(void); -DFhackCExport int SDL_PollEvent(SDL::Event* event); -DFhackCExport int SDL_PushEvent(SDL::Event* event); -DFhackCExport int SDL_Init(uint32_t flags); -DFhackCExport int wgetch(WINDOW * win); - -DFhackCExport int SDL_UpperBlit(DFHack::DFSDL_Surface* src, DFHack::DFSDL_Rect* srcrect, DFHack::DFSDL_Surface* dst, DFHack::DFSDL_Rect* dstrect); -DFhackCExport vPtr SDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, - uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFhackCExport vPtr SDL_CreateRGBSurfaceFrom(vPtr pixels, int width, int height, int depth, int pitch, - uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); -DFhackCExport void SDL_FreeSurface(vPtr surface); -DFhackCExport vPtr SDL_ConvertSurface(vPtr surface, vPtr format, uint32_t flags); -DFhackCExport int SDL_LockSurface(vPtr surface); -DFhackCExport void SDL_UnlockSurface(vPtr surface); -DFhackCExport uint8_t SDL_GetMouseState(int *x, int *y); -DFhackCExport void * SDL_GetVideoSurface(void); - -DFhackCExport int SDL_SemWait(vPtr sem); -DFhackCExport int SDL_SemPost(vPtr sem); +union SDL_Event; -// new Hooks API DFhackCExport void dfhooks_init(); DFhackCExport void dfhooks_shutdown(); DFhackCExport void dfhooks_update(); DFhackCExport void dfhooks_prerender(); -DFhackCExport bool dfhooks_sdl_event(SDL::Event* event); +DFhackCExport bool dfhooks_sdl_event(SDL_Event* event); DFhackCExport bool dfhooks_ncurses_key(int key); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index cab3ee9cc..188d63854 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -410,6 +410,17 @@ namespace DFHack {namespace Lua { } } + template + requires std::is_arithmetic_v + void GetVector(lua_State *state, std::vector &pvec, int idx = 1) { + lua_pushnil(state); // first key + while (lua_next(state, idx) != 0) + { + pvec.push_back(lua_tointeger(state, -1)); + lua_pop(state, 1); // remove value, leave key + } + } + DFHACK_EXPORT void GetVector(lua_State *state, std::vector &pvec, int idx = 1); DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true); diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index d14bdb6e9..06915fde5 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -61,28 +61,6 @@ namespace DFHack { class color_ostream; } -/*! \namespace dts - * std.reverse() == dts, The namespace that include forward compatible helpers - * which can be used from newer standards. The preprocessor check prefers - * standard version if one is available. The standard version gets imported with - * using. - */ -namespace dts { -// Check if lib supports the feature test macro or version is over c++14. -#if __cpp_lib_make_unique < 201304 && __cplusplus < 201402L -//! Insert c++14 make_unique to be forward compatible. Array versions are -//! missing -template -typename std::enable_if::value, std::unique_ptr >::type -make_unique(Args&&... args) -{ - return std::unique_ptr{new T{std::forward(args)...}}; -} -#else /* >= c++14 */ -using std::make_unique; -#endif -} - template void print_bits ( T val, std::ostream& out ) { @@ -496,3 +474,5 @@ DFHACK_EXPORT std::string UTF2DF(const std::string &in); DFHACK_EXPORT std::string DF2UTF(const std::string &in); DFHACK_EXPORT std::string DF2CONSOLE(const std::string &in); DFHACK_EXPORT std::string DF2CONSOLE(DFHack::color_ostream &out, const std::string &in); + +DFHACK_EXPORT std::string cxx_demangle(const std::string &mangled_name, std::string *status_out); diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 78c5e5dc8..5cf8bb390 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -35,8 +35,6 @@ distribution. #include "Core.h" #include "DataFuncs.h" -#include "RemoteClient.h" - typedef struct lua_State lua_State; namespace tthread @@ -300,14 +298,16 @@ namespace DFHack }; #define DFHACK_PLUGIN_AUX(m_plugin_name, is_dev) \ +extern "C" { \ DFhackDataExport const char * plugin_name = m_plugin_name;\ DFhackDataExport const char * plugin_version = DFHack::Version::dfhack_version();\ DFhackDataExport const char * plugin_git_description = DFHack::Version::git_description();\ DFhackDataExport int plugin_abi_version = DFHack::Version::dfhack_abi_version();\ DFhackDataExport DFHack::Plugin *plugin_self = NULL;\ - std::vector _plugin_globals;\ - DFhackDataExport std::vector* plugin_globals = &_plugin_globals; \ - DFhackDataExport bool plugin_dev = is_dev; + std::vector plugin_globals_noptr;\ + DFhackDataExport std::vector* plugin_globals = &plugin_globals_noptr; \ + DFhackDataExport bool plugin_dev = is_dev; \ +} /// You have to include DFHACK_PLUGIN("plugin_name") in every plugin you write - just once. Ideally at the top of the main file. #ifdef DEV_PLUGIN diff --git a/library/include/RemoteClient.h b/library/include/RemoteClient.h index e71b985cd..921d351c3 100644 --- a/library/include/RemoteClient.h +++ b/library/include/RemoteClient.h @@ -26,6 +26,7 @@ distribution. #include "Pragma.h" #include "Export.h" #include "ColorText.h" +#include "Core.h" class CPassiveSocket; class CActiveSocket; @@ -39,17 +40,6 @@ namespace DFHack using dfproto::IntMessage; using dfproto::StringMessage; - enum command_result - { - CR_LINK_FAILURE = -3, // RPC call failed due to I/O or protocol error - CR_NEEDS_CONSOLE = -2, // Attempt to call interactive command without console - CR_NOT_IMPLEMENTED = -1, // Command not implemented, or plugin not loaded - CR_OK = 0, // Success - CR_FAILURE = 1, // Failure - CR_WRONG_USAGE = 2, // Wrong arguments or ui state - CR_NOT_FOUND = 3 // Target object not found (for RPC mainly) - }; - enum DFHackReplyCode : int16_t { RPC_REPLY_RESULT = -1, RPC_REPLY_FAIL = -2, diff --git a/library/include/SDL_events.h b/library/include/SDL_events.h deleted file mode 100644 index 80067f07d..000000000 --- a/library/include/SDL_events.h +++ /dev/null @@ -1,210 +0,0 @@ -/* - SDL - Simple DirectMedia Layer - Copyright (C) 1997-2009 Sam Lantinga - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - Sam Lantinga - slouken@libsdl.org -*/ - -// Fake - only structs. Shamelessly pilfered from the SDL library. -// Needed for processing its event types without polluting our namespaces with C garbage - -#pragma once -#include "SDL_keyboard.h" - -namespace SDL -{ - enum ButtonState - { - BTN_RELEASED = 0, - BTN_PRESSED = 1 - }; - - /** Event enumerations */ - enum EventType - { - ET_NOEVENT = 0, /**< Unused (do not remove) */ - ET_ACTIVEEVENT, /**< Application loses/gains visibility */ - ET_KEYDOWN, /**< Keys pressed */ - ET_KEYUP, /**< Keys released */ - ET_MOUSEMOTION, /**< Mouse moved */ - ET_MOUSEBUTTONDOWN, /**< Mouse button pressed */ - ET_MOUSEBUTTONUP, /**< Mouse button released */ - ET_JOYAXISMOTION, /**< Joystick axis motion */ - ET_JOYBALLMOTION, /**< Joystick trackball motion */ - ET_JOYHATMOTION, /**< Joystick hat position change */ - ET_JOYBUTTONDOWN, /**< Joystick button pressed */ - ET_JOYBUTTONUP, /**< Joystick button released */ - ET_QUIT, /**< User-requested quit */ - ET_SYSWMEVENT, /**< System specific event */ - ET_EVENT_RESERVEDA, /**< Reserved for future use.. */ - ET_EVENT_RESERVEDB, /**< Reserved for future use.. */ - ET_VIDEORESIZE, /**< User resized video mode */ - ET_VIDEOEXPOSE, /**< Screen needs to be redrawn */ - ET_EVENT_RESERVED2, /**< Reserved for future use.. */ - ET_EVENT_RESERVED3, /**< Reserved for future use.. */ - ET_EVENT_RESERVED4, /**< Reserved for future use.. */ - ET_EVENT_RESERVED5, /**< Reserved for future use.. */ - ET_EVENT_RESERVED6, /**< Reserved for future use.. */ - ET_EVENT_RESERVED7, /**< Reserved for future use.. */ - /** Events ET_USEREVENT through ET_MAXEVENTS-1 are for your use */ - ET_USEREVENT = 24, - /** This last event is only for bounding internal arrays - * It is the number of bits in the event mask datatype -- Uint32 - */ - ET_NUMEVENTS = 32 - }; - - /** Application visibility event structure */ - struct ActiveEvent - { - uint8_t type; /**< ET_ACTIVEEVENT */ - uint8_t gain; /**< Whether given states were gained or lost (1/0) */ - uint8_t state; /**< A mask of the focus states */ - }; - - /** Keyboard event structure */ - struct KeyboardEvent - { - uint8_t type; /**< ET_KEYDOWN or ET_KEYUP */ - uint8_t which; /**< The keyboard device index */ - uint8_t state; /**< BTN_PRESSED or BTN_RELEASED */ - keysym ksym; - }; - - /** Mouse motion event structure */ - struct MouseMotionEvent - { - uint8_t type; /**< ET_MOUSEMOTION */ - uint8_t which; /**< The mouse device index */ - uint8_t state; /**< The current button state */ - uint16_t x, y; /**< The X/Y coordinates of the mouse */ - int16_t xrel; /**< The relative motion in the X direction */ - int16_t yrel; /**< The relative motion in the Y direction */ - }; - - /** Mouse button event structure */ - struct MouseButtonEvent - { - uint8_t type; /**< ET_MOUSEBUTTONDOWN or ET_MOUSEBUTTONUP */ - uint8_t which; /**< The mouse device index */ - uint8_t button; /**< The mouse button index */ - uint8_t state; /**< BTN_PRESSED or BTN_RELEASED */ - uint16_t x, y; /**< The X/Y coordinates of the mouse at press time */ - }; - - /** Joystick axis motion event structure */ - struct JoyAxisEvent - { - uint8_t type; /**< ET_JOYAXISMOTION */ - uint8_t which; /**< The joystick device index */ - uint8_t axis; /**< The joystick axis index */ - int16_t value; /**< The axis value (range: -32768 to 32767) */ - }; - - /** Joystick trackball motion event structure */ - struct JoyBallEvent - { - uint8_t type; /**< ET_JOYBALLMOTION */ - uint8_t which; /**< The joystick device index */ - uint8_t ball; /**< The joystick trackball index */ - int16_t xrel; /**< The relative motion in the X direction */ - int16_t yrel; /**< The relative motion in the Y direction */ - }; - - /** Joystick hat position change event structure */ - struct JoyHatEvent - { - uint8_t type; /**< ET_JOYHATMOTION */ - uint8_t which; /**< The joystick device index */ - uint8_t hat; /**< The joystick hat index */ - uint8_t value; /**< The hat position value: - * SDL_HAT_LEFTUP SDL_HAT_UP SDL_HAT_RIGHTUP - * SDL_HAT_LEFT SDL_HAT_CENTERED SDL_HAT_RIGHT - * SDL_HAT_LEFTDOWN SDL_HAT_DOWN SDL_HAT_RIGHTDOWN - * Note that zero means the POV is centered. - */ - }; - - /** Joystick button event structure */ - struct JoyButtonEvent - { - uint8_t type; /**< ET_JOYBUTTONDOWN or ET_JOYBUTTONUP */ - uint8_t which; /**< The joystick device index */ - uint8_t button; /**< The joystick button index */ - uint8_t state; /**< BTN_PRESSED or BTN_RELEASED */ - }; - - /** The "window resized" event - * When you get this event, you are responsible for setting a new video - * mode with the new width and height. - */ - struct ResizeEvent - { - uint8_t type; /**< ET_VIDEORESIZE */ - int w; /**< New width */ - int h; /**< New height */ - }; - - /** The "screen redraw" event */ - struct ExposeEvent - { - uint8_t type; /**< ET_VIDEOEXPOSE */ - }; - - /** The "quit requested" event */ - struct QuitEvent - { - uint8_t type; /**< ET_QUIT */ - }; - - /** A user-defined event type */ - struct UserEvent - { - uint8_t type; /**< ETL_USEREVENT through ET_NUMEVENTS-1 */ - int code; /**< User defined event code */ - void *data1; /**< User defined data pointer */ - void *data2; /**< User defined data pointer */ - }; - - /** If you want to use this event, you should include SDL_syswm.h */ - struct SysWMmsg; - struct SysWMEvent - { - uint8_t type; - SysWMmsg *msg; - }; - - /** General event structure */ - union Event - { - uint8_t type; - ActiveEvent active; - KeyboardEvent key; - MouseMotionEvent motion; - MouseButtonEvent button; - JoyAxisEvent jaxis; - JoyBallEvent jball; - JoyHatEvent jhat; - JoyButtonEvent jbutton; - ResizeEvent resize; - ExposeEvent expose; - QuitEvent quit; - UserEvent user; - SysWMEvent syswm; - }; -} diff --git a/library/include/SDL_keyboard.h b/library/include/SDL_keyboard.h deleted file mode 100644 index 1d58bd63f..000000000 --- a/library/include/SDL_keyboard.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - SDL - Simple DirectMedia Layer - Copyright (C) 1997-2009 Sam Lantinga - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - Sam Lantinga - slouken@libsdl.org -*/ - -// Fake - only structs. Shamelessly pilfered from the SDL library. -// Needed for processing its event types without polluting our namespaces with C garbage - -#pragma once -#include "SDL_keysym.h" -#include - -namespace SDL -{ - /** Keysym structure - * - * - The scancode is hardware dependent, and should not be used by general - * applications. If no hardware scancode is available, it will be 0. - * - * - The 'unicode' translated character is only available when character - * translation is enabled by the SDL_EnableUNICODE() API. If non-zero, - * this is a UNICODE character corresponding to the keypress. If the - * high 9 bits of the character are 0, then this maps to the equivalent - * ASCII character: - * @code - * char ch; - * if ( (keysym.unicode & 0xFF80) == 0 ) { - * ch = keysym.unicode & 0x7F; - * } else { - * An international character.. - * } - * @endcode - */ - typedef struct keysym - { - uint8_t scancode; /**< hardware specific scancode */ - Key sym; /**< SDL virtual keysym */ - Mod mod; /**< current key modifiers */ - uint16_t unicode; /**< translated character */ - } keysym; - - /** This is the mask which refers to all hotkey bindings */ - #define ALL_HOTKEYS 0xFFFFFFFF -} diff --git a/library/include/SDL_keysym.h b/library/include/SDL_keysym.h deleted file mode 100644 index e19f786af..000000000 --- a/library/include/SDL_keysym.h +++ /dev/null @@ -1,329 +0,0 @@ -/* - SDL - Simple DirectMedia Layer - Copyright (C) 1997-2009 Sam Lantinga - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - Sam Lantinga - slouken@libsdl.org -*/ - -// Fake - only structs. Shamelessly pilfered from the SDL library. -// Needed for processing its event types without polluting our namespaces with C garbage - -#pragma once - -namespace SDL -{ - /** What we really want is a mapping of every raw key on the keyboard. - * To support international keyboards, we use the range 0xA1 - 0xFF - * as international virtual keycodes. We'll follow in the footsteps of X11... - * @brief The names of the keys - */ - enum Key - { - /** @name ASCII mapped keysyms - * The keyboard syms have been cleverly chosen to map to ASCII - */ - /*@{*/ - K_UNKNOWN = 0, - K_FIRST = 0, - K_BACKSPACE = 8, - K_TAB = 9, - K_CLEAR = 12, - K_RETURN = 13, - K_PAUSE = 19, - K_ESCAPE = 27, - K_SPACE = 32, - K_EXCLAIM = 33, - K_QUOTEDBL = 34, - K_HASH = 35, - K_DOLLAR = 36, - K_AMPERSAND = 38, - K_QUOTE = 39, - K_LEFTPAREN = 40, - K_RIGHTPAREN = 41, - K_ASTERISK = 42, - K_PLUS = 43, - K_COMMA = 44, - K_MINUS = 45, - K_PERIOD = 46, - K_SLASH = 47, - K_0 = 48, - K_1 = 49, - K_2 = 50, - K_3 = 51, - K_4 = 52, - K_5 = 53, - K_6 = 54, - K_7 = 55, - K_8 = 56, - K_9 = 57, - K_COLON = 58, - K_SEMICOLON = 59, - K_LESS = 60, - K_EQUALS = 61, - K_GREATER = 62, - K_QUESTION = 63, - K_AT = 64, - /* - Skip uppercase letters - */ - K_LEFTBRACKET = 91, - K_BACKSLASH = 92, - K_RIGHTBRACKET = 93, - K_CARET = 94, - K_UNDERSCORE = 95, - K_BACKQUOTE = 96, - K_a = 97, - K_b = 98, - K_c = 99, - K_d = 100, - K_e = 101, - K_f = 102, - K_g = 103, - K_h = 104, - K_i = 105, - K_j = 106, - K_k = 107, - K_l = 108, - K_m = 109, - K_n = 110, - K_o = 111, - K_p = 112, - K_q = 113, - K_r = 114, - K_s = 115, - K_t = 116, - K_u = 117, - K_v = 118, - K_w = 119, - K_x = 120, - K_y = 121, - K_z = 122, - K_DELETE = 127, - /* End of ASCII mapped keysyms */ - /*@}*/ - - /** @name International keyboard syms */ - /*@{*/ - K_WORLD_0 = 160, /* 0xA0 */ - K_WORLD_1 = 161, - K_WORLD_2 = 162, - K_WORLD_3 = 163, - K_WORLD_4 = 164, - K_WORLD_5 = 165, - K_WORLD_6 = 166, - K_WORLD_7 = 167, - K_WORLD_8 = 168, - K_WORLD_9 = 169, - K_WORLD_10 = 170, - K_WORLD_11 = 171, - K_WORLD_12 = 172, - K_WORLD_13 = 173, - K_WORLD_14 = 174, - K_WORLD_15 = 175, - K_WORLD_16 = 176, - K_WORLD_17 = 177, - K_WORLD_18 = 178, - K_WORLD_19 = 179, - K_WORLD_20 = 180, - K_WORLD_21 = 181, - K_WORLD_22 = 182, - K_WORLD_23 = 183, - K_WORLD_24 = 184, - K_WORLD_25 = 185, - K_WORLD_26 = 186, - K_WORLD_27 = 187, - K_WORLD_28 = 188, - K_WORLD_29 = 189, - K_WORLD_30 = 190, - K_WORLD_31 = 191, - K_WORLD_32 = 192, - K_WORLD_33 = 193, - K_WORLD_34 = 194, - K_WORLD_35 = 195, - K_WORLD_36 = 196, - K_WORLD_37 = 197, - K_WORLD_38 = 198, - K_WORLD_39 = 199, - K_WORLD_40 = 200, - K_WORLD_41 = 201, - K_WORLD_42 = 202, - K_WORLD_43 = 203, - K_WORLD_44 = 204, - K_WORLD_45 = 205, - K_WORLD_46 = 206, - K_WORLD_47 = 207, - K_WORLD_48 = 208, - K_WORLD_49 = 209, - K_WORLD_50 = 210, - K_WORLD_51 = 211, - K_WORLD_52 = 212, - K_WORLD_53 = 213, - K_WORLD_54 = 214, - K_WORLD_55 = 215, - K_WORLD_56 = 216, - K_WORLD_57 = 217, - K_WORLD_58 = 218, - K_WORLD_59 = 219, - K_WORLD_60 = 220, - K_WORLD_61 = 221, - K_WORLD_62 = 222, - K_WORLD_63 = 223, - K_WORLD_64 = 224, - K_WORLD_65 = 225, - K_WORLD_66 = 226, - K_WORLD_67 = 227, - K_WORLD_68 = 228, - K_WORLD_69 = 229, - K_WORLD_70 = 230, - K_WORLD_71 = 231, - K_WORLD_72 = 232, - K_WORLD_73 = 233, - K_WORLD_74 = 234, - K_WORLD_75 = 235, - K_WORLD_76 = 236, - K_WORLD_77 = 237, - K_WORLD_78 = 238, - K_WORLD_79 = 239, - K_WORLD_80 = 240, - K_WORLD_81 = 241, - K_WORLD_82 = 242, - K_WORLD_83 = 243, - K_WORLD_84 = 244, - K_WORLD_85 = 245, - K_WORLD_86 = 246, - K_WORLD_87 = 247, - K_WORLD_88 = 248, - K_WORLD_89 = 249, - K_WORLD_90 = 250, - K_WORLD_91 = 251, - K_WORLD_92 = 252, - K_WORLD_93 = 253, - K_WORLD_94 = 254, - K_WORLD_95 = 255, /* 0xFF */ - /*@}*/ - - /** @name Numeric keypad */ - /*@{*/ - K_KP0 = 256, - K_KP1 = 257, - K_KP2 = 258, - K_KP3 = 259, - K_KP4 = 260, - K_KP5 = 261, - K_KP6 = 262, - K_KP7 = 263, - K_KP8 = 264, - K_KP9 = 265, - K_KP_PERIOD = 266, - K_KP_DIVIDE = 267, - K_KP_MULTIPLY = 268, - K_KP_MINUS = 269, - K_KP_PLUS = 270, - K_KP_ENTER = 271, - K_KP_EQUALS = 272, - /*@}*/ - - /** @name Arrows + Home/End pad */ - /*@{*/ - K_UP = 273, - K_DOWN = 274, - K_RIGHT = 275, - K_LEFT = 276, - K_INSERT = 277, - K_HOME = 278, - K_END = 279, - K_PAGEUP = 280, - K_PAGEDOWN = 281, - /*@}*/ - - /** @name Function keys */ - /*@{*/ - K_F1 = 282, - K_F2 = 283, - K_F3 = 284, - K_F4 = 285, - K_F5 = 286, - K_F6 = 287, - K_F7 = 288, - K_F8 = 289, - K_F9 = 290, - K_F10 = 291, - K_F11 = 292, - K_F12 = 293, - K_F13 = 294, - K_F14 = 295, - K_F15 = 296, - /*@}*/ - - /** @name Key state modifier keys */ - /*@{*/ - K_NUMLOCK = 300, - K_CAPSLOCK = 301, - K_SCROLLOCK = 302, - K_RSHIFT = 303, - K_LSHIFT = 304, - K_RCTRL = 305, - K_LCTRL = 306, - K_RALT = 307, - K_LALT = 308, - K_RMETA = 309, - K_LMETA = 310, - K_LSUPER = 311, /**< Left "Windows" key */ - K_RSUPER = 312, /**< Right "Windows" key */ - K_MODE = 313, /**< "Alt Gr" key */ - K_COMPOSE = 314, /**< Multi-key compose key */ - /*@}*/ - - /** @name Miscellaneous function keys */ - /*@{*/ - K_HELP = 315, - K_PRINT = 316, - K_SYSREQ = 317, - K_BREAK = 318, - K_MENU = 319, - K_POWER = 320, /**< Power Macintosh power key */ - K_EURO = 321, /**< Some european keyboards */ - K_UNDO = 322, /**< Atari keyboard has Undo */ - /*@}*/ - - /* Add any other keys here */ - - K_LAST - }; - - /** Enumeration of valid key mods (possibly OR'd together) */ - enum Mod { - KMOD_NONE = 0x0000, - KMOD_LSHIFT= 0x0001, - KMOD_RSHIFT= 0x0002, - KMOD_LCTRL = 0x0040, - KMOD_RCTRL = 0x0080, - KMOD_LALT = 0x0100, - KMOD_RALT = 0x0200, - KMOD_LMETA = 0x0400, - KMOD_RMETA = 0x0800, - KMOD_NUM = 0x1000, - KMOD_CAPS = 0x2000, - KMOD_MODE = 0x4000, - KMOD_RESERVED = 0x8000, - KMOD_CTRL = (KMOD_LCTRL|KMOD_RCTRL), - KMOD_SHIFT = (KMOD_LSHIFT|KMOD_RSHIFT), - KMOD_ALT = (KMOD_LALT|KMOD_RALT), - KMOD_META = (KMOD_LMETA|KMOD_RMETA) - }; -} diff --git a/library/include/df/custom/enabler.methods.inc b/library/include/df/custom/enabler.methods.inc index b06af2345..5cc584bc2 100644 --- a/library/include/df/custom/enabler.methods.inc +++ b/library/include/df/custom/enabler.methods.inc @@ -1,6 +1,8 @@ void zoom_display(df::zoom_commands command) { + /* DFHack::DFSDL::DFSDL_SemWait(async_zoom.sem); async_zoom.queue.push_back(command); DFHack::DFSDL::DFSDL_SemPost(async_zoom.sem); DFHack::DFSDL::DFSDL_SemPost(async_zoom.sem_fill); + */ } diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 78163108e..22dbb0370 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -237,7 +237,7 @@ DFHACK_EXPORT std::string getRoomDescription(df::building *building, df::unit *u * starting at the top left and moving right, row by row, * the block's items are checked for anything on the ground within that stockpile. */ -class DFHACK_EXPORT StockpileIterator : public std::iterator +class DFHACK_EXPORT StockpileIterator { df::building_stockpilest* stockpile; df::map_block* block; @@ -245,6 +245,12 @@ class DFHACK_EXPORT StockpileIterator : public std::iteratorzoom_display from plugins - DFHACK_EXPORT void zoom(df::zoom_commands cmd); + //DFHACK_EXPORT void zoom(df::zoom_commands cmd); /// Returns the state of [GRAPHICS:YES/NO] DFHACK_EXPORT bool inGraphicsMode(); @@ -229,6 +231,9 @@ namespace DFHack DFHACK_EXPORT bool hasActiveScreens(Plugin *p); DFHACK_EXPORT void raise(df::viewscreen *screen); + // returns a new set of interface keys that ensures that string input matches the DF text buffer + DFHACK_EXPORT std::set normalize_text_keys(const std::set& keys); + /// Retrieve the string representation of the bound key. DFHACK_EXPORT std::string getKeyDisplay(df::interface_key key); @@ -359,6 +364,8 @@ namespace DFHack virtual df::item *getSelectedItem() { return nullptr; } virtual df::job *getSelectedJob() { return nullptr; } virtual df::building *getSelectedBuilding() { return nullptr; } + virtual df::building_stockpilest *getSelectedStockpile() { return nullptr; } + virtual df::building_civzonest *getSelectedCivZone() { return nullptr; } virtual df::plant *getSelectedPlant() { return nullptr; } static virtual_identity _identity; @@ -384,6 +391,7 @@ namespace DFHack virtual ~dfhack_lua_viewscreen(); static df::viewscreen *get_pointer(lua_State *L, int idx, bool make); + static void markInputAsHandled(); virtual bool is_lua_screen() { return true; } virtual bool isFocused() { return !defocused; } @@ -403,6 +411,8 @@ namespace DFHack virtual df::item *getSelectedItem(); virtual df::job *getSelectedJob(); virtual df::building *getSelectedBuilding(); + virtual df::building_civzonest *getSelectedCivZone(); + virtual df::building_stockpilest *getSelectedStockpile(); virtual df::plant *getSelectedPlant(); static virtual_identity _identity; diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h index 95e628d5a..b820c3332 100644 --- a/library/include/modules/Textures.h +++ b/library/include/modules/Textures.h @@ -1,7 +1,14 @@ #pragma once -#include "Export.h" +#include +#include + #include "ColorText.h" +#include "Export.h" + +struct SDL_Surface; + +typedef uintptr_t TexposHandle; namespace DFHack { @@ -12,53 +19,64 @@ namespace DFHack { */ namespace Textures { +const uint32_t TILE_WIDTH_PX = 8; +const uint32_t TILE_HEIGHT_PX = 12; + /** - * Call this on DFHack init and on every viewscreen change so we can reload - * and reindex textures as needed. + * Load texture and get handle. + * Keep it to obtain valid texpos. */ -void init(DFHack::color_ostream &out); +DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = false); /** - * Call this when DFHack is being unloaded. - * + * Load tileset from image file. + * Return vector of handles to obtain valid texposes. */ -void cleanup(); +DFHACK_EXPORT std::vector loadTileset(const std::string& file, + int tile_px_w = TILE_WIDTH_PX, + int tile_px_h = TILE_HEIGHT_PX, + bool reserved = false); /** - * Get first texpos for the DFHack logo. This texpos and the next 11 make up the - * 4x3 grid of logo textures that can be displayed on the UI layer. + * Get texpos by handle. + * Always use this function, if you need to get valid texpos for your texture. + * Texpos can change on game textures reset, but handle will be the same. */ -DFHACK_EXPORT long getDfhackLogoTexposStart(); +DFHACK_EXPORT long getTexposByHandle(TexposHandle handle); /** - * Get the first texpos for the UI pin tiles. Each are 2x2 grids. + * Delete all info about texture by TexposHandle */ -DFHACK_EXPORT long getGreenPinTexposStart(); -DFHACK_EXPORT long getRedPinTexposStart(); +DFHACK_EXPORT void deleteHandle(TexposHandle handle); /** - * Get the first texpos for the DFHack icons. It's a 5x2 grid. + * Create new texture with RGBA32 format and pixels as data in row major order. + * Register this texture and return TexposHandle. */ -DFHACK_EXPORT long getIconsTexposStart(); +DFHACK_EXPORT TexposHandle createTile(std::vector& pixels, int tile_px_w = TILE_WIDTH_PX, + int tile_px_h = TILE_HEIGHT_PX, bool reserved = false); /** - * Get the first texpos for the on and off icons. It's a 2x1 grid. + * Create new textures as tileset with RGBA32 format and pixels as data in row major order. + * Register this textures and return vector of TexposHandle. */ -DFHACK_EXPORT long getOnOffTexposStart(); +DFHACK_EXPORT std::vector createTileset(std::vector& pixels, + int texture_px_w, int texture_px_h, + int tile_px_w = TILE_WIDTH_PX, + int tile_px_h = TILE_HEIGHT_PX, + bool reserved = false); /** - * Get the first texpos for the control panel icons. 10x2 grid. + * Call this on DFHack init just once to setup interposed handlers and + * init static assets. */ -DFHACK_EXPORT long getControlPanelTexposStart(); +void init(DFHack::color_ostream& out); /** - * Get the first texpos for the DFHack borders. Each is a 7x3 grid. + * Call this when DFHack is being unloaded. + * */ -DFHACK_EXPORT long getThinBordersTexposStart(); -DFHACK_EXPORT long getMediumBordersTexposStart(); -DFHACK_EXPORT long getBoldBordersTexposStart(); -DFHACK_EXPORT long getPanelBordersTexposStart(); -DFHACK_EXPORT long getWindowBordersTexposStart(); - -} -} +void cleanup(); + +} // namespace Textures +} // namespace DFHack diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 4fd9246aa..11a6c120a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -109,11 +109,16 @@ DFHACK_EXPORT bool isWar(df::unit* unit); DFHACK_EXPORT bool isTame(df::unit* unit); DFHACK_EXPORT bool isTamable(df::unit* unit); DFHACK_EXPORT bool isDomesticated(df::unit* unit); +DFHACK_EXPORT bool isMarkedForTraining(df::unit* unit); +DFHACK_EXPORT bool isMarkedForTaming(df::unit* unit); +DFHACK_EXPORT bool isMarkedForWarTraining(df::unit* unit); +DFHACK_EXPORT bool isMarkedForHuntTraining(df::unit* unit); DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); DFHACK_EXPORT bool isMarkedForGelding(df::unit* unit); DFHACK_EXPORT bool isGeldable(df::unit* unit); DFHACK_EXPORT bool isGelded(df::unit* unit); DFHACK_EXPORT bool isEggLayer(df::unit* unit); +DFHACK_EXPORT bool isEggLayerRace(df::unit* unit); DFHACK_EXPORT bool isGrazer(df::unit* unit); DFHACK_EXPORT bool isMilkable(df::unit* unit); DFHACK_EXPORT bool isForest(df::unit* unit); @@ -148,6 +153,8 @@ DFHACK_EXPORT df::unit *getUnit(const int32_t index); DFHACK_EXPORT bool getUnitsInBox(std::vector &units, int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); +DFHACK_EXPORT bool getUnitsByNobleRole(std::vector &units, std::string noble); +DFHACK_EXPORT df::unit *getUnitByNobleRole(std::string noble); DFHACK_EXPORT bool getCitizens(std::vector &citizens, bool ignore_sanity = false); DFHACK_EXPORT int32_t findIndexById(int32_t id); @@ -189,7 +196,7 @@ DFHACK_EXPORT std::string getRaceBabyNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceChildName(df::unit* unit); - +DFHACK_EXPORT std::string getReadableName(df::unit* unit); DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); DFHACK_EXPORT int getKillCount(df::unit *unit); diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index e094bbb57..0a652f428 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -3,7 +3,6 @@ local _ENV = mkmodule('argparse') local getopt = require('3rdparty.alt_getopt') -local guidm = require('gui.dwarfmode') function processArgs(args, validArgs) local result = {} @@ -174,6 +173,7 @@ end function coords(arg, arg_name, skip_validation) if arg == 'here' then + local guidm = require('gui.dwarfmode') -- globals may not be available yet local cursor = guidm.getCursorPos() if not cursor then arg_error(arg_name, @@ -196,4 +196,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} +function boolean(arg, arg_name) + local arg_lower = string.lower(arg) + if toBool[arg_lower] == nil then + arg_error(arg_name, + 'unknown value: "%s"; expected "true", "yes", "false", or "no"', arg) + end + + return toBool[arg_lower] +end + return _ENV diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 8ea5e9dac..059c008d2 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -38,6 +38,9 @@ COLOR_LIGHTMAGENTA = 13 COLOR_YELLOW = 14 COLOR_WHITE = 15 +COLOR_GRAY = COLOR_GREY +COLOR_DARKGRAY = COLOR_DARKGREY + -- Events if dfhack.is_core_context then diff --git a/library/lua/gui.lua b/library/lua/gui.lua index e22ddae20..05d848bb3 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -2,6 +2,7 @@ local _ENV = mkmodule('gui') +local textures = require('gui.textures') local utils = require('utils') local dscreen = dfhack.screen @@ -9,13 +10,19 @@ local getval = utils.getval local to_pen = dfhack.pen.parse -CLEAR_PEN = to_pen{tile=df.global.init.texpos_border_interior, ch=32, fg=0, bg=0, write_to_lower=true} +CLEAR_PEN = to_pen{tile=dfhack.internal.getAddress('init') and df.global.init.texpos_border_interior, ch=32, fg=0, bg=0, write_to_lower=true} TRANSPARENT_PEN = to_pen{tile=0, ch=0} KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true} +local function set_and_get_undo(field, is_set) + local prev_value = df.global.enabler[field] + df.global.enabler[field] = is_set and 1 or 0 + return function() df.global.enabler[field] = prev_value end +end + local MOUSE_KEYS = { - _MOUSE_L = true, - _MOUSE_R = true, + _MOUSE_L = curry(set_and_get_undo, 'mouse_lbut'), + _MOUSE_R = curry(set_and_get_undo, 'mouse_rbut'), _MOUSE_M = true, _MOUSE_L_DOWN = true, _MOUSE_R_DOWN = true, @@ -26,7 +33,7 @@ local FAKE_INPUT_KEYS = copyall(MOUSE_KEYS) FAKE_INPUT_KEYS._STRING = true function simulateInput(screen,...) - local keys = {} + local keys, enabled_mouse_keys = {}, {} local function push_key(arg) local kv = arg if type(arg) == 'string' then @@ -34,6 +41,10 @@ function simulateInput(screen,...) if kv == nil and not FAKE_INPUT_KEYS[arg] then error('Invalid keycode: '..arg) end + if MOUSE_KEYS[arg] then + df.global.enabler.tracking_on = 1 + enabled_mouse_keys[arg] = true + end end if type(kv) == 'number' then keys[#keys+1] = kv @@ -56,7 +67,16 @@ function simulateInput(screen,...) end end end + local undo_fns = {} + for mk, fn in pairs(MOUSE_KEYS) do + if type(fn) == 'function' then + table.insert(undo_fns, fn(enabled_mouse_keys[mk])) + end + end dscreen._doSimulateInput(screen, keys) + for _, undo_fn in ipairs(undo_fns) do + undo_fn() + end end function mkdims_xy(x1,y1,x2,y2) @@ -695,21 +715,6 @@ end DEFAULT_INITIAL_PAUSE = true -local zscreen_inhibit_mouse_l = false - --- ensure underlying DF screens don't also react to handled clicks -function markMouseClicksHandled(keys) - if keys._MOUSE_L_DOWN then - -- note we can't clear mouse_lbut here. otherwise we break dragging, - df.global.enabler.mouse_lbut_down = 0 - zscreen_inhibit_mouse_l = true - end - if keys._MOUSE_R_DOWN then - df.global.enabler.mouse_rbut_down = 0 - df.global.enabler.mouse_rbut = 0 - end -end - ZScreen = defclass(ZScreen, Screen) ZScreen.ATTRS{ defocusable=true, @@ -788,37 +793,24 @@ function ZScreen:onInput(keys) local has_mouse = self:isMouseOver() if not self:hasFocus() then if has_mouse and - (keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or + (keys._MOUSE_L or keys._MOUSE_R or keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN) then self:raise() else self:sendInputToParent(keys) - return + return true end end if ZScreen.super.onInput(self, keys) then - markMouseClicksHandled(keys) - return - end - - if self.pass_mouse_clicks and keys._MOUSE_L_DOWN and not has_mouse then + -- noop + elseif self.pass_mouse_clicks and keys._MOUSE_L and not has_mouse then self.defocused = self.defocusable self:sendInputToParent(keys) - return - elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + elseif keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() - markMouseClicksHandled(keys) - return else - if zscreen_inhibit_mouse_l then - if keys._MOUSE_L then - return - else - zscreen_inhibit_mouse_l = false - end - end local passit = self.pass_pause and keys.D_PAUSE if not passit and self.pass_mouse_clicks then if keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or @@ -839,8 +831,8 @@ function ZScreen:onInput(keys) if passit then self:sendInputToParent(keys) end - return end + return true end function ZScreen:raise() @@ -871,6 +863,12 @@ end function ZScreen:onGetSelectedBuilding() return zscreen_get_any(self, 'Building') end +function ZScreen:onGetSelectedStockpile() + return zscreen_get_any(self, 'Stockpile') +end +function ZScreen:onGetSelectedCivZone() + return zscreen_get_any(self, 'CivZone') +end function ZScreen:onGetSelectedPlant() return zscreen_get_any(self, 'Plant') end @@ -912,33 +910,49 @@ local BASE_FRAME = { paused_pen = to_pen{fg=COLOR_RED, bg=COLOR_BLACK}, } -local function make_frame(name, double_line) - local texpos = dfhack.textures['get'..name..'BordersTexposStart']() - local tp = function(offset) - if texpos == -1 then return nil end - return texpos + offset - end +local function make_frame(tp, double_line) local frame = copyall(BASE_FRAME) - frame.t_frame_pen = to_pen{ tile=tp(1), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.l_frame_pen = to_pen{ tile=tp(7), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.b_frame_pen = to_pen{ tile=tp(15), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.r_frame_pen = to_pen{ tile=tp(9), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.lt_frame_pen = to_pen{ tile=tp(0), ch=double_line and 201 or 218, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.lb_frame_pen = to_pen{ tile=tp(14), ch=double_line and 200 or 192, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.rt_frame_pen = to_pen{ tile=tp(2), ch=double_line and 187 or 191, fg=COLOR_GREY, bg=COLOR_BLACK } - frame.rb_frame_pen = to_pen{ tile=tp(16), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.t_frame_pen = to_pen{ tile=curry(tp, 2), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.l_frame_pen = to_pen{ tile=curry(tp, 8), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.b_frame_pen = to_pen{ tile=curry(tp, 16), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.r_frame_pen = to_pen{ tile=curry(tp, 10), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.lt_frame_pen = to_pen{ tile=curry(tp, 1), ch=double_line and 201 or 218, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.lb_frame_pen = to_pen{ tile=curry(tp, 15), ch=double_line and 200 or 192, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.rt_frame_pen = to_pen{ tile=curry(tp, 3), ch=double_line and 187 or 191, fg=COLOR_GREY, bg=COLOR_BLACK } + frame.rb_frame_pen = to_pen{ tile=curry(tp, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK } return frame end -FRAME_WINDOW = make_frame('Window', true) -FRAME_PANEL = make_frame('Panel', false) -FRAME_MEDIUM = make_frame('Medium', false) -FRAME_BOLD = make_frame('Bold', true) -FRAME_INTERIOR = make_frame('Thin', false) -FRAME_INTERIOR.signature_pen = false -FRAME_INTERIOR_MEDIUM = copyall(FRAME_MEDIUM) -FRAME_INTERIOR_MEDIUM.signature_pen = false +function FRAME_WINDOW(resizable) + local frame = make_frame(textures.tp_border_window, true) + if not resizable then + frame.rb_frame_pen = to_pen{ tile=curry(textures.tp_border_panel, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK } + end + return frame +end +function FRAME_PANEL() + return make_frame(textures.tp_border_panel, false) +end +function FRAME_MEDIUM() + return make_frame(textures.tp_border_medium, false) +end +function FRAME_BOLD() + return make_frame(textures.tp_border_bold, true) +end +function FRAME_THIN() + return make_frame(textures.tp_border_thin, false) +end +function FRAME_INTERIOR() + local frame = make_frame(textures.tp_border_thin, false) + frame.signature_pen = false + return frame +end +function FRAME_INTERIOR_MEDIUM() + local frame = make_frame(textures.tp_border_medium, false) + frame.signature_pen = false + return frame +end -- for compatibility with pre-steam code GREY_LINE_FRAME = FRAME_PANEL @@ -951,18 +965,16 @@ BOLD_FRAME = FRAME_BOLD INTERIOR_FRAME = FRAME_INTERIOR INTERIOR_MEDIUM_FRAME = FRAME_INTERIOR_MEDIUM - -function paint_frame(dc,rect,style,title,inactive,pause_forced,resizable) +function paint_frame(dc, rect, style, title, inactive, pause_forced, resizable) + if type(style) == 'function' then + style = style(resizable) + end local pen = style.frame_pen local x1,y1,x2,y2 = dc.x1+rect.x1, dc.y1+rect.y1, dc.x1+rect.x2, dc.y1+rect.y2 dscreen.paintTile(style.lt_frame_pen or pen, x1, y1) dscreen.paintTile(style.rt_frame_pen or pen, x2, y1) dscreen.paintTile(style.lb_frame_pen or pen, x1, y2) - local rb_frame_pen = style.rb_frame_pen - if rb_frame_pen == FRAME_WINDOW.rb_frame_pen and not resizable then - rb_frame_pen = FRAME_PANEL.rb_frame_pen - end - dscreen.paintTile(rb_frame_pen or pen, x2, y2) + dscreen.paintTile(style.rb_frame_pen or pen, x2, y2) dscreen.fillRect(style.t_frame_pen or style.h_frame_pen or pen,x1+1,y1,x2-1,y1) dscreen.fillRect(style.b_frame_pen or style.h_frame_pen or pen,x1+1,y2,x2-1,y2) dscreen.fillRect(style.l_frame_pen or style.v_frame_pen or pen,x1,y1+1,x1,y2-1) @@ -1018,6 +1030,11 @@ function FramedScreen:onRenderFrame(dc, rect) paint_frame(dc,rect,self.frame_style,self.frame_title) end +function FramedScreen:onInput(keys) + FramedScreen.super.onInput(self, keys) + return true -- FramedScreens are modal +end + -- Inverts the brightness of the color, optionally taking a "bold" parameter, -- which you should include if you're reading the fg color of a pen. function invert_color(color, bold) diff --git a/library/lua/gui/buildings.lua b/library/lua/gui/buildings.lua index 8871346f2..93b0e2182 100644 --- a/library/lua/gui/buildings.lua +++ b/library/lua/gui/buildings.lua @@ -4,8 +4,6 @@ local _ENV = mkmodule('gui.buildings') local gui = require('gui') local widgets = require('gui.widgets') -local dlg = require('gui.dialogs') -local utils = require('utils') ARROW = string.char(26) @@ -270,6 +268,7 @@ function BuildingDialog:onInput(keys) return true end self:inputToSubviews(keys) + return true end function showBuildingPrompt(title, prompt, on_select, on_cancel, build_filter) diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 1b7858416..63f9990c1 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -11,7 +11,7 @@ MessageBox.focus_path = 'MessageBox' MessageBox.ATTRS{ frame_style = gui.WINDOW_FRAME, - frame_inset = 1, + frame_inset = {l=1, t=1, b=1, r=0}, -- new attrs on_accept = DEFAULT_NIL, on_cancel = DEFAULT_NIL, @@ -33,13 +33,14 @@ end function MessageBox:getWantedFrameSize() local label = self.subviews.label local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4) - local text_area_width = label:getTextWidth() - if label.frame_inset then - -- account for scroll icons - text_area_width = text_area_width + (label.frame_inset.l or 0) - text_area_width = text_area_width + (label.frame_inset.r or 0) + local text_area_width = label:getTextWidth() + 1 + local text_height = label:getTextHeight() + local sw, sh = dfhack.screen.getWindowSize() + if text_height > sh - 4 then + -- account for scrollbar + text_area_width = text_area_width + 2 end - return math.max(width, text_area_width), label:getTextHeight() + return math.max(width, text_area_width), text_height end function MessageBox:onRenderFrame(dc,rect) @@ -56,17 +57,17 @@ function MessageBox:onDestroy() end function MessageBox:onInput(keys) - if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() if keys.SELECT and self.on_accept then self.on_accept() - elseif (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) and self.on_cancel then + elseif (keys.LEAVESCREEN or keys._MOUSE_R) and self.on_cancel then self.on_cancel() end - gui.markMouseClicksHandled(keys) return true end - return self:inputToSubviews(keys) + self:inputToSubviews(keys) + return true end function showMessage(title, text, tcolor, on_close) @@ -128,15 +129,15 @@ function InputBox:onInput(keys) self.on_input(self.subviews.edit.text) end return true - elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + elseif keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() if self.on_cancel then self.on_cancel() end - gui.markMouseClicksHandled(keys) return true end - return self:inputToSubviews(keys) + self:inputToSubviews(keys) + return true end function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width) @@ -230,15 +231,15 @@ function ListBox:getWantedFrameSize() end function ListBox:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self:dismiss() if self.on_cancel then self.on_cancel() end - gui.markMouseClicksHandled(keys) return true end - return self:inputToSubviews(keys) + self:inputToSubviews(keys) + return true end function showListPrompt(title, text, tcolor, choices, on_select, on_cancel, min_width, filter) diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index eea881768..112d8f6df 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -5,7 +5,6 @@ local _ENV = mkmodule('gui.materials') local gui = require('gui') local widgets = require('gui.widgets') local dlg = require('gui.dialogs') -local utils = require('utils') ARROW = string.char(26) @@ -266,7 +265,8 @@ function MaterialDialog:onInput(keys) end return true end - return self:inputToSubviews(keys) + self:inputToSubviews(keys) + return true end function showMaterialPrompt(title, prompt, on_select, on_cancel, mat_filter) diff --git a/library/lua/gui/textures.lua b/library/lua/gui/textures.lua new file mode 100644 index 000000000..6dac234e0 --- /dev/null +++ b/library/lua/gui/textures.lua @@ -0,0 +1,93 @@ +-- DFHack textures + +local _ENV = mkmodule('gui.textures') + +---@alias TexposHandle integer + +-- Preloaded DFHack Assets. +-- Use this 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), +} + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_green_pin(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.green_pin[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_red_pin(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.red_pin[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_icons(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.icons[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_on_off(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.on_off[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_control_panel(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.control_panel[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_border_thin(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.border_thin[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_border_medium(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.border_medium[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return TexposHandle +function tp_border_bold(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.border_bold[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_border_panel(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.border_panel[offset]) +end + +-- Get valid texpos for preloaded texture in tileset +---@param offset integer +---@return integer +function tp_border_window(offset) + return dfhack.textures.getTexposByHandle(texpos_handles.border_window[offset]) +end + +return _ENV diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index a58ac228c..912094fc4 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -4,9 +4,9 @@ local _ENV = mkmodule('gui.widgets') local gui = require('gui') local guidm = require('gui.dwarfmode') +local textures = require('gui.textures') local utils = require('utils') -local dscreen = dfhack.screen local getval = utils.getval local to_pen = dfhack.pen.parse @@ -223,9 +223,9 @@ local function Panel_begin_drag(self, drag_offset, resize_edge) self.prev_focus_owner = self.focus_group.cur self:setFocus(true) if self.resize_edge then - if self.on_resize_begin then self.on_resize_begin(success) end + self:onResizeBegin() else - if self.on_drag_begin then self.on_drag_begin(success) end + self:onDragBegin() end end @@ -236,11 +236,12 @@ local function Panel_end_drag(self, frame, success) else self:setFocus(false) end + local resize_edge = self.resize_edge Panel_update_frame(self, frame, true) - if self.resize_edge then - if self.on_resize_end then self.on_resize_end(success) end + if resize_edge then + self:onResizeEnd(success, self.frame) else - if self.on_drag_end then self.on_drag_end(success) end + self:onDragEnd(success, self.frame) end end @@ -273,7 +274,7 @@ end function Panel:onInput(keys) if self.kbd_get_pos then - if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then Panel_end_drag(self, not keys.SELECT and self.saved_frame or nil, not not keys.SELECT) return true @@ -281,7 +282,6 @@ function Panel:onInput(keys) for code in pairs(keys) do local dx, dy = guidm.get_movement_delta(code, 1, 10) if dx then - local frame_rect = self.frame_rect local kbd_pos = self.kbd_get_pos() kbd_pos.x = kbd_pos.x + dx kbd_pos.y = kbd_pos.y + dy @@ -292,9 +292,9 @@ function Panel:onInput(keys) return end if self.drag_offset then - if keys._MOUSE_R_DOWN then + if keys._MOUSE_R then Panel_end_drag(self, self.saved_frame) - elseif keys._MOUSE_L then + elseif keys._MOUSE_L_DOWN then Panel_update_frame(self, Panel_make_frame(self)) end return true @@ -302,7 +302,7 @@ function Panel:onInput(keys) if Panel.super.onInput(self, keys) then return true end - if not keys._MOUSE_L_DOWN then return end + if not keys._MOUSE_L then return end local x,y = self:getMouseFramePos() if not x then return end @@ -489,11 +489,27 @@ function Panel:onRenderFrame(dc, rect) dc:seek(pos.x, pos.y):pen(pen):char(string.char(0xDB)) end if self.drag_offset and not self.kbd_get_pos - and df.global.enabler.mouse_lbut == 0 then + and df.global.enabler.mouse_lbut_down == 0 then Panel_end_drag(self, nil, true) end end +function Panel:onDragBegin() + if self.on_drag_begin then self.on_drag_begin() end +end + +function Panel:onDragEnd(success, new_frame) + if self.on_drag_end then self.on_drag_end(success, new_frame) end +end + +function Panel:onResizeBegin() + if self.on_resize_begin then self.on_resize_begin() end +end + +function Panel:onResizeEnd(success, new_frame) + if self.on_resize_end then self.on_resize_end(success, new_frame) end +end + ------------ -- Window -- ------------ @@ -640,8 +656,12 @@ function EditField:setCursor(cursor) end function EditField:setText(text, cursor) + local old = self.text self.text = text self:setCursor(cursor) + if self.on_change and text ~= old then + self.on_change(self.text, old) + end end function EditField:postUpdateLayout() @@ -698,12 +718,8 @@ function EditField:onInput(keys) end end - if self.key and (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) then - local old = self.text + if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then self:setText(self.saved_text) - if self.on_change and old ~= self.saved_text then - self.on_change(self.text, old) - end self:setFocus(false) return true end @@ -724,12 +740,6 @@ function EditField:onInput(keys) end end return not not self.key - elseif keys._MOUSE_L then - local mouse_x, mouse_y = self:getMousePos() - if mouse_x then - self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) - return true - end elseif keys._STRING then local old = self.text if keys._STRING == 0 then @@ -747,9 +757,6 @@ function EditField:onInput(keys) return self.modal end end - if self.on_change and self.text ~= old then - self.on_change(self.text, old) - end return true elseif keys.KEYBOARD_CURSOR_LEFT then self:setCursor(self.cursor - 1) @@ -759,9 +766,10 @@ function EditField:onInput(keys) find('.*[%w_%-][^%w_%-]') self:setCursor(prev_word_end or 1) return true - elseif keys.CUSTOM_CTRL_A then -- home - self:setCursor(1) - return true + -- commented out until we get HOME key support from DF + -- elseif keys.CUSTOM_CTRL_A then -- home + -- self:setCursor(1) + -- return true elseif keys.KEYBOARD_CURSOR_RIGHT then self:setCursor(self.cursor + 1) return true @@ -772,6 +780,22 @@ function EditField:onInput(keys) elseif keys.CUSTOM_CTRL_E then -- end self:setCursor() return true + elseif keys.CUSTOM_CTRL_C then + dfhack.internal.setClipboardTextCp437(self.text) + return true + elseif keys.CUSTOM_CTRL_X then + dfhack.internal.setClipboardTextCp437(self.text) + self:setText('') + return true + elseif keys.CUSTOM_CTRL_V then + self:insert(dfhack.internal.getClipboardTextCp437()) + return true + elseif keys._MOUSE_L_DOWN then + local mouse_x = self:getMousePos() + if mouse_x then + self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) + return true + end end -- if we're modal, then unconditionally eat all the input @@ -962,7 +986,7 @@ function Scrollbar:onRenderBody(dc) if self.is_dragging then scrollbar_do_drag(self) end - if df.global.enabler.mouse_lbut == 0 then + if df.global.enabler.mouse_lbut_down == 0 then self.last_scroll_ms = 0 self.is_dragging = false self.scroll_spec = nil @@ -984,7 +1008,7 @@ function Scrollbar:onInput(keys) return false end - if self.parent_view:getMousePos() then + if self.parent_view and self.parent_view:getMousePos() then if keys.CONTEXT_SCROLL_UP then self.on_scroll('up_small') return true @@ -999,7 +1023,7 @@ function Scrollbar:onInput(keys) return true end end - if not keys._MOUSE_L_DOWN then return false end + if not keys._MOUSE_L then return false end local _,y = self:getMousePos() if not y then return false end local scroll_spec = nil @@ -1087,7 +1111,7 @@ end -- returns it (in parsed pen form) local function make_hpen(pen, hpen) if not hpen then - pen = dfhack.pen.parse(pen) + pen = to_pen(pen) -- Swap the foreground and background hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) @@ -1096,7 +1120,7 @@ local function make_hpen(pen, hpen) -- text_hpen needs a character in order to paint the background using -- Painter:fill(), so let's make it paint a space to show the background -- color - local hpen_parsed = dfhack.pen.parse(hpen) + local hpen_parsed = to_pen(hpen) hpen_parsed.ch = string.byte(' ') return hpen_parsed end @@ -1362,11 +1386,11 @@ function Label:onInput(keys) if self:inputToSubviews(keys) then return true end - if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then + if keys._MOUSE_L and self:getMousePos() and self.on_click then self.on_click() return true end - if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then + if keys._MOUSE_R and self:getMousePos() and self.on_rclick then self.on_rclick() return true end @@ -1474,13 +1498,130 @@ end function HotkeyLabel:onInput(keys) if HotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate + elseif keys._MOUSE_L and self:getMousePos() and self.on_activate and not is_disabled(self) then self.on_activate() return true end end +---------------- +-- HelpButton -- +---------------- + +HelpButton = defclass(HelpButton, Panel) + +HelpButton.ATTRS{ + command=DEFAULT_NIL, +} + +local button_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} +local help_pen_center = to_pen{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} +local configure_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + +function HelpButton:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 + init_table.frame.w = init_table.frame.w or 3 +end + +function HelpButton:init() + local command = self.command .. ' ' + + self:addviews{ + Label{ + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=button_pen_left}, + {tile=help_pen_center}, + {tile=button_pen_right}, + }, + on_click=function() dfhack.run_command('gui/launcher', command) end, + }, + } +end + +--------------------- +-- ConfigureButton -- +--------------------- + +ConfigureButton = defclass(ConfigureButton, Panel) + +ConfigureButton.ATTRS{ + on_click=DEFAULT_NIL, +} + +function ConfigureButton:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 + init_table.frame.w = init_table.frame.w or 3 +end + +function ConfigureButton:init() + self:addviews{ + Label{ + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=button_pen_left}, + {tile=configure_pen_center}, + {tile=button_pen_right}, + }, + on_click=self.on_click, + }, + } +end + +----------------- +-- BannerPanel -- +----------------- + +BannerPanel = defclass(BannerPanel, Panel) + +function BannerPanel:onRenderBody(dc) + dc:pen(COLOR_RED) + for y=0,self.frame_rect.height-1 do + dc:seek(0, y):char('[') + dc:seek(self.frame_rect.width-1):char(']') + end +end + +---------------- +-- TextButton -- +---------------- + +TextButton = defclass(TextButton, BannerPanel) + +function TextButton:init(info) + self.label = HotkeyLabel{ + frame={t=0, l=1, r=1}, + key=info.key, + key_sep=info.key_sep, + label=info.label, + on_activate=info.on_activate, + text_pen=info.text_pen, + text_dpen=info.text_dpen, + text_hpen=info.text_hpen, + disabled=info.disabled, + enabled=info.enabled, + auto_height=info.auto_height, + auto_width=info.auto_width, + on_click=info.on_click, + on_rclick=info.on_rclick, + scroll_keys=info.scroll_keys, + } + + self:addviews{self.label} +end + +function TextButton:setLabel(label) + self.label:setLabel(label) +end + ---------------------- -- CycleHotkeyLabel -- ---------------------- @@ -1552,8 +1693,7 @@ function CycleHotkeyLabel:setOption(value_or_index, call_on_change) end end if not option_idx then - error(('cannot find option with value or index: "%s"') - :format(value_or_index)) + option_idx = 1 end local old_option_idx = self.option_idx self.option_idx = option_idx @@ -1589,7 +1729,7 @@ end function CycleHotkeyLabel:onInput(keys) if CycleHotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() and not is_disabled(self) then + elseif keys._MOUSE_L and self:getMousePos() and not is_disabled(self) then self:cycle() return true end @@ -1893,7 +2033,7 @@ function List:onInput(keys) return self:submit() elseif keys.CUSTOM_SHIFT_ENTER then return self:submit2() - elseif keys._MOUSE_L_DOWN then + elseif keys._MOUSE_L then local idx = self:getIdxUnderMouse() if idx then local now_ms = dfhack.getTickCount() @@ -1949,12 +2089,9 @@ end -- Filtered List -- ------------------- -FILTER_FULL_TEXT = false - FilteredList = defclass(FilteredList, Widget) FilteredList.ATTRS { - case_sensitive = false, edit_below = false, edit_key = DEFAULT_NIL, edit_ignore_keys = DEFAULT_NIL, @@ -2104,7 +2241,6 @@ function FilteredList:setFilter(filter, pos) pos = nil for i,v in ipairs(self.choices) do - local ok = true local search_key = v.search_key if not search_key then if type(v.text) ~= 'table' then @@ -2119,28 +2255,7 @@ function FilteredList:setFilter(filter, pos) search_key = table.concat(texts, ' ') end end - for _,key in ipairs(tokens) do - key = key:escape_pattern() - if key ~= '' then - if not self.case_sensitive then - search_key = string.lower(search_key) - key = string.lower(key) - end - - -- the separate checks for non-space or non-punctuation allows - -- punctuation itself to be matched if that is useful (e.g. - -- filenames or parameter names) - if not FILTER_FULL_TEXT and not search_key:match('%f[^%p\x00]'..key) - and not search_key:match('%f[^%s\x00]'..key) then - ok = false - break - elseif FILTER_FULL_TEXT and not search_key:find(key) then - ok = false - break - end - end - end - if ok then + if utils.search_text(search_key, tokens) then table.insert(choices, v) cidx[#choices] = i if ipos == i then @@ -2246,7 +2361,7 @@ end function Tab:onInput(keys) if Tab.super.onInput(self, keys) then return true end - if keys._MOUSE_L_DOWN and self:getMousePos() then + if keys._MOUSE_L and self:getMousePos() then self.on_select(self.id) return true end @@ -2348,7 +2463,7 @@ local function rangeslider_get_width_per_idx(self) end function RangeSlider:onInput(keys) - if not keys._MOUSE_L_DOWN then return false end + 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_left_idx_fn(), self.get_right_idx_fn() @@ -2392,9 +2507,15 @@ local function rangeslider_do_drag(self, width_per_idx) 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 @@ -2450,7 +2571,7 @@ function RangeSlider:onRenderBody(dc, rect) if self.is_dragging_target then rangeslider_do_drag(self, width_per_idx) end - if df.global.enabler.mouse_lbut == 0 then + if df.global.enabler.mouse_lbut_down == 0 then self.is_dragging_target = nil self.is_dragging_idx = nil end diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index 4ce078a22..a4e75ff0c 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -789,7 +789,7 @@ function ls(filter_str, skip_tags, show_dev_commands, exclude_strs) end if not show_dev_commands then local dev_tags = {'dev', 'unavailable'} - if dfhack.getHideArmokTools() then + if filter_str ~= 'armok' and dfhack.getHideArmokTools() then table.insert(dev_tags, 'armok') end table.insert(excludes, {tag=dev_tags}) diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua index 34b030d4e..a4c07d38c 100644 --- a/library/lua/memscan.lua +++ b/library/lua/memscan.lua @@ -217,6 +217,7 @@ function get_code_segment() for i,mem in ipairs(dfhack.internal.getMemRanges()) do if mem.read and mem.execute and (string.match(mem.name,'/dwarfort%.exe$') + or string.match(mem.name,'/dwarfort$') or string.match(mem.name,'/Dwarf_Fortress$') or string.match(mem.name,'Dwarf Fortress%.exe')) then @@ -243,6 +244,7 @@ local function find_data_segment() end elseif mem.read and mem.write and (string.match(mem.name,'/dwarfort%.exe$') + or string.match(mem.name,'/dwarfort$') or string.match(mem.name,'/Dwarf_Fortress$') or string.match(mem.name,'Dwarf Fortress%.exe')) then @@ -532,4 +534,32 @@ function get_screen_size() return w,h end +-- Global table + +function read_c_string(char_ptr) + local s = '' + local i = 0 + while char_ptr[i] ~= 0 do + s = s .. string.char(char_ptr[i]) + i = i + 1 + end + return s +end + +function read_global_table(global_table) + global_table = global_table or df.global.global_table + local out = {} + local i = 0 + while true do + -- use _displace() so we can read past the bounds of the array set in structures, if necessary + local entry = global_table[0]:_displace(i) + if not entry.name or not entry.address then + break + end + out[read_c_string(entry.name)] = entry + i = i + 1 + end + return out +end + return _ENV diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua index 450012357..c5d9fb364 100644 --- a/library/lua/script-manager.lua +++ b/library/lua/script-manager.lua @@ -51,9 +51,10 @@ function reload(refresh_active_mod_scripts) enabled_map = utils.OrderedTable() local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name) if script_path:find('scripts_modactive') then - internal_script = dfhack.internal.scripts[script_path..'/'..script_name] + local full_path = script_path..'/'..script_name + internal_script = dfhack.internal.scripts[full_path] if internal_script then - internal_script.env = nil + dfhack.internal.scripts[full_path] = nil end end end or nil @@ -74,7 +75,7 @@ function list() end --------------------- --- mod script paths +-- mod paths -- this perhaps could/should be queried from the Steam API -- are there any installation configurations where this will be wrong, though? @@ -98,8 +99,8 @@ local function get_mod_id_and_version(path) end if not version then -- note this doesn't include the closing brace since some people put - -- non-number characters in here, and DF only reads the digits as the - -- numeric version + -- non-number characters in here, and DF only reads the leading digits + -- as the numeric version _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)') end -- note that we do *not* want to break out of this loop early since @@ -108,37 +109,31 @@ local function get_mod_id_and_version(path) return id, version end -local function add_script_path(mod_script_paths, path) +local function add_mod_paths(mod_paths, id, base_path, subdir) + local sep = base_path:endswith('/') and '' or '/' + local path = ('%s%s%s'):format(base_path, sep, subdir) if dfhack.filesystem.isdir(path) then - print('indexing mod scripts: ' .. path) - table.insert(mod_script_paths, path) + print('indexing mod path: ' .. path) + table.insert(mod_paths, {id=id, path=path}) end end -local function add_script_paths(mod_script_paths, base_path, include_modactive) - if not base_path:endswith('/') then - base_path = base_path .. '/' - end - if include_modactive then - add_script_path(mod_script_paths, base_path..'scripts_modactive') - end - add_script_path(mod_script_paths, base_path..'scripts_modinstalled') -end - -function get_mod_script_paths() +function get_mod_paths(installed_subdir, active_subdir) -- ordered map of mod id -> {handled=bool, versions=map of version -> path} local mods = utils.OrderedTable() - local mod_script_paths = {} + local mod_paths = {} -- if a world is loaded, process active mods first, and lock to active version - if dfhack.isWorldLoaded() then + if active_subdir and dfhack.isWorldLoaded() then for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do path = tostring(path.value) + -- skip vanilla "mods" if not path:startswith(INSTALLED_MODS_PATH) then goto continue end local id = get_mod_id_and_version(path) if not id then goto continue end mods[id] = {handled=true} - add_script_paths(mod_script_paths, path, true) + add_mod_paths(mod_paths, id, path, active_subdir) + add_mod_paths(mod_paths, id, path, installed_subdir) ::continue:: end end @@ -159,8 +154,8 @@ function get_mod_script_paths() ::skip_path_root:: end - -- add script paths from most recent version of all not-yet-handled mods - for _,v in pairs(mods) do + -- add paths from most recent version of all not-yet-handled mods + for id,v in pairs(mods) do if v.handled then goto continue end local max_version, path for version,mod_path in pairs(v.versions) do @@ -169,11 +164,19 @@ function get_mod_script_paths() max_version = version end end - add_script_paths(mod_script_paths, path) + add_mod_paths(mod_paths, id, path, installed_subdir) ::continue:: end - return mod_script_paths + return mod_paths +end + +function get_mod_script_paths() + local paths = {} + for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do + table.insert(paths, v.path) + end + return paths end return _ENV diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 3883439f1..fb41835da 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -460,6 +460,32 @@ function erase_sorted(vector,item,field,cmp) return erase_sorted_key(vector,key,field,cmp) end +FILTER_FULL_TEXT = false + +function search_text(text, search_tokens) + text = dfhack.toSearchNormalized(text) + if type(search_tokens) ~= 'table' then + search_tokens = search_tokens:split() + end + + for _,search_token in ipairs(search_tokens) do + if search_token == '' then goto continue end + search_token = dfhack.toSearchNormalized(search_token:escape_pattern()) + + -- the separate checks for non-space or non-punctuation allows + -- punctuation itself to be matched if that is useful (e.g. + -- filenames or parameter names) + if not FILTER_FULL_TEXT and not text:match('%f[^%p\x00]'..search_token) + and not text:match('%f[^%s\x00]'..search_token) then + return false + elseif FILTER_FULL_TEXT and not text:find(search_token) then + return false + end + ::continue:: + end + return true +end + -- Calls a method with a string temporary function call_with_string(obj,methodname,...) return dfhack.with_temp_object( diff --git a/library/modules/Burrows.cpp b/library/modules/Burrows.cpp index c2fe45eae..afd6ac9e3 100644 --- a/library/modules/Burrows.cpp +++ b/library/modules/Burrows.cpp @@ -51,13 +51,21 @@ using namespace df::enums; using df::global::world; using df::global::plotinfo; -df::burrow *Burrows::findByName(std::string name) +df::burrow *Burrows::findByName(std::string name, bool ignore_final_plus) { auto &vec = df::burrow::get_vector(); - for (size_t i = 0; i < vec.size(); i++) - if (vec[i]->name == name) + if (ignore_final_plus && name.ends_with('+')) + name = name.substr(0, name.length() - 1); + + for (size_t i = 0; i < vec.size(); i++) { + std::string bname = vec[i]->name; + if (ignore_final_plus && bname.ends_with('+')) + bname = bname.substr(0, bname.length() - 1); + + if (bname == name) return vec[i]; + } return NULL; } @@ -70,23 +78,13 @@ void Burrows::clearUnits(df::burrow *burrow) { auto unit = df::unit::find(burrow->units[i]); - if (unit) + if (unit) { erase_from_vector(unit->burrows, burrow->id); + erase_from_vector(unit->inactive_burrows, burrow->id); + } } burrow->units.clear(); - -/* TODO: understand how this changes for v50 - // Sync plotinfo if active - if (plotinfo && plotinfo->main.mode == ui_sidebar_mode::Burrows && - plotinfo->burrows.in_add_units_mode && plotinfo->burrows.sel_id == burrow->id) - { - auto &sel = plotinfo->burrows.sel_units; - - for (size_t i = 0; i < sel.size(); i++) - sel[i] = false; - } -*/ } bool Burrows::isAssignedUnit(df::burrow *burrow, df::unit *unit) @@ -94,7 +92,8 @@ bool Burrows::isAssignedUnit(df::burrow *burrow, df::unit *unit) CHECK_NULL_POINTER(unit); CHECK_NULL_POINTER(burrow); - return binsearch_index(unit->burrows, burrow->id) >= 0; + return binsearch_index(unit->burrows, burrow->id) >= 0 || + binsearch_index(unit->inactive_burrows, burrow->id) >= 0; } void Burrows::setAssignedUnit(df::burrow *burrow, df::unit *unit, bool enable) @@ -104,27 +103,17 @@ void Burrows::setAssignedUnit(df::burrow *burrow, df::unit *unit, bool enable) CHECK_NULL_POINTER(unit); CHECK_NULL_POINTER(burrow); - if (enable) - { - insert_into_vector(unit->burrows, burrow->id); + if (enable) { + if (burrow->limit_workshops & 2) // inactive flag + insert_into_vector(unit->inactive_burrows, burrow->id); + else + insert_into_vector(unit->burrows, burrow->id); insert_into_vector(burrow->units, unit->id); - } - else - { + } else { erase_from_vector(unit->burrows, burrow->id); + erase_from_vector(unit->inactive_burrows, burrow->id); erase_from_vector(burrow->units, unit->id); } - -/* TODO: understand how this changes for v50 - // Sync plotinfo if active - if (plotinfo && plotinfo->main.mode == ui_sidebar_mode::Burrows && - plotinfo->burrows.in_add_units_mode && plotinfo->burrows.sel_id == burrow->id) - { - int idx = linear_index(plotinfo->burrows.list_units, unit); - if (idx >= 0) - plotinfo->burrows.sel_units[idx] = enable; - } -*/ } void Burrows::listBlocks(std::vector *pvec, df::burrow *burrow) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index b95b6302a..dec43a1c5 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -5,6 +5,8 @@ #include "Debug.h" #include "PluginManager.h" +#include + namespace DFHack { DBG_DECLARE(core, dfsdl, DebugCategory::LINFO); } @@ -14,29 +16,34 @@ using namespace DFHack; static DFLibrary *g_sdl_handle = nullptr; static DFLibrary *g_sdl_image_handle = nullptr; static const std::vector SDL_LIBS { - "SDL.dll", + "SDL2.dll", "SDL.framework/Versions/A/SDL", "SDL.framework/SDL", - "libSDL-1.2.so.0" + "libSDL2-2.0.so.0" }; static const std::vector SDL_IMAGE_LIBS { - "SDL_image.dll", + "SDL2_image.dll", "SDL_image.framework/Versions/A/SDL_image", "SDL_image.framework/SDL_image", - "libSDL_image-1.2.so.0" + "libSDL2_image-2.0.so.0" }; -DFSDL_Surface * (*g_IMG_Load)(const char *) = nullptr; -int (*g_SDL_SetAlpha)(DFSDL_Surface *, uint32_t, uint8_t) = nullptr; -DFSDL_Surface * (*g_SDL_GetVideoSurface)(void) = nullptr; -DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t) = nullptr; -DFSDL_Surface * (*g_SDL_CreateRGBSurfaceFrom)(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) = nullptr; -int (*g_SDL_UpperBlit)(DFSDL_Surface *, const DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *) = nullptr; -DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t) = nullptr; -void (*g_SDL_FreeSurface)(DFSDL_Surface *) = nullptr; -int (*g_SDL_SemWait)(DFSDL_sem *) = nullptr; -int (*g_SDL_SemPost)(DFSDL_sem *) = nullptr; -int (*g_SDL_PushEvent)(DFSDL_Event *) = nullptr; +SDL_Surface * (*g_IMG_Load)(const char *) = nullptr; +SDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t) = nullptr; +SDL_Surface * (*g_SDL_CreateRGBSurfaceFrom)(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) = nullptr; +int (*g_SDL_UpperBlit)(SDL_Surface *, const SDL_Rect *, SDL_Surface *, SDL_Rect *) = nullptr; +SDL_Surface * (*g_SDL_ConvertSurface)(SDL_Surface *, const SDL_PixelFormat *, uint32_t) = nullptr; +void (*g_SDL_FreeSurface)(SDL_Surface *) = nullptr; +// int (*g_SDL_SemWait)(DFSDL_sem *) = nullptr; +// int (*g_SDL_SemPost)(DFSDL_sem *) = nullptr; +int (*g_SDL_PushEvent)(SDL_Event *) = nullptr; +SDL_bool (*g_SDL_HasClipboardText)(); +int (*g_SDL_SetClipboardText)(const char *text); +char * (*g_SDL_GetClipboardText)(); +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; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -65,17 +72,22 @@ bool DFSDL::init(color_ostream &out) { } bind(g_sdl_image_handle, IMG_Load); - bind(g_sdl_handle, SDL_SetAlpha); - bind(g_sdl_handle, SDL_GetVideoSurface); bind(g_sdl_handle, SDL_CreateRGBSurface); bind(g_sdl_handle, SDL_CreateRGBSurfaceFrom); bind(g_sdl_handle, SDL_UpperBlit); bind(g_sdl_handle, SDL_ConvertSurface); bind(g_sdl_handle, SDL_FreeSurface); - bind(g_sdl_handle, SDL_SemWait); - bind(g_sdl_handle, SDL_SemPost); + // bind(g_sdl_handle, SDL_SemWait); + // bind(g_sdl_handle, SDL_SemPost); bind(g_sdl_handle, SDL_PushEvent); - #undef bind + bind(g_sdl_handle, SDL_HasClipboardText); + bind(g_sdl_handle, SDL_SetClipboardText); + bind(g_sdl_handle, SDL_GetClipboardText); + bind(g_sdl_handle, SDL_free); + bind(g_sdl_handle, SDL_AllocFormat); + bind(g_sdl_handle, SDL_CreateRGBSurfaceWithFormat); + bind(g_sdl_handle, SDL_ShowSimpleMessageBox); +#undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); return true; @@ -93,46 +105,84 @@ void DFSDL::cleanup() { } } -DFSDL_Surface * DFSDL::DFIMG_Load(const char *file) { +SDL_Surface * DFSDL::DFIMG_Load(const char *file) { return g_IMG_Load(file); } -int DFSDL::DFSDL_SetAlpha(DFSDL_Surface *surface, uint32_t flag, uint8_t alpha) { - return g_SDL_SetAlpha(surface, flag, alpha); -} - -DFSDL_Surface * DFSDL::DFSDL_GetVideoSurface(void) { - return g_SDL_GetVideoSurface(); -} - -DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { +SDL_Surface * DFSDL::DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { return g_SDL_CreateRGBSurface(flags, width, height, depth, Rmask, Gmask, Bmask, Amask); } -DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { +SDL_Surface * DFSDL::DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { return g_SDL_CreateRGBSurfaceFrom(pixels, width, height, depth, pitch, Rmask, Gmask, Bmask, Amask); } -int DFSDL::DFSDL_UpperBlit(DFSDL_Surface *src, const DFSDL_Rect *srcrect, DFSDL_Surface *dst, DFSDL_Rect *dstrect) { +int DFSDL::DFSDL_UpperBlit(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect) { return g_SDL_UpperBlit(src, srcrect, dst, dstrect); } -DFSDL_Surface * DFSDL::DFSDL_ConvertSurface(DFSDL_Surface *src, const DFSDL_PixelFormat *fmt, uint32_t flags) { +SDL_Surface * DFSDL::DFSDL_ConvertSurface(SDL_Surface *src, const SDL_PixelFormat *fmt, uint32_t flags) { return g_SDL_ConvertSurface(src, fmt, flags); } -void DFSDL::DFSDL_FreeSurface(DFSDL_Surface *surface) { +void DFSDL::DFSDL_FreeSurface(SDL_Surface *surface) { g_SDL_FreeSurface(surface); } -int DFSDL::DFSDL_SemWait(DFSDL_sem *sem) { - return g_SDL_SemWait(sem); +// int DFSDL::DFSDL_SemWait(DFSDL_sem *sem) { +// return g_SDL_SemWait(sem); +// } + +// int DFSDL::DFSDL_SemPost(DFSDL_sem *sem) { +// return g_SDL_SemPost(sem); +// } + +int DFSDL::DFSDL_PushEvent(SDL_Event *event) { + return g_SDL_PushEvent(event); +} + +void DFSDL::DFSDL_free(void *ptr) { + g_SDL_free(ptr); } -int DFSDL::DFSDL_SemPost(DFSDL_sem *sem) { - return g_SDL_SemPost(sem); +char * DFSDL::DFSDL_GetClipboardText() { + return g_SDL_GetClipboardText(); } -int DFSDL::DFSDL_PushEvent(DFSDL_Event *event) { - return g_SDL_PushEvent(event); +int DFSDL::DFSDL_SetClipboardText(const char *text) { + return g_SDL_SetClipboardText(text); +} + +SDL_PixelFormat* DFSDL::DFSDL_AllocFormat(uint32_t pixel_format) { + return g_SDL_AllocFormat(pixel_format); +} + +SDL_Surface* DFSDL::DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format) { + return g_SDL_CreateRGBSurfaceWithFormat(flags, width, height, depth, format); +} + +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); +} + +DFHACK_EXPORT std::string DFHack::getClipboardTextCp437() { + if (!g_sdl_handle || g_SDL_HasClipboardText() != SDL_TRUE) + return ""; + char *text = g_SDL_GetClipboardText(); + // convert tabs to spaces so they don't get converted to '?' + for (char *c = text; *c; ++c) { + if (*c == '\t') + *c = ' '; + } + std::string textcp437 = UTF2DF(text); + DFHack::DFSDL::DFSDL_free(text); + return textcp437; +} + +DFHACK_EXPORT bool DFHack::setClipboardTextCp437(std::string text) { + if (!g_sdl_handle) + return false; + return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(DF2UTF(text).c_str()); } diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 1fd064d56..9aab53466 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -108,7 +108,7 @@ static bool is_running_on_wine() { return !!pwine_get_version; } -static DWORD findProcess(LPWSTR name) { +static DWORD findProcess(LPCWSTR name) { PROCESSENTRY32W entry; entry.dwSize = sizeof(PROCESSENTRY32W); @@ -150,11 +150,14 @@ static bool launchDFHack(color_ostream& out) { si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - // note that the enviornment must be explicitly zeroed out and not NULL, + static LPCWSTR procname = L"hack/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(L"hack/launchdf.exe", - NULL, NULL, NULL, FALSE, 0, "\0", NULL, &si, &pi); + BOOL res = CreateProcessW(procname, + NULL, NULL, NULL, FALSE, 0, (LPVOID)env, NULL, &si, &pi); return !!res; } diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 02b1892e9..e985cfa96 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -43,6 +43,7 @@ #include #include #include +#include namespace DFHack { DBG_DECLARE(eventmanager, log, DebugCategory::LINFO); @@ -205,6 +206,9 @@ std::array compileManagerArray() { //job initiated static int32_t lastJobId = -1; +//job started +static unordered_set startedJobs; + //job completed static unordered_map prevJobs; @@ -246,6 +250,15 @@ static int32_t reportToRelevantUnitsTime = -1; //interaction static int32_t lastReportInteraction; +struct hash_pair { + template + size_t operator()(const std::pair& p) const { + auto h1 = std::hash{}(p.first); + auto h2 = std::hash{}(p.second); + return h1 ^ (h2 << 1); + } +}; + void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) { static bool doOnce = false; // const string eventNames[] = {"world loaded", "world unloaded", "map loaded", "map unloaded", "viewscreen changed", "core initialized", "begin unload", "paused", "unpaused"}; @@ -259,6 +272,7 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event } if ( event == DFHack::SC_MAP_UNLOADED ) { lastJobId = -1; + startedJobs.clear(); for (auto &prevJob : prevJobs) { Job::deleteJobStruct(prevJob.second, true); } @@ -276,9 +290,9 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event gameLoaded = false; multimap copy(handlers[EventType::UNLOAD].begin(), handlers[EventType::UNLOAD].end()); - for (auto &key_value : copy) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for map unloaded state change event\n"); - key_value.second.eventHandler(out, nullptr); + handle.eventHandler(out, nullptr); } } else if ( event == DFHack::SC_MAP_LOADED ) { /* @@ -375,8 +389,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) { continue; int32_t eventFrequency = -100; if ( a != EventType::TICK ) - for (auto &key_value : handlers[a]) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : handlers[a]) { if (handle.freq < eventFrequency || eventFrequency == -100 ) eventFrequency = handle.freq; } @@ -439,8 +452,7 @@ static void manageJobInitiatedEvent(color_ostream& out) { continue; if ( link->item->id <= lastJobId ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for job initiated event\n"); handle.eventHandler(out, (void*)link->item); } @@ -453,22 +465,27 @@ static void manageJobStartedEvent(color_ostream& out) { if (!df::global::world) return; - static unordered_set startedJobs; - // iterate event handler callbacks multimap copy(handlers[EventType::JOB_STARTED].begin(), handlers[EventType::JOB_STARTED].end()); - for (df::job_list_link* link = df::global::world->jobs.list.next; link != nullptr; link = link->next) { - df::job* job = link->item; - if (job && Job::getWorker(job) && !startedJobs.count(job->id)) { - startedJobs.emplace(job->id); - for (auto &key_value : copy) { - auto &handler = key_value.second; - // the jobs must have a worker to start + + unordered_set newStartedJobs; + + for (df::job_list_link* link = &df::global::world->jobs.list; link->next != nullptr; link = link->next) { + df::job* job = link->next->item; + if (!job || !Job::getWorker(job)) + continue; + + int32_t j_id = job->id; + newStartedJobs.emplace(j_id); + if (!startedJobs.count(j_id)) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for job started event\n"); - handler.eventHandler(out, job); + handle.eventHandler(out, job); } } } + + startedJobs = newStartedJobs; } //helper function for manageJobCompletedEvent @@ -483,15 +500,16 @@ TODO: consider checking item creation / experience gain just in case static void manageJobCompletedEvent(color_ostream& out) { if (!df::global::world) return; + int32_t tick0 = eventLastTick[EventType::JOB_COMPLETED]; int32_t tick1 = df::global::world->frame_counter; multimap copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end()); - map nowJobs; + unordered_map nowJobs; for ( df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next ) { if ( link->item == nullptr ) continue; - nowJobs[link->item->id] = link->item; + nowJobs.emplace(link->item->id, link->item); } #if 0 @@ -573,10 +591,9 @@ static void manageJobCompletedEvent(color_ostream& out) { continue; //still false positive if cancelled at EXACTLY the right time, but experiments show this doesn't happen - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for repeated job completed event\n"); - handle.eventHandler(out, (void*)&job0); + handle.eventHandler(out, (void*) &job0); } continue; } @@ -586,28 +603,27 @@ static void manageJobCompletedEvent(color_ostream& out) { if ( job0.flags.bits.repeat || job0.completion_timer != 0 ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for job completed event\n"); - handle.eventHandler(out, (void*)&job0); + handle.eventHandler(out, (void*) &job0); } } //erase old jobs, copy over possibly altered jobs - for (auto &prevJob : prevJobs) { - Job::deleteJobStruct(prevJob.second, true); + for (auto &[_,prev_job] : prevJobs) { + Job::deleteJobStruct(prev_job, true); } prevJobs.clear(); //create new jobs - for (auto &nowJob : nowJobs) { + for (auto &[_,now_job] : nowJobs) { /*map::iterator i = prevJobs.find((*j).first); if ( i != prevJobs.end() ) { continue; }*/ - df::job* newJob = Job::cloneJobStruct(nowJob.second, true); - prevJobs[newJob->id] = newJob; + df::job* newJob = Job::cloneJobStruct(now_job, true); + prevJobs.emplace(newJob->id, newJob); } } @@ -617,15 +633,17 @@ static void manageNewUnitActiveEvent(color_ostream& out) { multimap copy(handlers[EventType::UNIT_NEW_ACTIVE].begin(), handlers[EventType::UNIT_NEW_ACTIVE].end()); // iterate event handler callbacks - for (auto &key_value : copy) { - auto &handler = key_value.second; - for (df::unit* unit : df::global::world->units.active) { - int32_t id = unit->id; - if (!activeUnits.count(id)) { - activeUnits.emplace(id); - DEBUG(log,out).print("calling handler for new unit event\n"); - handler.eventHandler(out, (void*) intptr_t(id)); // intptr_t() avoids cast from smaller type warning - } + vector new_active_unit_ids; + for (df::unit* unit : df::global::world->units.active) { + if (!activeUnits.count(unit->id)) { + activeUnits.emplace(unit->id); + new_active_unit_ids.emplace_back(unit->id); + } + } + for (int32_t unit_id : new_active_unit_ids) { + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for new unit event\n"); + handle.eventHandler(out, (void*) intptr_t(unit_id)); // intptr_t() avoids cast from smaller type warning } } } @@ -635,22 +653,26 @@ static void manageUnitDeathEvent(color_ostream& out) { if (!df::global::world) return; multimap copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end()); + vector dead_unit_ids; for (auto unit : df::global::world->units.all) { //if ( unit->counters.death_id == -1 ) { if ( Units::isActive(unit) ) { livingUnits.insert(unit->id); continue; } + if (!Units::isDead(unit)) continue; // for units that have left the map but aren't dead //dead: if dead since last check, trigger events if ( livingUnits.find(unit->id) == livingUnits.end() ) continue; + livingUnits.erase(unit->id); + dead_unit_ids.emplace_back(unit->id); + } - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (int32_t unit_id : dead_unit_ids) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit death event\n"); - handle.eventHandler(out, (void*)intptr_t(unit->id)); + handle.eventHandler(out, (void*)intptr_t(unit_id)); } - livingUnits.erase(unit->id); } } @@ -666,6 +688,8 @@ static void manageItemCreationEvent(color_ostream& out) { multimap copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end()); size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false); if ( index != 0 ) index--; + + std::vector created_items; for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) { df::item* item = df::global::world->items.all[a]; //already processed @@ -683,12 +707,17 @@ static void manageItemCreationEvent(color_ostream& out) { //spider webs don't count if ( item->flags.bits.spider_web ) continue; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + created_items.push_back(item->id); + } + + // handle all created items + for (int32_t item_id : created_items) { + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for item created event\n"); - handle.eventHandler(out, (void*)intptr_t(item->id)); + handle.eventHandler(out, (void*)intptr_t(item_id)); } } + nextItem = *df::global::item_next_id; } @@ -703,6 +732,7 @@ static void manageBuildingEvent(color_ostream& out) { **/ multimap copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end()); //first alert people about new buildings + vector new_buildings; for ( int32_t a = nextBuilding; a < *df::global::building_next_id; a++ ) { int32_t index = df::building::binsearch_index(df::global::world->buildings.all, a); if ( index == -1 ) { @@ -711,30 +741,34 @@ static void manageBuildingEvent(color_ostream& out) { continue; } buildings.insert(a); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for created building event\n"); - handle.eventHandler(out, (void*)intptr_t(a)); - } + new_buildings.emplace_back(a); + } nextBuilding = *df::global::building_next_id; //now alert people about destroyed buildings - for ( auto a = buildings.begin(); a != buildings.end(); ) { - int32_t id = *a; + for ( auto it = buildings.begin(); it != buildings.end(); ) { + int32_t id = *it; int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id); if ( index != -1 ) { - a++; + ++it; continue; } - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for destroyed building event\n"); handle.eventHandler(out, (void*)intptr_t(id)); } - a = buildings.erase(a); + it = buildings.erase(it); } + + //alert people about newly created buildings + std::for_each(new_buildings.begin(), new_buildings.end(), [&](int32_t building){ + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for created building event\n"); + handle.eventHandler(out, (void*)intptr_t(building)); + } + }); } static void manageConstructionEvent(color_ostream& out) { @@ -743,35 +777,41 @@ static void manageConstructionEvent(color_ostream& out) { //unordered_set constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end()); multimap copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end()); - // find & send construction removals - for (auto iter = constructions.begin(); iter != constructions.end();) { - auto &construction = *iter; - // if we can't find it, it was removed - if (df::construction::find(construction.pos) != nullptr) { - ++iter; - continue; + + unordered_set next_construction_set; // will be swapped with constructions + next_construction_set.reserve(constructions.bucket_count()); + vector new_constructions; + + // find new constructions - swapping found constructions over from constructions to next_construction_set + for (auto c : df::global::world->constructions) { + auto &construction = *c; + auto it = constructions.find(construction); + if (it == constructions.end()) { + // handle new construction event later + new_constructions.emplace_back(construction); } - // send construction to handlers, because it was removed - for (const auto &key_value: copy) { - EventHandler handle = key_value.second; + else { + constructions.erase(it); + } + next_construction_set.emplace(construction); + } + + constructions.swap(next_construction_set); + + // now next_construction_set contains all the constructions that were removed (not found in df::global::world->constructions) + for (auto& construction : next_construction_set) { + // handle construction removed event + for (const auto &[_,handle]: copy) { DEBUG(log,out).print("calling handler for destroyed construction event\n"); handle.eventHandler(out, (void*) &construction); } - // erase from existent constructions - iter = constructions.erase(iter); } - // find & send construction additions - for (auto c: df::global::world->constructions) { - auto &construction = *c; - // add construction to constructions, if it isn't already present - if (constructions.emplace(construction).second) { - // send construction to handlers, because it is new - for (const auto &key_value: copy) { - EventHandler handle = key_value.second; - DEBUG(log,out).print("calling handler for created construction event\n"); - handle.eventHandler(out, (void*) &construction); - } + // now handle all the new constructions + for (auto& construction : new_constructions) { + for (const auto &[_,handle]: copy) { + DEBUG(log,out).print("calling handler for created construction event\n"); + handle.eventHandler(out, (void*) &construction); } } } @@ -781,6 +821,8 @@ static void manageSyndromeEvent(color_ostream& out) { return; multimap copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end()); int32_t highestTime = -1; + + std::vector new_syndrome_data; for (auto unit : df::global::world->units.all) { /* @@ -795,14 +837,16 @@ static void manageSyndromeEvent(color_ostream& out) { if ( startTime <= lastSyndromeTime ) continue; - SyndromeData data(unit->id, b); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for syndrome event\n"); - handle.eventHandler(out, (void*)&data); - } + new_syndrome_data.emplace_back(unit->id, b); + } + } + for (auto& data : new_syndrome_data) { + for (auto &[_,handle] : copy) { + DEBUG(log,out).print("calling handler for syndrome event\n"); + handle.eventHandler(out, (void*)&data); } } + lastSyndromeTime = highestTime; } @@ -815,8 +859,7 @@ static void manageInvasionEvent(color_ostream& out) { return; nextInvasion = df::global::plotinfo->invasions.next_id; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for invasion event\n"); handle.eventHandler(out, (void*)intptr_t(nextInvasion-1)); } @@ -829,6 +872,18 @@ static void manageEquipmentEvent(color_ostream& out) { unordered_map itemIdToInventoryItem; unordered_set currentlyEquipped; + + vector equipment_pickups; + vector equipment_drops; + vector equipment_changes; + + + // This vector stores the pointers to newly created changed items + // needed as the stack allocated temporary (in the loop) is lost when we go to + // handle the event calls, so we move that data to the heap if its needed, + // and then once we are done we delete everything. + vector changed_items; + for (auto unit : df::global::world->units.all) { itemIdToInventoryItem.clear(); currentlyEquipped.clear(); @@ -856,40 +911,30 @@ static void manageEquipmentEvent(color_ostream& out) { auto c = itemIdToInventoryItem.find(dfitem_new->item->id); if ( c == itemIdToInventoryItem.end() ) { //new item equipped (probably just picked up) - InventoryChangeData data(unit->id, nullptr, &item_new); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for new item equipped inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(item_new)); + equipment_pickups.emplace_back(unit->id, nullptr, changed_items.back()); continue; } - InventoryItem item_old = (*c).second; + InventoryItem item_old = c->second; df::unit_inventory_item& item0 = item_old.item; df::unit_inventory_item& item1 = item_new.item; if ( item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && item0.wound_id == item1.wound_id ) continue; //some sort of change in how it's equipped - - InventoryChangeData data(unit->id, &item_old, &item_new); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(item_new)); + InventoryItem* item_new_ptr = changed_items.back(); + changed_items.emplace_back(new InventoryItem(item_old)); + InventoryItem* item_old_ptr = changed_items.back(); + equipment_changes.emplace_back(unit->id, item_old_ptr, item_new_ptr); } //check for dropped items for (auto i : v) { if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() ) continue; //TODO: delete ptr if invalid - InventoryChangeData data(unit->id, &i, nullptr); - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; - DEBUG(log,out).print("calling handler for dropped item inventory change event\n"); - handle.eventHandler(out, (void*)&data); - } + changed_items.emplace_back(new InventoryItem(i)); + equipment_drops.emplace_back(unit->id, changed_items.back(), nullptr); } if ( !hadEquipment ) delete temp; @@ -902,6 +947,31 @@ static void manageEquipmentEvent(color_ostream& out) { equipment.push_back(item); } } + + // now handle events + std::for_each(equipment_pickups.begin(), equipment_pickups.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for new item equipped inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + std::for_each(equipment_drops.begin(), equipment_drops.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for dropped item inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + std::for_each(equipment_changes.begin(), equipment_changes.end(), [&](InventoryChangeData& data) { + for (auto &[_, handle] : copy) { + DEBUG(log,out).print("calling handler for inventory change event\n"); + handle.eventHandler(out, (void*) &data); + } + }); + + // clean up changed items list + std::for_each(changed_items.begin(), changed_items.end(), [](InventoryItem* p){ + delete p; + }); } static void updateReportToRelevantUnits() { @@ -939,8 +1009,7 @@ static void manageReportEvent(color_ostream& out) { for ( ; idx < reports.size(); idx++ ) { df::report* report = reports[idx]; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for report event\n"); handle.eventHandler(out, (void*)intptr_t(report->id)); } @@ -981,7 +1050,7 @@ static void manageUnitAttackEvent(color_ostream& out) { if ( strikeReports.empty() ) return; updateReportToRelevantUnits(); - map > alreadyDone; + unordered_set, hash_pair> already_done; for (int reportId : strikeReports) { df::report* report = df::report::find(reportId); if ( !report ) @@ -1011,27 +1080,25 @@ static void manageUnitAttackEvent(color_ostream& out) { UnitAttackData data{}; data.report_id = report->id; - if ( wound1 && !alreadyDone[unit1->id][unit2->id] ) { + if ( wound1 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) { data.attacker = unit1->id; data.defender = unit2->id; data.wound = wound1->id; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit1 attack unit attack event\n"); handle.eventHandler(out, (void*)&data); } } - if ( wound2 && !alreadyDone[unit1->id][unit2->id] ) { + if ( wound2 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) { data.attacker = unit2->id; data.defender = unit1->id; data.wound = wound2->id; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit2 attack unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1041,9 +1108,9 @@ static void manageUnitAttackEvent(color_ostream& out) { data.attacker = unit2->id; data.defender = unit1->id; data.wound = -1; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit1 killed unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1053,9 +1120,9 @@ static void manageUnitAttackEvent(color_ostream& out) { data.attacker = unit1->id; data.defender = unit2->id; data.wound = -1; - alreadyDone[data.attacker][data.defender] = 1; - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + + already_done.emplace(unit1->id, unit2->id); + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for unit2 killed unit attack event\n"); handle.eventHandler(out, (void*)&data); } @@ -1313,8 +1380,7 @@ static void manageInteractionEvent(color_ostream& out) { lastAttacker = df::unit::find(data.attacker); //lastDefender = df::unit::find(data.defender); //fire event - for (auto &key_value : copy) { - EventHandler &handle = key_value.second; + for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for interaction event\n"); handle.eventHandler(out, (void*)&data); } diff --git a/library/modules/Graphic.cpp b/library/modules/Graphic.cpp index 7f6b8f4d3..b55ee83ed 100644 --- a/library/modules/Graphic.cpp +++ b/library/modules/Graphic.cpp @@ -44,7 +44,7 @@ using namespace DFHack; std::unique_ptr DFHack::createGraphic() { - return dts::make_unique(); + return std::make_unique(); } bool Graphic::Register(DFTileSurface* (*func)(int,int)) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ee5eaa31c..475ac5728 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -69,6 +69,7 @@ using namespace DFHack; #include "df/item_corpsepiecest.h" #include "df/item_corpsest.h" #include "df/job.h" +#include "df/legend_pagest.h" #include "df/occupation.h" #include "df/plant.h" #include "df/popup_message.h" @@ -84,8 +85,11 @@ using namespace DFHack; #include "df/unit.h" #include "df/unit_inventory_item.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_legendsst.h" #include "df/viewscreen_new_regionst.h" +#include "df/viewscreen_setupdwarfgamest.h" #include "df/viewscreen_titlest.h" +#include "df/viewscreen_worldst.h" #include "df/world.h" const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size @@ -159,7 +163,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(title) DEFINE_GET_FOCUS_STRING_HANDLER(new_region) { - if (screen->doing_mods) + if (screen->raw_load) + focusStrings.push_back(baseFocus + "/Loading"); + else if (screen->doing_mods) focusStrings.push_back(baseFocus + "/Mods"); else if (screen->doing_simple_params) focusStrings.push_back(baseFocus + "/Basic"); @@ -170,6 +176,60 @@ DEFINE_GET_FOCUS_STRING_HANDLER(new_region) focusStrings.push_back(baseFocus); } +DEFINE_GET_FOCUS_STRING_HANDLER(setupdwarfgame) +{ + if (screen->doing_custom_settings) + focusStrings.push_back(baseFocus + "/CustomSettings"); + else if (game->main_interface.options.open) + focusStrings.push_back(baseFocus + "/Abort"); + else if (screen->initial_selection == 1) + focusStrings.push_back(baseFocus + "/Default"); + else if (game->main_interface.name_creator.open) { + switch (game->main_interface.name_creator.context) { + case df::name_creator_context_type::EMBARK_FORT_NAME: + focusStrings.push_back(baseFocus + "/FortName"); + break; + case df::name_creator_context_type::EMBARK_GROUP_NAME: + focusStrings.push_back(baseFocus + "/GroupName"); + break; + default: + break; + } + } + else if (game->main_interface.image_creator.open) { + focusStrings.push_back(baseFocus + "/GroupSymbol"); + } + else if (screen->viewing_objections != 0) + focusStrings.push_back(baseFocus + "/Objections"); + else { + switch (screen->mode) { + case 0: focusStrings.push_back(baseFocus + "/Dwarves"); break; + case 1: focusStrings.push_back(baseFocus + "/Items"); break; + case 2: focusStrings.push_back(baseFocus + "/Animals"); break; + default: + break; + } + } + + if (focusStrings.empty()) + focusStrings.push_back(baseFocus + "/Default"); +} + +DEFINE_GET_FOCUS_STRING_HANDLER(legends) +{ + if (screen->init_stage != -1) + focusStrings.push_back(baseFocus + "/Loading"); + else if (screen->page.size() <= 1) + focusStrings.push_back(baseFocus + "/Default"); + else + focusStrings.push_back(baseFocus + '/' + screen->page[screen->active_page_index]->header); +} + +DEFINE_GET_FOCUS_STRING_HANDLER(world) +{ + focusStrings.push_back(baseFocus + '/' + enum_item_key(screen->view_mode)); +} + DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) { std::string newFocusString; @@ -182,27 +242,51 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) if (game->main_interface.info.open) { newFocusString = baseFocus; newFocusString += "/Info"; - newFocusString += "/" + enum_item_key(game->main_interface.info.current_mode); + newFocusString += '/' + enum_item_key(game->main_interface.info.current_mode); switch(game->main_interface.info.current_mode) { case df::enums::info_interface_mode_type::CREATURES: - newFocusString += "/" + enum_item_key(game->main_interface.info.creatures.current_mode); + if (game->main_interface.info.creatures.showing_overall_training) + newFocusString += "/OverallTraining"; + else if (game->main_interface.info.creatures.showing_activity_details) + newFocusString += "/ActivityDetails"; + else if (game->main_interface.info.creatures.adding_trainer) + newFocusString += "/AddingTrainer"; + else if (game->main_interface.info.creatures.assign_work_animal) + newFocusString += "/AssignWorkAnimal"; + else + newFocusString += '/' + enum_item_key(game->main_interface.info.creatures.current_mode); break; case df::enums::info_interface_mode_type::BUILDINGS: - newFocusString += "/" + enum_item_key(game->main_interface.info.buildings.mode); + newFocusString += '/' + enum_item_key(game->main_interface.info.buildings.mode); break; case df::enums::info_interface_mode_type::LABOR: - newFocusString += "/" + enum_item_key(game->main_interface.info.labor.mode); + newFocusString += '/' + enum_item_key(game->main_interface.info.labor.mode); break; case df::enums::info_interface_mode_type::ARTIFACTS: - newFocusString += "/" + enum_item_key(game->main_interface.info.artifacts.mode); + newFocusString += '/' + enum_item_key(game->main_interface.info.artifacts.mode); break; case df::enums::info_interface_mode_type::JUSTICE: - newFocusString += "/" + enum_item_key(game->main_interface.info.justice.current_mode); + if (game->main_interface.info.justice.interrogating) + newFocusString += "/Interrogating"; + else if (game->main_interface.info.justice.convicting) + newFocusString += "/Convicting"; + else + newFocusString += '/' + enum_item_key(game->main_interface.info.justice.current_mode); break; case df::enums::info_interface_mode_type::WORK_ORDERS: if (game->main_interface.info.work_orders.conditions.open) newFocusString += "/Conditions"; + else if (game->main_interface.create_work_order.open) + newFocusString += "/Create"; + else + newFocusString += "/Default"; + break; + case df::enums::info_interface_mode_type::ADMINISTRATORS: + if (game->main_interface.info.administrators.choosing_candidate) + newFocusString += "/Candidates"; + else if (game->main_interface.info.administrators.assigning_symbol) + newFocusString += "/Symbols"; else newFocusString += "/Default"; break; @@ -215,7 +299,93 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) if (game->main_interface.view_sheets.open) { newFocusString = baseFocus; newFocusString += "/ViewSheets"; - newFocusString += "/" + enum_item_key(game->main_interface.view_sheets.active_sheet); + newFocusString += '/' + enum_item_key(game->main_interface.view_sheets.active_sheet); + switch (game->main_interface.view_sheets.active_sheet) { + case df::view_sheet_type::UNIT: + switch (game->main_interface.view_sheets.active_sub_tab) { + case 0: newFocusString += "/Overview"; break; + case 1: newFocusString += "/Items"; break; + case 2: + newFocusString += "/Health"; + switch (game->main_interface.view_sheets.unit_health_active_tab) { + case 0: newFocusString += "/Status"; break; + case 1: newFocusString += "/Wounds"; break; + case 2: newFocusString += "/Treatment"; break; + case 3: newFocusString += "/History"; break; + case 4: newFocusString += "/Description"; break; + default: break; + } + break; + case 3: + newFocusString += "/Skills"; + switch (game->main_interface.view_sheets.unit_skill_active_tab) { + case 0: newFocusString += "/Labor"; break; + case 1: newFocusString += "/Combat"; break; + case 2: newFocusString += "/Social"; break; + case 3: newFocusString += "/Other"; break; + case 4: + newFocusString += "/Knowledge"; + if (game->main_interface.view_sheets.skill_description_raw_str.size()) + newFocusString += "/Details"; + else + newFocusString += "/Default"; + break; + default: break; + } + break; + case 4: newFocusString += "/Rooms"; break; + case 5: + newFocusString += "/Labor"; + switch (game->main_interface.view_sheets.unit_labor_active_tab) { + case 0: newFocusString += "/WorkDetails"; break; + case 1: newFocusString += "/Workshops"; break; + case 2: newFocusString += "/Locations"; break; + case 3: newFocusString += "/WorkAnimals"; break; + default: break; + } + break; + case 6: newFocusString += "/Relations"; break; + case 7: newFocusString += "/Groups"; break; + case 8: + newFocusString += "/Military"; + switch (game->main_interface.view_sheets.unit_military_active_tab) { + case 0: newFocusString += "/Squad"; break; + case 1: newFocusString += "/Uniform"; break; + case 2: newFocusString += "/Kills"; break; + default: break; + } + break; + case 9: + newFocusString += "/Thoughts"; + switch (game->main_interface.view_sheets.thoughts_active_tab) { + case 0: newFocusString += "/Recent"; break; + case 1: newFocusString += "/Memories"; break; + default: break; + } + break; + case 10: + newFocusString += "/Personality"; + switch (game->main_interface.view_sheets.personality_active_tab) { + case 0: newFocusString += "/Traits"; break; + case 1: newFocusString += "/Values"; break; + case 2: newFocusString += "/Preferences"; break; + case 3: newFocusString += "/Needs"; break; + default: break; + } + break; + default: + break; + } + break; + case df::view_sheet_type::BUILDING: + if (game->main_interface.view_sheets.linking_lever) + newFocusString = baseFocus + "/LinkingLever"; + else if (auto bld = df::building::find(game->main_interface.view_sheets.viewing_bldid)) + newFocusString += '/' + enum_item_key(bld->getType()); + break; + default: + break; + } focusStrings.push_back(newFocusString); } @@ -239,7 +409,7 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) newFocusString += "/Zone"; if (game->main_interface.civzone.cur_bld) { newFocusString += "/Some"; - newFocusString += "/" + enum_item_key(game->main_interface.civzone.cur_bld->type); + newFocusString += '/' + enum_item_key(game->main_interface.civzone.cur_bld->type); } break; case df::enums::main_bottom_mode_type::ZONE_PAINT: @@ -328,11 +498,15 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) if (game->main_interface.trade.open) { newFocusString = baseFocus; newFocusString += "/Trade"; + if (game->main_interface.trade.choosing_merchant) + newFocusString += "/ChoosingMerchant"; + else + newFocusString += "/Default"; focusStrings.push_back(newFocusString); } if (game->main_interface.job_details.open) { newFocusString = baseFocus; - newFocusString += "/JobDetails"; + newFocusString += "/JobDetails/" + enum_item_key(game->main_interface.job_details.context); focusStrings.push_back(newFocusString); } if (game->main_interface.assign_trade.open) { @@ -377,6 +551,7 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) if (game->main_interface.unit_selector.open) { newFocusString = baseFocus; newFocusString += "/UnitSelector"; + newFocusString += '/' + enum_item_key(game->main_interface.unit_selector.context); focusStrings.push_back(newFocusString); } if (game->main_interface.announcement_alert.open) { @@ -411,7 +586,13 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) } if (game->main_interface.location_selector.open) { newFocusString = baseFocus; - newFocusString += "/LocationSelector"; + newFocusString += "/LocationSelector/"; + if (game->main_interface.location_selector.choosing_temple_religious_practice) + newFocusString += "Temple"; + else if (game->main_interface.location_selector.choosing_craft_guild) + newFocusString += "Guildhall"; + else + newFocusString += "Default"; focusStrings.push_back(newFocusString); } if (game->main_interface.location_details.open) { @@ -459,11 +640,6 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) newFocusString += "/AssignUniform"; focusStrings.push_back(newFocusString); } - if (game->main_interface.create_work_order.open) { - newFocusString = baseFocus; - newFocusString += "/CreateWorkOrder"; - focusStrings.push_back(newFocusString); - } if (game->main_interface.hotkey.open) { newFocusString = baseFocus; newFocusString += "/Hotkey"; @@ -509,7 +685,7 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dungeonmode) if (!adventure) return; - focus += "/" + enum_item_key(adventure->menu); + focus += '/' + enum_item_key(adventure->menu); } */ @@ -1137,9 +1313,11 @@ bool Gui::any_stockpile_hotkey(df::viewscreen* top) } df::building_stockpilest* Gui::getAnyStockpile(df::viewscreen* top) { - if (matchFocusString("dwarfmode/Some/Stockpile")) { + if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedStockpile(); + + if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::STOCKPILE) return game->main_interface.stockpile.cur_bld; - } return NULL; } @@ -1158,9 +1336,12 @@ bool Gui::any_civzone_hotkey(df::viewscreen* top) { } df::building_civzonest *Gui::getAnyCivZone(df::viewscreen* top) { - if (matchFocusString("dwarfmode/Zone")) { + if (auto dfscreen = dfhack_viewscreen::try_cast(top)) + return dfscreen->getSelectedCivZone(); + + if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::ZONE) return game->main_interface.civzone.cur_bld; - } + return NULL; } @@ -1175,8 +1356,6 @@ df::building_civzonest *Gui::getSelectedCivZone(color_ostream &out, bool quiet) df::building *Gui::getAnyBuilding(df::viewscreen *top) { - using df::global::game; - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) return dfscreen->getSelectedBuilding(); @@ -1437,7 +1616,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce if (flags.bits.D_DISPLAY) { world->status.display_timer = ANNOUNCE_DISPLAY_TIME; - Gui::writeToGamelog("x" + to_string(repeat_count + 1)); + Gui::writeToGamelog('x' + to_string(repeat_count + 1)); } return -1; } @@ -1699,7 +1878,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) if (a_flags.bits.D_DISPLAY) { world->status.display_timer = r.display_timer; - Gui::writeToGamelog("x" + to_string(repeat_count + 1)); + Gui::writeToGamelog('x' + to_string(repeat_count + 1)); } DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str()); return true; @@ -1919,13 +2098,13 @@ void Gui::resetDwarfmodeView(bool pause) *df::global::pause_state = true; } -bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center) +bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center, bool highlight) { // Reverse-engineered from DF announcement and scrolling code using df::global::window_x; using df::global::window_y; using df::global::window_z; - if (!window_x || !window_y || !window_z || !world) + if (!window_x || !window_y || !window_z || !world || !game) return false; auto dims = getDwarfmodeViewDims(); @@ -1962,6 +2141,12 @@ bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center) game->minimap.update = true; game->minimap.mustmake = true; + if (highlight) { + game->main_interface.recenter_indicator_m.x = x; + game->main_interface.recenter_indicator_m.y = y; + game->main_interface.recenter_indicator_m.z = z; + } + return true; } @@ -2072,7 +2257,7 @@ bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t } // returns the map coordinates that the mouse cursor is over -df::coord Gui::getMousePos() +df::coord Gui::getMousePos(bool allow_out_of_bounds) { df::coord pos; if (gps && gps->precise_mouse_x > -1) { @@ -2086,7 +2271,7 @@ df::coord Gui::getMousePos() pos.y += gps->mouse_y; } } - if (!Maps::isValidTilePos(pos.x, pos.y, pos.z)) + if (!allow_out_of_bounds && !Maps::isValidTilePos(pos.x, pos.y, pos.z)) return df::coord(); return pos; } diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 03d6cda4a..a91ea3470 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -39,6 +39,7 @@ distribution. using namespace std; #include "ModuleFactory.h" +#include "modules/Job.h" #include "modules/MapCache.h" #include "modules/Materials.h" #include "modules/Items.h" @@ -48,8 +49,17 @@ using namespace std; #include "df/body_part_template_flags.h" #include "df/building.h" #include "df/building_actual.h" +#include "df/building_tradedepotst.h" +#include "df/builtin_mats.h" +#include "df/caravan_state.h" #include "df/caste_raw.h" #include "df/creature_raw.h" +#include "df/dfhack_material_category.h" +#include "df/entity_buy_prices.h" +#include "df/entity_buy_requests.h" +#include "df/entity_sell_category.h" +#include "df/entity_sell_prices.h" +#include "df/entity_raw.h" #include "df/general_ref.h" #include "df/general_ref_building_holderst.h" #include "df/general_ref_contained_in_itemst.h" @@ -410,6 +420,7 @@ bool ItemTypeInfo::matches(const df::job_item &item, MaterialInfo *mat, xmask1.bits.cookable = true; break; + // TODO: split this into BOX and BAG case BOX: OK(1,bag); OK(1,sand_bearing); OK(1,milk); OK(2,dye); OK(2,plaster_containing); @@ -826,15 +837,15 @@ std::string Items::getDescription(df::item *item, int type, bool decorate) item->getItemDescription(&tmp, type); if (decorate) { - if (item->flags.bits.foreign) - tmp = "(" + tmp + ")"; - addQuality(tmp, item->getQuality()); if (item->isImproved()) { tmp = '\xAE' + tmp + '\xAF'; // («) + tmp + (») addQuality(tmp, item->getImprovementQuality()); } + + if (item->flags.bits.foreign) + tmp = "(" + tmp + ")"; } return tmp; @@ -1195,18 +1206,24 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, int16_t mat switch (item_type) { case item_type::BAR: - case item_type::SMALLGEM: case item_type::BLOCKS: case item_type::SKIN_TANNED: value = 5; break; - case item_type::ROUGH: + case item_type::SMALLGEM: + value = 20; + break; + case item_type::BOULDER: case item_type::WOOD: value = 3; break; + case item_type::ROUGH: + value = 6; + break; + case item_type::DOOR: case item_type::FLOODGATE: case item_type::BED: @@ -1222,6 +1239,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, int16_t mat case item_type::TABLE: case item_type::COFFIN: case item_type::BOX: + case item_type::BAG: case item_type::BIN: case item_type::ARMORSTAND: case item_type::WEAPONRACK: @@ -1365,6 +1383,7 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, int16_t mat case item_type::COIN: case item_type::GLOB: case item_type::ORTHOPEDIC_CAST: + case item_type::BRANCH: value = 1; break; @@ -1433,7 +1452,506 @@ int Items::getItemBaseValue(int16_t item_type, int16_t item_subtype, int16_t mat return value; } -int Items::getValue(df::item *item) +static int32_t get_war_multiplier(df::item *item, df::caravan_state *caravan) { + static const int32_t DEFAULT_WAR_MULTIPLIER = 256; + + if (!caravan) + return DEFAULT_WAR_MULTIPLIER; + auto caravan_he = df::historical_entity::find(caravan->entity); + if (!caravan_he) + return DEFAULT_WAR_MULTIPLIER; + int32_t war_alignment = caravan_he->entity_raw->sphere_alignment[df::sphere_type::WAR]; + if (war_alignment == DEFAULT_WAR_MULTIPLIER) + return DEFAULT_WAR_MULTIPLIER; + switch (item->getType()) { + case df::item_type::WEAPON: + { + auto weap_def = df::itemdef_weaponst::find(item->getSubtype()); + auto caravan_cre_raw = df::creature_raw::find(caravan_he->race); + if (!weap_def || !caravan_cre_raw || caravan_cre_raw->adultsize < weap_def->minimum_size) + return DEFAULT_WAR_MULTIPLIER; + break; + } + case df::item_type::ARMOR: + case df::item_type::SHOES: + case df::item_type::HELM: + case df::item_type::GLOVES: + case df::item_type::PANTS: + { + if (item->getEffectiveArmorLevel() <= 0) + return DEFAULT_WAR_MULTIPLIER; + auto caravan_cre_raw = df::creature_raw::find(caravan_he->race); + auto maker_cre_raw = df::creature_raw::find(item->getMakerRace()); + if (!caravan_cre_raw || !maker_cre_raw) + return DEFAULT_WAR_MULTIPLIER; + if (caravan_cre_raw->adultsize < ((maker_cre_raw->adultsize * 6) / 7)) + return DEFAULT_WAR_MULTIPLIER; + if (caravan_cre_raw->adultsize > ((maker_cre_raw->adultsize * 8) / 7)) + return DEFAULT_WAR_MULTIPLIER; + break; + } + case df::item_type::SHIELD: + case df::item_type::AMMO: + case df::item_type::BACKPACK: + case df::item_type::QUIVER: + break; + default: + return DEFAULT_WAR_MULTIPLIER; + } + return war_alignment; +} + +static const int32_t DEFAULT_AGREEMENT_MULTIPLIER = 128; + +static int32_t get_buy_request_multiplier(df::item *item, const df::entity_buy_prices *buy_prices) { + if (!buy_prices) + return DEFAULT_AGREEMENT_MULTIPLIER; + + int16_t item_type = item->getType(); + int16_t item_subtype = item->getSubtype(); + int16_t mat_type = item->getMaterial(); + int32_t mat_subtype = item->getMaterialIndex(); + + for (size_t idx = 0; idx < buy_prices->price.size(); ++idx) { + if (buy_prices->items->item_type[idx] != item_type) + continue; + if (buy_prices->items->item_subtype[idx] != -1 && buy_prices->items->item_subtype[idx] != item_subtype) + continue; + if (buy_prices->items->mat_types[idx] != -1 && buy_prices->items->mat_types[idx] != mat_type) + continue; + if (buy_prices->items->mat_indices[idx] != -1 && buy_prices->items->mat_indices[idx] != mat_subtype) + continue; + return buy_prices->price[idx]; + } + return DEFAULT_AGREEMENT_MULTIPLIER; +} + +template +static int get_price(const std::vector &res, int32_t val, const std::vector &pri) { + for (size_t idx = 0; idx < res.size(); ++idx) { + if (res[idx] == val && pri.size() > idx) + return pri[idx]; + } + return -1; +} + +template +static int get_price(const std::vector &mat_res, int32_t mat, const std::vector &gloss_res, int32_t gloss, const std::vector &pri) { + for (size_t idx = 0; idx < mat_res.size(); ++idx) { + if (mat_res[idx] == mat && (gloss_res[idx] == -1 || gloss_res[idx] == gloss) && pri.size() > idx) + return pri[idx]; + } + return -1; +} + +static const uint16_t PLANT_BASE = 419; +static const uint16_t NUM_PLANT_TYPES = 200; + +static int32_t get_sell_request_multiplier(df::item *item, const df::historical_entity::T_resources &resources, const std::vector *prices) { + static const df::dfhack_material_category silk_cat(df::dfhack_material_category::mask_silk); + static const df::dfhack_material_category yarn_cat(df::dfhack_material_category::mask_yarn); + static const df::dfhack_material_category leather_cat(df::dfhack_material_category::mask_leather); + + int16_t item_type = item->getType(); + int16_t item_subtype = item->getSubtype(); + int16_t mat_type = item->getMaterial(); + int32_t mat_subtype = item->getMaterialIndex(); + + bool inorganic = mat_type == df::builtin_mats::INORGANIC; + bool is_plant = (uint16_t)(mat_type - PLANT_BASE) < NUM_PLANT_TYPES; + + switch (item_type) { + case df::item_type::BAR: + if (inorganic) { + if (int32_t price = get_price(resources.metals, mat_subtype, prices[df::entity_sell_category::MetalBars]); price != -1) + return price; + } + break; + case df::item_type::SMALLGEM: + if (inorganic) { + if (int32_t price = get_price(resources.gems, mat_subtype, prices[df::entity_sell_category::SmallCutGems]); price != -1) + return price; + } + break; + case df::item_type::BLOCKS: + if (inorganic) { + if (int32_t price = get_price(resources.stones, mat_subtype, prices[df::entity_sell_category::StoneBlocks]); price != -1) + return price; + } + break; + case df::item_type::ROUGH: + if (int32_t price = get_price(resources.misc_mat.glass.mat_type, mat_type, resources.misc_mat.glass.mat_index, mat_subtype, + prices[df::entity_sell_category::Glass]); price != -1) + return price; + break; + case df::item_type::BOULDER: + if (int32_t price = get_price(resources.stones, mat_subtype, prices[df::entity_sell_category::Stone]); price != -1) + return price; + if (int32_t price = get_price(resources.misc_mat.clay.mat_type, mat_type, resources.misc_mat.clay.mat_index, mat_subtype, + prices[df::entity_sell_category::Clay]); price != -1) + return price; + break; + case df::item_type::WOOD: + if (int32_t price = get_price(resources.organic.wood.mat_type, mat_type, resources.organic.wood.mat_index, mat_subtype, + prices[df::entity_sell_category::Wood]); price != -1) + return price; + break; + case df::item_type::CHAIN: + if (is_plant) { + if (int32_t price = get_price(resources.organic.fiber.mat_type, mat_type, resources.organic.fiber.mat_index, mat_subtype, + prices[df::entity_sell_category::RopesPlant]); price != -1) + return price; + } + { + MaterialInfo mi; + mi.decode(mat_type, mat_subtype); + if (mi.isValid()) { + if (mi.matches(silk_cat)) { + if (int32_t price = get_price(resources.organic.silk.mat_type, mat_type, resources.organic.silk.mat_index, mat_subtype, + prices[df::entity_sell_category::RopesSilk]); price != -1) + return price; + } + if (mi.matches(yarn_cat)) { + if (int32_t price = get_price(resources.organic.wool.mat_type, mat_type, resources.organic.wool.mat_index, mat_subtype, + prices[df::entity_sell_category::RopesYarn]); price != -1) + return price; + } + } + } + break; + case df::item_type::FLASK: + if (int32_t price = get_price(resources.misc_mat.flasks.mat_type, mat_type, resources.misc_mat.flasks.mat_index, mat_subtype, + prices[df::entity_sell_category::FlasksWaterskins]); price != -1) + return price; + break; + case df::item_type::GOBLET: + if (int32_t price = get_price(resources.misc_mat.crafts.mat_type, mat_type, resources.misc_mat.crafts.mat_index, mat_subtype, + prices[df::entity_sell_category::CupsMugsGoblets]); price != -1) + return price; + break; + case df::item_type::INSTRUMENT: + if (int32_t price = get_price(resources.instrument_type, mat_subtype, prices[df::entity_sell_category::Instruments]); price != -1) + return price; + break; + case df::item_type::TOY: + if (int32_t price = get_price(resources.toy_type, mat_subtype, prices[df::entity_sell_category::Toys]); price != -1) + return price; + break; + case df::item_type::CAGE: + if (int32_t price = get_price(resources.misc_mat.cages.mat_type, mat_type, resources.misc_mat.cages.mat_index, mat_subtype, + prices[df::entity_sell_category::Cages]); price != -1) + return price; + break; + case df::item_type::BARREL: + if (int32_t price = get_price(resources.misc_mat.barrels.mat_type, mat_type, resources.misc_mat.barrels.mat_index, mat_subtype, + prices[df::entity_sell_category::Barrels]); price != -1) + return price; + break; + case df::item_type::BUCKET: + if (int32_t price = get_price(resources.misc_mat.barrels.mat_type, mat_type, resources.misc_mat.barrels.mat_index, mat_subtype, + prices[df::entity_sell_category::Buckets]); price != -1) + return price; + break; + case df::item_type::WEAPON: + if (int32_t price = get_price(resources.weapon_type, mat_subtype, prices[df::entity_sell_category::Weapons]); price != -1) + return price; + if (int32_t price = get_price(resources.digger_type, mat_subtype, prices[df::entity_sell_category::DiggingImplements]); price != -1) + return price; + if (int32_t price = get_price(resources.training_weapon_type, mat_subtype, prices[df::entity_sell_category::TrainingWeapons]); price != -1) + return price; + break; + case df::item_type::ARMOR: + if (int32_t price = get_price(resources.armor_type, mat_subtype, prices[df::entity_sell_category::Bodywear]); price != -1) + return price; + break; + case df::item_type::SHOES: + if (int32_t price = get_price(resources.shoes_type, mat_subtype, prices[df::entity_sell_category::Footwear]); price != -1) + return price; + break; + case df::item_type::SHIELD: + if (int32_t price = get_price(resources.shield_type, mat_subtype, prices[df::entity_sell_category::Shields]); price != -1) + return price; + break; + case df::item_type::HELM: + if (int32_t price = get_price(resources.helm_type, mat_subtype, prices[df::entity_sell_category::Headwear]); price != -1) + return price; + break; + case df::item_type::GLOVES: + if (int32_t price = get_price(resources.gloves_type, mat_subtype, prices[df::entity_sell_category::Handwear]); price != -1) + return price; + break; + case df::item_type::BAG: + { + MaterialInfo mi; + mi.decode(mat_type, mat_subtype); + if (mi.isValid() && mi.matches(leather_cat)) { + if (int32_t price = get_price(resources.organic.leather.mat_type, mat_type, resources.organic.leather.mat_index, mat_subtype, + prices[df::entity_sell_category::BagsLeather]); price != -1) + return price; + } + if (is_plant) { + if (int32_t price = get_price(resources.organic.fiber.mat_type, mat_type, resources.organic.fiber.mat_index, mat_subtype, + prices[df::entity_sell_category::BagsPlant]); price != -1) + return price; + } + if (mi.isValid() && mi.matches(silk_cat)) { + if (int32_t price = get_price(resources.organic.silk.mat_type, mat_type, resources.organic.silk.mat_index, mat_subtype, + prices[df::entity_sell_category::BagsSilk]); price != -1) + return price; + } + if (mi.isValid() && mi.matches(yarn_cat)) { + if (int32_t price = get_price(resources.organic.wool.mat_type, mat_type, resources.organic.wool.mat_index, mat_subtype, + prices[df::entity_sell_category::BagsYarn]); price != -1) + return price; + } + } + break; + case df::item_type::FIGURINE: + case df::item_type::AMULET: + case df::item_type::SCEPTER: + case df::item_type::CROWN: + case df::item_type::RING: + case df::item_type::EARRING: + case df::item_type::BRACELET: + case df::item_type::TOTEM: + case df::item_type::BOOK: + if (int32_t price = get_price(resources.misc_mat.crafts.mat_type, mat_type, resources.misc_mat.crafts.mat_index, mat_subtype, + prices[df::entity_sell_category::Crafts]); price != -1) + return price; + break; + case df::item_type::AMMO: + if (int32_t price = get_price(resources.ammo_type, mat_subtype, prices[df::entity_sell_category::Ammo]); price != -1) + return price; + break; + case df::item_type::GEM: + if (inorganic) { + if (int32_t price = get_price(resources.gems, mat_subtype, prices[df::entity_sell_category::LargeCutGems]); price != -1) + return price; + } + break; + case df::item_type::ANVIL: + if (int32_t price = get_price(resources.metal.anvil.mat_type, mat_type, resources.metal.anvil.mat_index, mat_subtype, + prices[df::entity_sell_category::Anvils]); price != -1) + return price; + break; + case df::item_type::MEAT: + if (int32_t price = get_price(resources.misc_mat.meat.mat_type, mat_type, resources.misc_mat.meat.mat_index, mat_subtype, + prices[df::entity_sell_category::Meat]); price != -1) + return price; + break; + case df::item_type::FISH: + case df::item_type::FISH_RAW: + if (int32_t price = get_price(resources.fish_races, mat_type, resources.fish_castes, mat_subtype, + prices[df::entity_sell_category::Fish]); price != -1) + return price; + break; + case df::item_type::VERMIN: + case df::item_type::PET: + if (int32_t price = get_price(resources.animals.pet_races, mat_type, resources.animals.pet_castes, mat_subtype, + prices[df::entity_sell_category::Pets]); price != -1) + return price; + break; + case df::item_type::SEEDS: + if (int32_t price = get_price(resources.seeds.mat_type, mat_type, resources.seeds.mat_index, mat_subtype, + prices[df::entity_sell_category::Seeds]); price != -1) + return price; + break; + case df::item_type::PLANT: + if (int32_t price = get_price(resources.plants.mat_type, mat_type, resources.plants.mat_index, mat_subtype, + prices[df::entity_sell_category::Plants]); price != -1) + return price; + break; + case df::item_type::SKIN_TANNED: + if (int32_t price = get_price(resources.organic.leather.mat_type, mat_type, resources.organic.leather.mat_index, mat_subtype, + prices[df::entity_sell_category::Leather]); price != -1) + return price; + break; + case df::item_type::PLANT_GROWTH: + if (is_plant) { + if (int32_t price = get_price(resources.tree_fruit_plants, mat_type, resources.tree_fruit_growths, mat_subtype, + prices[df::entity_sell_category::FruitsNuts]); price != -1) + return price; + if (int32_t price = get_price(resources.shrub_fruit_plants, mat_type, resources.shrub_fruit_growths, mat_subtype, + prices[df::entity_sell_category::GardenVegetables]); price != -1) + return price; + } + break; + case df::item_type::THREAD: + if (is_plant) { + if (int32_t price = get_price(resources.organic.fiber.mat_type, mat_type, resources.organic.fiber.mat_index, mat_subtype, + prices[df::entity_sell_category::ThreadPlant]); price != -1) + return price; + } + { + MaterialInfo mi; + mi.decode(mat_type, mat_subtype); + if (mi.isValid() && mi.matches(silk_cat)) { + if (int32_t price = get_price(resources.organic.silk.mat_type, mat_type, resources.organic.silk.mat_index, mat_subtype, + prices[df::entity_sell_category::ThreadSilk]); price != -1) + return price; + } + if (mi.isValid() && mi.matches(yarn_cat)) { + if (int32_t price = get_price(resources.organic.wool.mat_type, mat_type, resources.organic.wool.mat_index, mat_subtype, + prices[df::entity_sell_category::ThreadYarn]); price != -1) + return price; + } + } + break; + case df::item_type::CLOTH: + if (is_plant) { + if (int32_t price = get_price(resources.organic.fiber.mat_type, mat_type, resources.organic.fiber.mat_index, mat_subtype, + prices[df::entity_sell_category::ClothPlant]); price != -1) + return price; + } + { + MaterialInfo mi; + mi.decode(mat_type, mat_subtype); + if (mi.isValid() && mi.matches(silk_cat)) { + if (int32_t price = get_price(resources.organic.silk.mat_type, mat_type, resources.organic.silk.mat_index, mat_subtype, + prices[df::entity_sell_category::ClothSilk]); price != -1) + return price; + } + if (mi.isValid() && mi.matches(yarn_cat)) { + if (int32_t price = get_price(resources.organic.wool.mat_type, mat_type, resources.organic.wool.mat_index, mat_subtype, + prices[df::entity_sell_category::ClothYarn]); price != -1) + return price; + } + } + break; + case df::item_type::PANTS: + if (int32_t price = get_price(resources.pants_type, mat_subtype, prices[df::entity_sell_category::Legwear]); price != -1) + return price; + break; + case df::item_type::BACKPACK: + if (int32_t price = get_price(resources.misc_mat.backpacks.mat_type, mat_type, resources.misc_mat.backpacks.mat_index, mat_subtype, + prices[df::entity_sell_category::Backpacks]); price != -1) + return price; + break; + case df::item_type::QUIVER: + if (int32_t price = get_price(resources.misc_mat.quivers.mat_type, mat_type, resources.misc_mat.quivers.mat_index, mat_subtype, + prices[df::entity_sell_category::Quivers]); price != -1) + return price; + break; + case df::item_type::TRAPCOMP: + if (int32_t price = get_price(resources.trapcomp_type, mat_subtype, prices[df::entity_sell_category::TrapComponents]); price != -1) + return price; + break; + case df::item_type::DRINK: + if (int32_t price = get_price(resources.misc_mat.booze.mat_type, mat_type, resources.misc_mat.booze.mat_index, mat_subtype, + prices[df::entity_sell_category::Drinks]); price != -1) + return price; + break; + case df::item_type::POWDER_MISC: + if (int32_t price = get_price(resources.misc_mat.powders.mat_type, mat_type, resources.misc_mat.powders.mat_index, mat_subtype, + prices[df::entity_sell_category::Powders]); price != -1) + return price; + if (int32_t price = get_price(resources.misc_mat.sand.mat_type, mat_type, resources.misc_mat.sand.mat_index, mat_subtype, + prices[df::entity_sell_category::Sand]); price != -1) + return price; + break; + case df::item_type::CHEESE: + if (int32_t price = get_price(resources.misc_mat.cheese.mat_type, mat_type, resources.misc_mat.cheese.mat_index, mat_subtype, + prices[df::entity_sell_category::Cheese]); price != -1) + return price; + break; + case df::item_type::LIQUID_MISC: + if (int32_t price = get_price(resources.misc_mat.extracts.mat_type, mat_type, resources.misc_mat.extracts.mat_index, mat_subtype, + prices[df::entity_sell_category::Extracts]); price != -1) + return price; + break; + case df::item_type::SPLINT: + if (int32_t price = get_price(resources.misc_mat.barrels.mat_type, mat_type, resources.misc_mat.barrels.mat_index, mat_subtype, + prices[df::entity_sell_category::Splints]); price != -1) + return price; + break; + case df::item_type::CRUTCH: + if (int32_t price = get_price(resources.misc_mat.barrels.mat_type, mat_type, resources.misc_mat.barrels.mat_index, mat_subtype, + prices[df::entity_sell_category::Crutches]); price != -1) + return price; + break; + case df::item_type::TOOL: + if (int32_t price = get_price(resources.tool_type, mat_subtype, prices[df::entity_sell_category::Tools]); price != -1) + return price; + break; + case df::item_type::EGG: + if (int32_t price = get_price(resources.egg_races, mat_type, resources.egg_castes, mat_subtype, + prices[df::entity_sell_category::Eggs]); price != -1) + return price; + break; + case df::item_type::SHEET: + if (int32_t price = get_price(resources.organic.parchment.mat_type, mat_type, resources.organic.parchment.mat_index, mat_subtype, + prices[df::entity_sell_category::Parchment]); price != -1) + return price; + break; + default: + break; + } + + for (size_t idx = 0; idx < resources.wood_products.item_type.size(); ++idx) { + if (resources.wood_products.item_type[idx] == item_type && + (resources.wood_products.item_subtype[idx] == -1 || resources.wood_products.item_subtype[idx] == item_subtype) && + resources.wood_products.material.mat_type[idx] == mat_type && + (resources.wood_products.material.mat_index[idx] == -1 || resources.wood_products.material.mat_index[idx] == mat_subtype) && + prices[df::entity_sell_category::Miscellaneous].size() > idx) + return prices[df::entity_sell_category::Miscellaneous][idx]; + } + + return DEFAULT_AGREEMENT_MULTIPLIER; +} + +static int32_t get_sell_request_multiplier(df::item *item, const df::caravan_state *caravan) { + const df::entity_sell_prices *sell_prices = caravan->sell_prices; + if (!sell_prices) + return DEFAULT_AGREEMENT_MULTIPLIER; + + auto caravan_he = df::historical_entity::find(caravan->entity); + if (!caravan_he) + return DEFAULT_AGREEMENT_MULTIPLIER; + + return get_sell_request_multiplier(item, caravan_he->resources, &sell_prices->price[0]); +} + +static int32_t get_sell_request_multiplier(df::unit *unit, const df::caravan_state *caravan) { + const df::entity_sell_prices *sell_prices = caravan->sell_prices; + if (!sell_prices) + return DEFAULT_AGREEMENT_MULTIPLIER; + + auto caravan_he = df::historical_entity::find(caravan->entity); + if (!caravan_he) + return DEFAULT_AGREEMENT_MULTIPLIER; + + auto & resources = caravan_he->resources; + int32_t price = get_price(resources.animals.pet_races, unit->race, resources.animals.pet_castes, unit->caste, + sell_prices->price[df::entity_sell_category::Pets]); + return (price != -1) ? price : DEFAULT_AGREEMENT_MULTIPLIER; +} + +static bool is_requested_trade_good(df::item *item, df::caravan_state *caravan) { + auto trade_state = caravan->trade_state; + if (caravan->time_remaining <= 0 || + (trade_state != df::caravan_state::T_trade_state::Approaching && + trade_state != df::caravan_state::T_trade_state::AtDepot)) + return false; + return get_buy_request_multiplier(item, caravan->buy_prices) > DEFAULT_AGREEMENT_MULTIPLIER; +} + +bool Items::isRequestedTradeGood(df::item *item, df::caravan_state *caravan) { + if (caravan) + return is_requested_trade_good(item, caravan); + + for (auto caravan : df::global::plotinfo->caravans) { + auto trade_state = caravan->trade_state; + if (caravan->time_remaining <= 0 || + (trade_state != df::caravan_state::T_trade_state::Approaching && + trade_state != df::caravan_state::T_trade_state::AtDepot)) + continue; + if (get_buy_request_multiplier(item, caravan->buy_prices) > DEFAULT_AGREEMENT_MULTIPLIER) + return true; + } + return false; +} + +int Items::getValue(df::item *item, df::caravan_state *caravan) { CHECK_NULL_POINTER(item); @@ -1445,16 +1963,38 @@ int Items::getValue(df::item *item) // Get base value for item type, subtype, and material int value = getItemBaseValue(item_type, item_subtype, mat_type, mat_subtype); - // Ignore entity value modifications + // entity value modifications + value *= get_war_multiplier(item, caravan); + value >>= 8; // Improve value based on quality - int quality = item->getQuality(); - value *= (quality + 1); - if (quality == 5) + switch (item->getQuality()) { + case 1: + value *= 1.1; + value += 3; + break; + case 2: + value *= 1.2; + value += 6; + break; + case 3: + value *= 1.333; + value += 10; + break; + case 4: + value *= 1.5; + value += 15; + break; + case 5: value *= 2; + value += 30; + break; + default: + break; + } // Add improvement values - int impValue = item->getThreadDyeValue(NULL) + item->getImprovementsValue(NULL); + int impValue = item->getThreadDyeValue(caravan) + item->getImprovementsValue(caravan); if (item_type == item_type::AMMO) // Ammo improvements are worth less impValue /= 30; value += impValue; @@ -1479,12 +2019,20 @@ int Items::getValue(df::item *item) if (item->flags.bits.artifact_mood) value *= 10; + // modify buy/sell prices + if (caravan) { + value *= get_buy_request_multiplier(item, caravan->buy_prices); + value >>= 7; + value *= get_sell_request_multiplier(item, caravan); + value >>= 7; + } + // Boost value from stack size value *= item->getStackSize(); // ...but not for coins if (item_type == item_type::COIN) { - value /= 500; + value /= 50; if (!value) value = 1; } @@ -1494,11 +2042,39 @@ int Items::getValue(df::item *item) { int divisor = 1; auto creature = vector_get(world->raws.creatures.all, mat_type); - if (creature && size_t(mat_subtype) < creature->caste.size()) - divisor = creature->caste[mat_subtype]->misc.petvalue_divisor; - if (divisor > 1) + if (creature) { + size_t caste = std::max(0, mat_subtype); + if (caste < creature->caste.size()) + divisor = creature->caste[caste]->misc.petvalue_divisor; + } + if (divisor > 1) { value /= divisor; + if (!value) + value = 1; + } } + + // Add in value from units contained in cages + if (item_type == item_type::CAGE) { + for (auto gref : item->general_refs) { + if (gref->getType() != df::general_ref_type::CONTAINS_UNIT) + continue; + auto unit = gref->getUnit(); + if (!unit) + continue; + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + int unit_value = caste->misc.petvalue; + if (Units::isWar(unit) || Units::isHunter(unit)) + unit_value *= 2; + if (caravan) { + unit_value *= get_sell_request_multiplier(unit, caravan); + unit_value >>= 7; + } + value += unit_value; + } + } + return value; } @@ -1637,6 +2213,63 @@ bool Items::canTradeWithContents(df::item *item) return true; } +bool Items::canTradeAnyWithContents(df::item *item) +{ + CHECK_NULL_POINTER(item); + + if (item->flags.bits.in_inventory) + return false; + + vector contained_items; + getContainedItems(item, &contained_items); + + if (contained_items.empty()) + return canTrade(item); + + for (df::item *cit : contained_items) { + if (canTrade(cit)) + return true; + } + + return false; +} + +bool Items::markForTrade(df::item *item, df::building_tradedepotst *depot) { + CHECK_NULL_POINTER(item); + CHECK_NULL_POINTER(depot); + + // validate that the depot is in a good state + if (depot->getBuildStage() < depot->getMaxBuildStage()) + return false; + if (depot->jobs.size() && depot->jobs[0]->job_type == df::job_type::DestroyBuilding) + return false; + + auto href = df::allocate(); + if (!href) + return false; + + auto job = new df::job(); + job->job_type = df::job_type::BringItemToDepot; + job->pos = df::coord(depot->centerx, depot->centery, depot->z); + + // job <-> item link + if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + delete job; + delete href; + return false; + } + + // job <-> building link + href->building_id = depot->id; + depot->jobs.push_back(job); + job->general_refs.push_back(href); + + // add to job list + Job::linkIntoWorld(job); + + return true; +} + bool Items::isRouteVehicle(df::item *item) { CHECK_NULL_POINTER(item); @@ -1656,3 +2289,45 @@ bool Items::isSquadEquipment(df::item *item) auto &vec = plotinfo->equipment.items_assigned[item->getType()]; return binsearch_index(vec, item->id) >= 0; } + +// reverse engineered, code reference: 0x140953150 in 50.11-win64-steam +// our name for this function: itemst::getCapacity +// bay12 name for this function: not known + +int32_t Items::getCapacity(df::item* item) +{ + CHECK_NULL_POINTER(item); + + switch (item->getType()) { + case df::enums::item_type::FLASK: + case df::enums::item_type::GOBLET: + return 180; + case df::enums::item_type::CAGE: + case df::enums::item_type::BARREL: + case df::enums::item_type::COFFIN: + case df::enums::item_type::BOX: + case df::enums::item_type::BAG: + case df::enums::item_type::BIN: + case df::enums::item_type::ARMORSTAND: + case df::enums::item_type::WEAPONRACK: + case df::enums::item_type::CABINET: + return 6000; + case df::enums::item_type::BUCKET: + return 600; + case df::enums::item_type::ANIMALTRAP: + case df::enums::item_type::BACKPACK: + return 3000; + case df::enums::item_type::QUIVER: + return 1200; + case df::enums::item_type::TOOL: + { + auto tool = virtual_cast(item); + if (tool) + return tool->subtype->container_capacity; + } + // fall through + default: + ; // fall through to default exit + } + return 0; +} diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 1976d2e12..87301fef2 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -387,7 +387,14 @@ bool DFHack::Job::removeJob(df::job* job) { // call the job cancel vmethod graciously provided by The Toady One. // job_handler::cancel_job calls job::~job, and then deletes job (this has // been confirmed by disassembly). - world->jobs.cancel_job(job); + + // HACK: GCC (starting around GCC 10 targeting C++20 as of v50.09) optimizes + // out the vmethod call here regardless of optimization level, so we need to + // invoke the vmethod manually through a pointer, as the Lua wrapper does. + // `volatile` does not seem to be necessary but is included for good + // measure. + volatile auto cancel_job_method = &df::job_handler::cancel_job; + (world->jobs.*cancel_job_method)(job); return true; } @@ -560,7 +567,7 @@ bool DFHack::Job::attachJobItem(df::job *job, df::item *item, return true; } -bool Job::isSuitableItem(df::job_item *item, df::item_type itype, int isubtype) +bool Job::isSuitableItem(const df::job_item *item, df::item_type itype, int isubtype) { CHECK_NULL_POINTER(item); @@ -574,7 +581,7 @@ bool Job::isSuitableItem(df::job_item *item, df::item_type itype, int isubtype) } bool Job::isSuitableMaterial( - df::job_item *item, int mat_type, int mat_index, df::item_type itype) + const df::job_item *item, int mat_type, int mat_index, df::item_type itype) { CHECK_NULL_POINTER(item); diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 2e831075d..a05474fa5 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -622,16 +622,15 @@ bool Maps::ReadGeology(vector > *layer_mats, vector return true; } +uint16_t Maps::getWalkableGroup(df::coord pos) { + auto block = getTileBlock(pos); + return block ? index_tile(block->walkable, pos) : 0; +} + bool Maps::canWalkBetween(df::coord pos1, df::coord pos2) { - auto block1 = getTileBlock(pos1); - auto block2 = getTileBlock(pos2); - - if (!block1 || !block2) - return false; - - auto tile1 = index_tile(block1->walkable, pos1); - auto tile2 = index_tile(block2->walkable, pos2); + auto tile1 = getWalkableGroup(pos1); + auto tile2 = getWalkableGroup(pos2); return tile1 && tile1 == tile2; } diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 7a1ef249f..1c2d736da 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -517,11 +517,13 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma 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.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.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))); @@ -604,7 +606,7 @@ bool DFHack::isStoneInorganic(int material) std::unique_ptr DFHack::createMaterials() { - return dts::make_unique(); + return std::make_unique(); } Materials::Materials() diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index 6ccf246aa..dee7e2b75 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -78,7 +78,6 @@ namespace DFHack { DBG_DECLARE(core, screen, DebugCategory::LINFO); } - /* * Screen painting API. */ @@ -106,9 +105,11 @@ df::coord2d Screen::getWindowSize() return df::coord2d(gps->dimx, gps->dimy); } +/* void Screen::zoom(df::zoom_commands cmd) { enabler->zoom_display(cmd); } +*/ bool Screen::inGraphicsMode() { @@ -293,6 +294,7 @@ static Pen doGetTile_default(int x, int y, bool map) { return Pen(0, 0, 0, -1); long *texpos = &gps->screentexpos[index]; + long *texpos_lower = &gps->screentexpos_lower[index]; uint32_t *flag = &gps->screentexpos_flag[index]; if (gps->top_in_use && @@ -300,6 +302,7 @@ static Pen doGetTile_default(int x, int y, bool map) { (use_graphics && gps->screentexpos_top[index]))) { screen = &gps->screen_top[index * 8]; texpos = &gps->screentexpos_top[index]; + texpos_lower = &gps->screentexpos_top_lower[index]; flag = &gps->screentexpos_top_flag[index]; } @@ -307,19 +310,37 @@ static Pen doGetTile_default(int x, int y, bool map) { uint8_t fg = to_16_bit_color(&screen[1]); uint8_t bg = to_16_bit_color(&screen[4]); int tile = 0; - if (use_graphics) + bool write_to_lower = false; + bool top_of_text = false; + bool bottom_of_text = false; + if (use_graphics) { tile = *texpos; + if (!tile && *texpos_lower) { + tile = *texpos_lower; + write_to_lower = true; + } + if (*flag & 0x8) + top_of_text = true; + else if (*flag &0x10) + bottom_of_text = true; + } + Pen ret; if (*flag & 1) { // TileColor - return Pen(ch, fg&7, bg, !!(fg&8), tile, fg, bg); + ret = Pen(ch, fg&7, bg, !!(fg&8), tile, fg, bg); } else if (*flag & 2) { // CharColor - return Pen(ch, fg, bg, tile, true); + ret = Pen(ch, fg, bg, tile, true); + } else { + // AsIs + ret = Pen(ch, fg, bg, tile, false); } - // AsIs - return Pen(ch, fg, bg, tile, false); + ret.write_to_lower = write_to_lower; + ret.top_of_text = top_of_text; + ret.bottom_of_text = bottom_of_text; + return ret; } GUI_HOOK_DEFINE(Screen::Hooks::get_tile, doGetTile_default); @@ -584,12 +605,27 @@ void Hide::merge() { } } } +std::set Screen::normalize_text_keys(const std::set& keys) { + std::set combined_keys; + std::copy_if(keys.begin(), keys.end(), std::inserter(combined_keys, combined_keys.begin()), + [](df::interface_key k){ return k <= df::interface_key::STRING_A000 || k > df::interface_key::STRING_A255; } ); + if (!(Core::getInstance().getModstate() & (DFH_MOD_CTRL | DFH_MOD_ALT)) && df::global::enabler->last_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); + combined_keys.emplace(key); + } + return combined_keys; +} + string Screen::getKeyDisplay(df::interface_key key) { - if (enabler) - return enabler->GetKeyDisplay(key); - - return "?"; + int c = keyToChar(key); + if (c != -1) + return string(1, c); + if (key >= df::interface_key::CUSTOM_SHIFT_A && key <= df::interface_key::CUSTOM_SHIFT_Z) + return string(1, 'A' + (key - df::interface_key::CUSTOM_SHIFT_A)); + return enabler->GetKeyDisplay(key); } int Screen::keyToChar(df::interface_key key) @@ -920,12 +956,25 @@ int dfhack_lua_viewscreen::do_notify(lua_State *L) return 1; } +void dfhack_lua_viewscreen::markInputAsHandled() { + if (!enabler) + return; + + // clear text buffer + enabler->last_text_input[0] = '\0'; + + // mark clicked mouse buttons as handled + enabler->mouse_lbut = 0; + enabler->mouse_rbut = 0; +} + int dfhack_lua_viewscreen::do_input(lua_State *L) { auto self = get_self(L); if (!self) return 0; auto keys = (std::set*)lua_touserdata(L, 2); + if (!keys) return 0; lua_getfield(L, -1, "onInput"); @@ -938,9 +987,13 @@ int dfhack_lua_viewscreen::do_input(lua_State *L) } lua_pushvalue(L, -2); - Lua::PushInterfaceKeys(L, *keys); + Lua::PushInterfaceKeys(L, Screen::normalize_text_keys(*keys)); + + lua_call(L, 2, 1); + if (lua_toboolean(L, -1)) + markInputAsHandled(); + lua_pop(L, 1); - lua_call(L, 2, 0); self->update_focus(L, -1); return 0; } @@ -967,6 +1020,8 @@ dfhack_lua_viewscreen::~dfhack_lua_viewscreen() void dfhack_lua_viewscreen::render() { + using df::global::enabler; + if (Screen::isDismissed(this)) { if (parent) @@ -974,6 +1029,14 @@ void dfhack_lua_viewscreen::render() return; } + if (enabler && + (enabler->mouse_lbut_down || enabler->mouse_rbut_down || enabler->mouse_mbut_down)) + { + // synthesize feed events for held mouse buttons + std::set keys; + feed(&keys); + } + dfhack_viewscreen::render(); safe_call_lua(do_render, 0, 0); @@ -1021,6 +1084,7 @@ void dfhack_lua_viewscreen::feed(std::set *keys) lua_pushlightuserdata(Lua::Core::State, keys); safe_call_lua(do_input, 1, 0); + df::global::enabler->last_text_input[0] = '\0'; } void dfhack_lua_viewscreen::onShow() @@ -1067,6 +1131,22 @@ df::building *dfhack_lua_viewscreen::getSelectedBuilding() return Lua::GetDFObject(Lua::Core::State, -1); } +df::building_stockpilest *dfhack_lua_viewscreen::getSelectedStockpile() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedStockpile"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} + +df::building_civzonest *dfhack_lua_viewscreen::getSelectedCivZone() +{ + Lua::StackUnwinder frame(Lua::Core::State); + lua_pushstring(Lua::Core::State, "onGetSelectedCivZone"); + safe_call_lua(do_notify, 1, 1); + return Lua::GetDFObject(Lua::Core::State, -1); +} + df::plant *dfhack_lua_viewscreen::getSelectedPlant() { Lua::StackUnwinder frame(Lua::Core::State); @@ -1075,8 +1155,7 @@ df::plant *dfhack_lua_viewscreen::getSelectedPlant() return Lua::GetDFObject(Lua::Core::State, -1); } -#define STATIC_FIELDS_GROUP -#include "../DataStaticsFields.cpp" +#include "DataStaticsFields.inc" using df::identity_traits; diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp index a30f879a0..8b485a3a6 100644 --- a/library/modules/Textures.cpp +++ b/library/modules/Textures.cpp @@ -1,3 +1,9 @@ +#include +#include +#include +#include +#include + #include "Internal.h" #include "modules/DFSDL.h" @@ -5,30 +11,54 @@ #include "Debug.h" #include "PluginManager.h" +#include "VTableInterpose.h" #include "df/enabler.h" +#include "df/viewscreen_adopt_regionst.h" +#include "df/viewscreen_loadgamest.h" +#include "df/viewscreen_new_arenast.h" +#include "df/viewscreen_new_regionst.h" + +#include +#include using df::global::enabler; using namespace DFHack; using namespace DFHack::DFSDL; namespace DFHack { - DBG_DECLARE(core, textures, DebugCategory::LINFO); -} - -static bool g_loaded = false; -static long g_num_dfhack_textures = 0; -static long g_dfhack_logo_texpos_start = -1; -static long g_green_pin_texpos_start = -1; -static long g_red_pin_texpos_start = -1; -static long g_icons_texpos_start = -1; -static long g_on_off_texpos_start = -1; -static long g_control_panel_texpos_start = -1; -static long g_thin_borders_texpos_start = -1; -static long g_medium_borders_texpos_start = -1; -static long g_bold_borders_texpos_start = -1; -static long g_panel_borders_texpos_start = -1; -static long g_window_borders_texpos_start = -1; +DBG_DECLARE(core, textures, DebugCategory::LINFO); +} + +struct ReservedRange { + void init(int32_t start) { + this->start = start; + this->end = start + ReservedRange::size; + this->current = start; + this->is_installed = true; + } + long get_new_texpos() { + if (this->current == this->end) + return -1; + return this->current++; + } + + static const int32_t size = 10000; // size of reserved texpos buffer + int32_t start = -1; + int32_t end = -1; + long current = -1; + bool is_installed = false; +}; + +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::vector g_delayed_regs; +static std::mutex g_adding_mutex; +static std::atomic loading_state = false; +static SDL_Surface* dummy_surface = NULL; // Converts an arbitrary Surface to something like the display format // (32-bit RGBA), and converts magenta to transparency if convert_magenta is set @@ -37,182 +67,396 @@ static long g_window_borders_texpos_start = -1; // // It uses the same pixel format (RGBA, R at lowest address) regardless of // hardware. -DFSDL_Surface * canonicalize_format(DFSDL_Surface *src) { - DFSDL_PixelFormat fmt; - fmt.palette = NULL; - fmt.BitsPerPixel = 32; - fmt.BytesPerPixel = 4; - fmt.Rloss = fmt.Gloss = fmt.Bloss = fmt.Aloss = 0; -//#if SDL_BYTEORDER == SDL_BIG_ENDIAN -// fmt.Rshift = 24; fmt.Gshift = 16; fmt.Bshift = 8; fmt.Ashift = 0; -//#else - fmt.Rshift = 0; fmt.Gshift = 8; fmt.Bshift = 16; fmt.Ashift = 24; -//#endif - fmt.Rmask = 255 << fmt.Rshift; - fmt.Gmask = 255 << fmt.Gshift; - fmt.Bmask = 255 << fmt.Bshift; - fmt.Amask = 255 << fmt.Ashift; - fmt.colorkey = 0; - fmt.alpha = 255; - - DFSDL_Surface *tgt = DFSDL_ConvertSurface(src, &fmt, 0); // SDL_SWSURFACE - DFSDL_FreeSurface(src); - return tgt; -} - -const uint32_t TILE_WIDTH_PX = 8; -const uint32_t TILE_HEIGHT_PX = 12; - -static size_t load_textures(color_ostream & out, const char * fname, - long *texpos_start) { - DFSDL_Surface *s = DFIMG_Load(fname); - if (!s) { - out.printerr("unable to load textures from '%s'\n", fname); - return 0; +SDL_Surface* canonicalize_format(SDL_Surface* src) { + // even though we have null check after DFIMG_Load + // in loadTileset() (the only consumer of this method) + // it's better put nullcheck here as well + if (!src) + return src; + + auto fmt = DFSDL_AllocFormat(SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32); + SDL_Surface* tgt = DFSDL_ConvertSurface(src, fmt, SDL_SWSURFACE); + DFSDL_FreeSurface(src); + for (int x = 0; x < tgt->w; ++x) { + for (int y = 0; y < tgt->h; ++y) { + Uint8* p = (Uint8*)tgt->pixels + y * tgt->pitch + x * 4; + if (p[3] == 0) { + for (int c = 0; c < 3; c++) { + p[c] = 0; + } + } + } + } + + return tgt; +} + +// register surface in texture raws, get a texpos +static long add_texture(SDL_Surface* surface) { + std::lock_guard lg_add_texture(g_adding_mutex); + auto texpos = enabler->textures.raws.size(); + enabler->textures.raws.push_back(surface); + return texpos; +} + +// register surface in texture raws to specific texpos +static void insert_texture(SDL_Surface* surface, long texpos) { + std::lock_guard lg_add_texture(g_adding_mutex); + enabler->textures.raws[texpos] = surface; +} + +// delete surface from texture raws +static void delete_texture(long texpos) { + std::lock_guard lg_add_texture(g_adding_mutex); + auto pos = static_cast(texpos); + if (pos >= enabler->textures.raws.size()) + return; + enabler->textures.raws[texpos] = NULL; +} + +// create new surface with RGBA32 format and pixels as data +SDL_Surface* create_texture(std::vector& pixels, int texture_px_w, int texture_px_h) { + auto surface = DFSDL_CreateRGBSurfaceWithFormat(0, texture_px_w, texture_px_h, 32, + SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32); + auto canvas_length = static_cast(texture_px_w * texture_px_h); + for (size_t i = 0; i < pixels.size() && i < canvas_length; i++) { + uint32_t* p = (uint32_t*)surface->pixels + i; + *p = pixels[i]; + } + return surface; +} + +// convert single surface into tiles according w/h +// register tiles in texture raws and return handles +std::vector slice_tileset(SDL_Surface* surface, int tile_px_w, int tile_px_h, + bool reserved) { + std::vector handles{}; + if (!surface) + return handles; + + int dimx = surface->w / tile_px_w; + int dimy = surface->h / tile_px_h; + + if (reserved && (dimx * dimy > reserved_range.end - reserved_range.current)) { + WARN(textures).print( + "there is not enough space in reserved range for whole tileset, using dynamic range\n"); + reserved = false; } - s = canonicalize_format(s); - DFSDL_SetAlpha(s, 0, 255); - int dimx = s->w / TILE_WIDTH_PX; - int dimy = s->h / TILE_HEIGHT_PX; - long count = 0; for (int y = 0; y < dimy; y++) { for (int x = 0; x < dimx; x++) { - DFSDL_Surface *tile = DFSDL_CreateRGBSurface(0, // SDL_SWSURFACE - TILE_WIDTH_PX, TILE_HEIGHT_PX, 32, - s->format->Rmask, s->format->Gmask, s->format->Bmask, - s->format->Amask); - DFSDL_SetAlpha(tile, 0,255); - DFSDL_Rect vp; - vp.x = TILE_WIDTH_PX * x; - vp.y = TILE_HEIGHT_PX * y; - vp.w = TILE_WIDTH_PX; - vp.h = TILE_HEIGHT_PX; - DFSDL_UpperBlit(s, &vp, tile, NULL); - if (!count++) - *texpos_start = enabler->textures.raws.size(); - enabler->textures.raws.push_back(tile); + SDL_Surface* tile = DFSDL_CreateRGBSurface( + 0, tile_px_w, tile_px_h, 32, surface->format->Rmask, surface->format->Gmask, + surface->format->Bmask, surface->format->Amask); + SDL_Rect vp{tile_px_w * x, tile_px_h * y, tile_px_w, tile_px_h}; + DFSDL_UpperBlit(surface, &vp, tile, NULL); + auto handle = Textures::loadTexture(tile, reserved); + handles.push_back(handle); } } - DFSDL_FreeSurface(s); - DEBUG(textures,out).print("loaded %ld textures from '%s'\n", count, fname); - return count; + DFSDL_FreeSurface(surface); + return handles; } -// DFHack could conceivably be loaded at any time, so we need to be able to -// handle loading textures before or after a world is loaded. -// If a world is already loaded, then append our textures to the raws. they'll -// be freed when the world is unloaded and we'll reload when we get to the title -// screen. If it's pre-world, append our textures and then adjust the "init" -// texture count so our textures will no longer be freed when worlds are -// unloaded. -// -void Textures::init(color_ostream &out) { - auto & textures = enabler->textures; - long num_textures = textures.raws.size(); - if (num_textures <= g_dfhack_logo_texpos_start) - g_loaded = false; +TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) { + if (!surface || !enabler) + return 0; // should be some error, i guess + if (loading_state) + reserved = true; // use reserved range during loading for all textures + + auto handle = reinterpret_cast(surface); + g_handle_to_surface.emplace(handle, surface); + surface->refcount++; // prevent destruct on next FreeSurface by game + + if (reserved && reserved_range.is_installed) { + auto texpos = reserved_range.get_new_texpos(); + if (texpos != -1) { + insert_texture(surface, texpos); + g_handle_to_reserved_texpos.emplace(handle, texpos); + dummy_surface->refcount--; + return handle; + } - if (g_loaded) - return; + if (loading_state) { // if we in loading state and reserved range is full -> error + ERR(textures).printerr("reserved range limit has been reached, use dynamic range\n"); + return 0; + } + } - bool is_pre_world = num_textures == textures.init_texture_size; - - g_num_dfhack_textures = load_textures(out, "hack/data/art/dfhack.png", - &g_dfhack_logo_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/green-pin.png", - &g_green_pin_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/red-pin.png", - &g_red_pin_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/icons.png", - &g_icons_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/on-off.png", - &g_on_off_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/control-panel.png", - &g_control_panel_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/border-thin.png", - &g_thin_borders_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/border-medium.png", - &g_medium_borders_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/border-bold.png", - &g_bold_borders_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/border-panel.png", - &g_panel_borders_texpos_start); - g_num_dfhack_textures += load_textures(out, "hack/data/art/border-window.png", - &g_window_borders_texpos_start); - - DEBUG(textures,out).print("loaded %ld textures\n", g_num_dfhack_textures); - - if (is_pre_world) - textures.init_texture_size += g_num_dfhack_textures; - - // NOTE: when GL modes are supported, we'll have to re-upload textures here - - g_loaded = true; -} - -// It's ok to leave NULLs in the raws list (according to usage in g_src) -void Textures::cleanup() { - if (!g_loaded) - return; + // if we here in loading state = true, then it should be dynamic range -> delay reg + if (loading_state) { + g_delayed_regs.push_back(handle); + } else { + auto texpos = add_texture(surface); + g_handle_to_texpos.emplace(handle, texpos); + } + + return handle; +} + +std::vector Textures::loadTileset(const std::string& 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()); + if (!surface) { + ERR(textures).printerr("unable to load textures from '%s'\n", file.c_str()); + 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()); + g_tileset_to_handles[file] = handles; - auto & textures = enabler->textures; - auto &raws = textures.raws; - size_t texpos_end = g_dfhack_logo_texpos_start + g_num_dfhack_textures; - for (size_t idx = g_dfhack_logo_texpos_start; idx <= texpos_end; ++idx) { - DFSDL_FreeSurface((DFSDL_Surface *)raws[idx]); - raws[idx] = NULL; + return handles; +} + +long Textures::getTexposByHandle(TexposHandle handle) { + if (!handle || !enabler) + return -1; + + if (g_handle_to_reserved_texpos.contains(handle)) + return g_handle_to_reserved_texpos[handle]; + if (g_handle_to_texpos.contains(handle)) + return g_handle_to_texpos[handle]; + if (std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle) != g_delayed_regs.end()) + return 0; + if (g_handle_to_surface.contains(handle)) { + g_handle_to_surface[handle]->refcount++; // prevent destruct on next FreeSurface by game + if (loading_state) { // reinit dor dynamic range during loading -> delayed + g_delayed_regs.push_back(handle); + return 0; + } + auto texpos = add_texture(g_handle_to_surface[handle]); + g_handle_to_texpos.emplace(handle, texpos); + return texpos; } - if (g_dfhack_logo_texpos_start == textures.init_texture_size - g_num_dfhack_textures) - textures.init_texture_size -= g_num_dfhack_textures; + return -1; +} + +TexposHandle Textures::createTile(std::vector& pixels, int tile_px_w, int tile_px_h, + bool reserved) { + if (!enabler) + return 0; - g_loaded = false; - g_num_dfhack_textures = 0; - g_dfhack_logo_texpos_start = -1; + auto texture = create_texture(pixels, tile_px_w, tile_px_h); + auto handle = Textures::loadTexture(texture, reserved); + return handle; } -long Textures::getDfhackLogoTexposStart() { - return g_dfhack_logo_texpos_start; +std::vector Textures::createTileset(std::vector& pixels, int texture_px_w, + int texture_px_h, int tile_px_w, int tile_px_h, + bool reserved) { + if (!enabler) + return std::vector{}; + + auto texture = create_texture(pixels, texture_px_w, texture_px_h); + auto handles = slice_tileset(texture, tile_px_w, tile_px_h, reserved); + return handles; } -long Textures::getGreenPinTexposStart() { - return g_green_pin_texpos_start; +void Textures::deleteHandle(TexposHandle handle) { + if (!enabler) + return; + + auto texpos = Textures::getTexposByHandle(handle); + if (texpos > 0) + delete_texture(texpos); + if (g_handle_to_reserved_texpos.contains(handle)) + g_handle_to_reserved_texpos.erase(handle); + if (g_handle_to_texpos.contains(handle)) + g_handle_to_texpos.erase(handle); + if (auto it = std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle); + it != g_delayed_regs.end()) + g_delayed_regs.erase(it); + if (g_handle_to_surface.contains(handle)) { + auto surface = g_handle_to_surface[handle]; + while (surface->refcount) + DFSDL_FreeSurface(surface); + g_handle_to_surface.erase(handle); + } } -long Textures::getRedPinTexposStart() { - return g_red_pin_texpos_start; +static void reset_texpos() { + DEBUG(textures).print("resetting texture mappings\n"); + g_handle_to_texpos.clear(); } -long Textures::getIconsTexposStart() { - return g_icons_texpos_start; +static void reset_reserved_texpos() { + DEBUG(textures).print("resetting reserved texture mappings\n"); + g_handle_to_reserved_texpos.clear(); } -long Textures::getOnOffTexposStart() { - return g_on_off_texpos_start; +static void reset_tilesets() { + DEBUG(textures).print("resetting tileset to handle mappings\n"); + g_tileset_to_handles.clear(); } -long Textures::getControlPanelTexposStart() { - return g_control_panel_texpos_start; +static void reset_surface() { + DEBUG(textures).print("deleting cached surfaces\n"); + for (auto& entry : g_handle_to_surface) { + DFSDL_FreeSurface(entry.second); + } + g_handle_to_surface.clear(); } -long Textures::getThinBordersTexposStart() { - return g_thin_borders_texpos_start; +static void register_delayed_handles() { + DEBUG(textures).print("register delayed handles, size %zd\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); + } + g_delayed_regs.clear(); } -long Textures::getMediumBordersTexposStart() { - return g_medium_borders_texpos_start; +// reset point on New Game +struct tracking_stage_new_region : df::viewscreen_new_regionst { + typedef df::viewscreen_new_regionst interpose_base; + + 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); + 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) + register_delayed_handles(); + this->m_raw_load_stage = this->raw_load_stage; + if (this->m_raw_load_stage == 1) + reset_texpos(); + } + INTERPOSE_NEXT(logic)(); + } + + private: + inline static int m_raw_load_stage = -2; // not valid state at the start +}; +IMPLEMENT_VMETHOD_INTERPOSE(tracking_stage_new_region, logic); + +// reset point on New Game in Existing World +struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst { + typedef df::viewscreen_adopt_regionst interpose_base; + + 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); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); + this->m_cur_step = this->cur_step; + if (this->m_cur_step == 1) + reset_texpos(); + } + INTERPOSE_NEXT(logic)(); + } + + private: + inline static int m_cur_step = -2; // not valid state at the start +}; +IMPLEMENT_VMETHOD_INTERPOSE(tracking_stage_adopt_region, logic); + +// reset point on Load Game +struct tracking_stage_load_region : df::viewscreen_loadgamest { + typedef df::viewscreen_loadgamest interpose_base; + + 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); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); + this->m_cur_step = this->cur_step; + if (this->m_cur_step == 1) + reset_texpos(); + } + INTERPOSE_NEXT(logic)(); + } + + private: + inline static int m_cur_step = -2; // not valid state at the start +}; +IMPLEMENT_VMETHOD_INTERPOSE(tracking_stage_load_region, logic); + +// reset point on New Arena +struct tracking_stage_new_arena : df::viewscreen_new_arenast { + typedef df::viewscreen_new_arenast interpose_base; + + 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); + bool tmp_state = loading_state; + loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false; + if (tmp_state != loading_state && !loading_state) + register_delayed_handles(); + this->m_cur_step = this->cur_step; + if (this->m_cur_step == 0) + reset_texpos(); + } + INTERPOSE_NEXT(logic)(); + } + + private: + inline static int m_cur_step = -2; // not valid state at the start +}; +IMPLEMENT_VMETHOD_INTERPOSE(tracking_stage_new_arena, logic); + +static void install_reset_point() { + INTERPOSE_HOOK(tracking_stage_new_region, logic).apply(); + INTERPOSE_HOOK(tracking_stage_adopt_region, logic).apply(); + INTERPOSE_HOOK(tracking_stage_load_region, logic).apply(); + INTERPOSE_HOOK(tracking_stage_new_arena, logic).apply(); +} + +static void uninstall_reset_point() { + INTERPOSE_HOOK(tracking_stage_new_region, logic).remove(); + INTERPOSE_HOOK(tracking_stage_adopt_region, logic).remove(); + INTERPOSE_HOOK(tracking_stage_load_region, logic).remove(); + INTERPOSE_HOOK(tracking_stage_new_arena, logic).remove(); } -long Textures::getBoldBordersTexposStart() { - return g_bold_borders_texpos_start; +static void reserve_static_range() { + if (static_cast(enabler->textures.init_texture_size) != enabler->textures.raws.size()) { + WARN(textures).print( + "reserved range can't be installed! all textures will be loaded to dynamic range!"); + return; + } + reserved_range.init(enabler->textures.init_texture_size); + dummy_surface = + DFSDL_CreateRGBSurfaceWithFormat(0, 0, 0, 32, SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32); + dummy_surface->refcount += ReservedRange::size; + for (int32_t i = 0; i < ReservedRange::size; i++) { + add_texture(dummy_surface); + } + enabler->textures.init_texture_size += ReservedRange::size; } -long Textures::getPanelBordersTexposStart() { - return g_panel_borders_texpos_start; +void Textures::init(color_ostream& out) { + if (!enabler) + return; + + reserve_static_range(); + install_reset_point(); + DEBUG(textures, out) + .print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start, + reserved_range.end); } -long Textures::getWindowBordersTexposStart() { - return g_window_borders_texpos_start; +void Textures::cleanup() { + if (!enabler) + return; + + reset_texpos(); + reset_reserved_texpos(); + reset_tilesets(); + reset_surface(); + uninstall_reset_point(); } diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index 282039c02..8ebae035f 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -27,7 +27,6 @@ distribution. #include #include #include -using namespace std; #include "modules/Translation.h" #include "VersionInfo.h" @@ -44,6 +43,8 @@ using namespace df::enums; #include "df/world.h" #include "df/d_init.h" +using std::vector, std::string; + using df::global::world; using df::global::d_init; using df::global::gametype; @@ -131,6 +132,37 @@ void Translation::setNickname(df::language_name *name, std::string nick) } } +static string translate_word(const df::language_name * name, size_t word_idx) { + CHECK_NULL_POINTER(name); + + auto translation = vector_get(world->raws.language.translations, name->language); + if (!translation) + return ""; + + auto word = vector_get(translation->words, word_idx); + if (!word) + return ""; + + return *word; +} + +static string translate_english_word(const df::language_name * name, size_t part_idx) { + CHECK_NULL_POINTER(name); + + if (part_idx >= 7) + return ""; + + auto words = vector_get(world->raws.language.words, name->words[part_idx]); + if (!words) + return ""; + + df::part_of_speech part = name->parts_of_speech[part_idx]; + if (part < df::part_of_speech::Noun || part > df::part_of_speech::VerbGerund) + return ""; + + return words->forms[part]; +} + string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart) { CHECK_NULL_POINTER(name); @@ -166,20 +198,20 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish { word.clear(); if (name->words[0] >= 0) - word.append(*world->raws.language.translations[name->language]->words[name->words[0]]); + word.append(translate_word(name, name->words[0])); if (name->words[1] >= 0) - word.append(*world->raws.language.translations[name->language]->words[name->words[1]]); + word.append(translate_word(name, name->words[1])); addNameWord(out, word); } word.clear(); for (int i = 2; i <= 5; i++) if (name->words[i] >= 0) - word.append(*world->raws.language.translations[name->language]->words[name->words[i]]); + word.append(translate_word(name, name->words[i])); addNameWord(out, word); if (name->words[6] >= 0) { word.clear(); - word.append(*world->raws.language.translations[name->language]->words[name->words[6]]); + word.append(translate_word(name, name->words[6])); addNameWord(out, word); } } @@ -189,9 +221,9 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish { word.clear(); if (name->words[0] >= 0) - word.append(world->raws.language.words[name->words[0]]->forms[name->parts_of_speech[0]]); + word.append(translate_english_word(name, 0)); if (name->words[1] >= 0) - word.append(world->raws.language.words[name->words[1]]->forms[name->parts_of_speech[1]]); + word.append(translate_english_word(name, 1)); addNameWord(out, word); } if (name->words[2] >= 0 || name->words[3] >= 0 || name->words[4] >= 0 || name->words[5] >= 0) @@ -201,10 +233,10 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish else out.append("The"); } - for (int i = 2; i <= 5; i++) + for (size_t i = 2; i <= 5; i++) { if (name->words[i] >= 0) - addNameWord(out, world->raws.language.words[name->words[i]]->forms[name->parts_of_speech[i]]); + addNameWord(out, translate_english_word(name, i)); } if (name->words[6] >= 0) { @@ -213,7 +245,7 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish else out.append("Of"); - addNameWord(out, world->raws.language.words[name->words[6]]->forms[name->parts_of_speech[6]]); + addNameWord(out, translate_english_word(name, 6)); } } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 533b40ca8..ff36481b7 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -53,12 +53,14 @@ using namespace std; #include "df/activity_entry.h" #include "df/burrow.h" #include "df/caste_raw.h" +#include "df/creature_interaction_effect_display_namest.h" #include "df/creature_raw.h" #include "df/curse_attr_change.h" #include "df/entity_position.h" #include "df/entity_position_assignment.h" #include "df/entity_raw.h" #include "df/entity_raw_flags.h" +#include "df/entity_site_link.h" #include "df/identity_type.h" #include "df/game_mode.h" #include "df/histfig_entity_link_positionst.h" @@ -73,13 +75,18 @@ using namespace std; #include "df/nemesis_record.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" +#include "df/syndrome.h" +#include "df/training_assignment.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_relationship_type.h" #include "df/unit_skill.h" #include "df/unit_soul.h" +#include "df/unit_syndrome.h" #include "df/unit_wound.h" #include "df/world.h" +#include "df/world_data.h" +#include "df/world_site.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" @@ -504,6 +511,38 @@ bool Units::isDomesticated(df::unit* unit) return tame; } +static df::training_assignment * get_training_assignment(df::unit* unit) { + return binsearch_in_vector(df::global::plotinfo->equipment.training_assignments, + &df::training_assignment::animal_id, unit->id); +} + +bool Units::isMarkedForTraining(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return !!get_training_assignment(unit); +} + +bool Units::isMarkedForTaming(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto assignment = get_training_assignment(unit); + return assignment && !assignment->flags.bits.train_war && !assignment->flags.bits.train_hunt; +} + +bool Units::isMarkedForWarTraining(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto assignment = get_training_assignment(unit); + return assignment && assignment->flags.bits.train_war; +} + +bool Units::isMarkedForHuntTraining(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto assignment = get_training_assignment(unit); + return assignment && assignment->flags.bits.train_hunt; +} + bool Units::isMarkedForSlaughter(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -552,6 +591,18 @@ bool Units::isEggLayer(df::unit* unit) || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); } +bool Units::isEggLayerRace(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + for (auto &caste : raw->caste) { + if (caste->flags.is_set(caste_raw_flags::LAYS_EGGS) + || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS)) + return true; + } + return false; +} + bool Units::isGrazer(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -770,9 +821,63 @@ bool Units::getUnitsInBox (std::vector &units, return true; } +static int32_t get_noble_position_id(const df::historical_entity::T_positions &positions, const string &noble) { + string target_id = toUpper(noble); + for (auto &position : positions.own) { + if (position->code == target_id) + return position->id; + } + return -1; +} + +static void add_assigned_noble_units(vector &units, const df::historical_entity::T_positions &positions, int32_t noble_position_id, size_t limit) { + for (auto &assignment : positions.assignments) { + if (assignment->position_id != noble_position_id) + continue; + auto histfig = df::historical_figure::find(assignment->histfig); + if (!histfig) + continue; + auto unit = df::unit::find(histfig->unit_id); + if (!unit) + continue; + units.emplace_back(unit); + if (limit > 0 && units.size() >= limit) + break; + } +} + +static void get_units_by_noble_role(vector &units, string noble, size_t limit = 0) { + auto &site = df::global::world->world_data->active_site[0]; + for (auto &link : site->entity_links) { + auto he = df::historical_entity::find(link->entity_id); + if (!he || + (he->type != df::historical_entity_type::SiteGovernment && + he->type != df::historical_entity_type::Civilization)) + continue; + int32_t noble_position_id = get_noble_position_id(he->positions, noble); + if (noble_position_id < 0) + continue; + add_assigned_noble_units(units, he->positions, noble_position_id, limit); + } +} + +bool Units::getUnitsByNobleRole(vector &units, std::string noble) { + units.clear(); + get_units_by_noble_role(units, noble); + return !units.empty(); +} + +df::unit *Units::getUnitByNobleRole(string noble) { + vector units; + get_units_by_noble_role(units, noble, 1); + if (units.empty()) + return NULL; + return units[0]; +} + bool Units::getCitizens(std::vector &citizens, bool ignore_sanity) { for (auto &unit : world->units.active) { - if (isCitizen(unit, ignore_sanity)) + if (isCitizen(unit, ignore_sanity) && isAlive(unit)) citizens.emplace_back(unit); } return true; @@ -1120,8 +1225,12 @@ string Units::getRaceBabyNameById(int32_t id) if (id >= 0 && (size_t)id < world->raws.creatures.all.size()) { df::creature_raw* raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_baby_name[0]; + if (raw) { + string & baby_name = raw->general_baby_name[0]; + if (!baby_name.empty()) + return baby_name; + return getRaceReadableNameById(id) + " baby"; + } } return ""; } @@ -1137,8 +1246,12 @@ string Units::getRaceChildNameById(int32_t id) if (id >= 0 && (size_t)id < world->raws.creatures.all.size()) { df::creature_raw* raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_child_name[0]; + if (raw) { + string & child_name = raw->general_child_name[0]; + if (!child_name.empty()) + return child_name; + return getRaceReadableNameById(id) + " child"; + } } return ""; } @@ -1149,6 +1262,48 @@ string Units::getRaceChildName(df::unit* unit) return getRaceChildNameById(unit->race); } +static string get_caste_name(df::unit* unit) { + int32_t id = unit->race; + if (id < 0 || (size_t)id >= world->raws.creatures.all.size()) + return ""; + df::creature_raw* raw = world->raws.creatures.all[id]; + int16_t caste = unit->caste; + if (!raw || caste < 0 || (size_t)caste >= raw->caste.size()) + return ""; + return raw->caste[caste]->caste_name[0]; +} + +string Units::getReadableName(df::unit* unit) { + string race_name = isBaby(unit) ? getRaceBabyName(unit) : + (isChild(unit) ? getRaceChildName(unit) : get_caste_name(unit)); + if (race_name.empty()) + race_name = getRaceReadableName(unit); + if (isHunter(unit)) + race_name = "hunter " + race_name; + if (isWar(unit)) + race_name = "war " + race_name; + string name = Translation::TranslateName(getVisibleName(unit), false); + if (name.empty()) { + name = race_name; + } else { + name += ", "; + name += race_name; + } + for (auto unit_syndrome : unit->syndromes.active) { + auto syndrome = df::syndrome::find(unit_syndrome->type); + if (!syndrome) + continue; + for (auto effect : syndrome->ce) { + auto cie = strict_virtual_cast(effect); + if (!cie) + continue; + name += " "; + name += cie->name; + break; + } + } + return name; +} double Units::getAge(df::unit *unit, bool true_age) { diff --git a/library/xml b/library/xml index 1809e3d70..6375044bc 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1809e3d708869bb16e0ac1745a61ac473ce745f6 +Subproject commit 6375044bc504cf5bf4579659755966bb30704b7f diff --git a/package/linux/dfhack b/package/linux/dfhack index 6b542f405..fab3d0602 100755 --- a/package/linux/dfhack +++ b/package/linux/dfhack @@ -1,26 +1,20 @@ #!/bin/sh -# NOTE: This is dfhack's modification of the normal invocation script, -# changed to properly set LD_PRELOAD so as to run DFHACK. -# # You can run DF under gdb by passing -g or --gdb as the first argument. # # If the file ".dfhackrc" exists in the DF directory or your home directory # it will be sourced by this script, to let you set environmental variables. # If it exists in both places it will first source the one in your home -# directory, then the on in the game directory. +# directory, then the one in the game directory. # # Shell variables .dfhackrc can set to affect this script: # DF_GDB_OPTS: Options to pass to gdb, if it's being run # DF_VALGRIND_OPTS: Options to pass to valgrind, if it's being run # DF_HELGRIND_OPTS: Options to pass to helgrind, if it's being run # DF_POST_CMD: Shell command to be run at very end of script -# DFHACK_NO_RENAME_LIBSTDCXX: Non-empty to prevent automatically renaming libstdc++ DF_DIR=$(dirname "$0") cd "${DF_DIR}" -export SDL_DISABLE_LOCK_KEYS=1 # Work around for bug in Debian/Ubuntu SDL patch. -#export SDL_VIDEO_CENTERED=1 # Centre the screen. Messes up resizing. # User config files RC=".dfhackrc" @@ -32,22 +26,6 @@ if [ -r "./$RC" ]; then . "./$RC" fi -# Disable bundled libstdc++ -libcxx_orig="libs/libstdc++.so.6" -libcxx_backup="libs/libstdc++.so.6.backup" -if [ -z "${DFHACK_NO_RENAME_LIBSTDCXX:-}" ] && [ -e "$libcxx_orig" ] && [ ! -e "$libcxx_backup" ]; then - mv "$libcxx_orig" "$libcxx_backup" - cat < /dev/null; then fi PRELOAD_LIB="${PRELOAD_LIB:+$PRELOAD_LIB:}${LIBSAN}${LIB}" -setarch_arch=$(cat hack/dfhack_setarch.txt || printf i386) +setarch_arch=$(cat hack/dfhack_setarch.txt || printf x86_64) if ! setarch "$setarch_arch" -R true 2>/dev/null; then echo "warn: architecture '$setarch_arch' not supported by setarch" >&2 if [ "$setarch_arch" = "i386" ]; then @@ -89,8 +60,8 @@ fi case "$1" in -g | --gdb) shift - echo "set exec-wrapper env LD_LIBRARY_PATH='$LD_LIBRARY_PATH' LD_PRELOAD='$PRELOAD_LIB' MALLOC_PERTURB_=45" > gdbcmd.tmp - gdb $DF_GDB_OPTS -x gdbcmd.tmp --args ./libs/Dwarf_Fortress "$@" + echo "set exec-wrapper env LD_LIBRARY_PATH='$LD_LIBRARY_PATH' LD_PRELOAD='${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB' MALLOC_PERTURB_=45" > gdbcmd.tmp + gdb $DF_GDB_OPTS -x gdbcmd.tmp --args ./dwarfort "$@" rm gdbcmd.tmp ret=$? ;; @@ -107,11 +78,11 @@ case "$1" in echo "set environment MALLOC_PERTURB_ 45" >> gdbcmd.tmp echo "set startup-with-shell off" >> gdbcmd.tmp echo "target extended-remote localhost:12345" >> gdbcmd.tmp - echo "set remote exec-file ./libs/Dwarf_Fortress" >> gdbcmd.tmp + echo "set remote exec-file ./dwarfort" >> gdbcmd.tmp # For some reason gdb ignores sysroot setting if it is from same file as # target extended-remote command echo "set sysroot /" > gdbcmd_sysroot.tmp - gdb $DF_GDB_OPTS -x gdbcmd.tmp -x gdbcmd_sysroot.tmp --args ./libs/Dwarf_Fortress "$@" + gdb $DF_GDB_OPTS -x gdbcmd.tmp -x gdbcmd_sysroot.tmp --args ./dwarfort "$@" rm gdbcmd.tmp gdbcmd_sysroot.tmp ret=$? ;; @@ -124,35 +95,35 @@ case "$1" in ;; -h | --helgrind) shift - LD_PRELOAD="$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_HELGRIND_OPTS --tool=helgrind --log-file=helgrind.log ./libs/Dwarf_Fortress "$@" + LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_HELGRIND_OPTS --tool=helgrind --log-file=helgrind.log ./dwarfort "$@" ret=$? ;; -v | --valgrind) shift - LD_PRELOAD="$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_VALGRIND_OPTS --log-file=valgrind.log ./libs/Dwarf_Fortress "$@" + LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_VALGRIND_OPTS --log-file=valgrind.log ./dwarfort "$@" ret=$? ;; -c | --callgrind) shift - LD_PRELOAD="$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_CALLGRIND_OPTS --tool=callgrind --separate-threads=yes --dump-instr=yes --instr-atstart=no --log-file=callgrind.log ./libs/Dwarf_Fortress "$@" + LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" setarch "$setarch_arch" -R valgrind $DF_CALLGRIND_OPTS --tool=callgrind --separate-threads=yes --dump-instr=yes --instr-atstart=no --log-file=callgrind.log ./dwarfort "$@" ret=$? ;; --strace) shift - strace -f setarch "$setarch_arch" -R env LD_PRELOAD="$PRELOAD_LIB" ./libs/Dwarf_Fortress "$@" 2> strace.log + strace -f setarch "$setarch_arch" -R env LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" ./dwarfort "$@" 2> strace.log ret=$? ;; -x | --exec) - exec setarch "$setarch_arch" -R env LD_PRELOAD="$PRELOAD_LIB" ./libs/Dwarf_Fortress "$@" + exec setarch "$setarch_arch" -R env LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" ./dwarfort "$@" # script does not resume ;; --sc | --sizecheck) PRELOAD_LIB="${PRELOAD_LIB:+$PRELOAD_LIB:}./hack/libsizecheck.so" - MALLOC_PERTURB_=45 setarch "$setarch_arch" -R env LD_PRELOAD="$PRELOAD_LIB" ./libs/Dwarf_Fortress "$@" + MALLOC_PERTURB_=45 setarch "$setarch_arch" -R env LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" ./dwarfort "$@" ret=$? ;; *) - setarch "$setarch_arch" -R env LD_PRELOAD="$PRELOAD_LIB" ./libs/Dwarf_Fortress "$@" + setarch "$setarch_arch" -R env LD_PRELOAD="${LD_PRELOAD:+$LD_PRELOAD:}$PRELOAD_LIB" ./dwarfort "$@" ret=$? ;; esac diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp index eaf741caf..6b95c2e87 100644 --- a/plugins/3dveins.cpp +++ b/plugins/3dveins.cpp @@ -7,15 +7,16 @@ #include "Core.h" #include "Console.h" +#include "DataDefs.h" +#include "Debug.h" #include "Export.h" +#include "MiscUtils.h" #include "PluginManager.h" + #include "modules/MapCache.h" #include "modules/Random.h" #include "modules/World.h" -#include "MiscUtils.h" - -#include "DataDefs.h" #include "df/world.h" #include "df/world_data.h" #include "df/world_region_details.h" @@ -47,6 +48,10 @@ DFHACK_PLUGIN("3dveins"); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gametype); +namespace DFHack { + DBG_DECLARE(_3dveins, process, DebugCategory::LINFO); +} + command_result cmd_3dveins(color_ostream &out, std::vector & parameters); DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) @@ -431,11 +436,13 @@ struct GeoLayer void print_mineral_stats(color_ostream &out) { for (auto it = mineral_count.begin(); it != mineral_count.end(); ++it) - out << " " << MaterialInfo(0,it->first.first).getToken() - << " " << ENUM_KEY_STR(inclusion_type,it->first.second) - << ": \t\t" << it->second << " (" << (float(it->second)/unmined_tiles) << ")" << std::endl; + 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(), + it->second, + (float(it->second) / unmined_tiles)); - out.print(" Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); + INFO(process, out).print ("3dveins: Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); } bool form_veins(color_ostream &out); @@ -467,12 +474,12 @@ struct GeoBiome void print_mineral_stats(color_ostream &out) { - out.print("Geological biome %d:\n", info.geo_index); + INFO(process,out).print("3dveins: Geological biome %d:\n", info.geo_index); for (size_t i = 0; i < layers.size(); i++) if (layers[i]) { - out << " Layer " << i << std::endl; + INFO(process, out).print("3dveins: Layer %ld\n", i); layers[i]->print_mineral_stats(out); } } @@ -586,7 +593,7 @@ bool VeinGenerator::init_biomes() if (info.geo_index < 0 || !info.geobiome) { - out.printerr("Biome %zd is not defined.\n", i); + WARN(process, out).print("Biome %zd is not defined.\n", i); return false; } @@ -797,8 +804,7 @@ bool VeinGenerator::scan_layer_depth(Block *b, df::coord2d column, int z) { if (z != min_level[idx]-1 && min_level[idx] <= top_solid) { - out.printerr( - "Discontinuous layer %d at (%d,%d,%d).\n", + WARN(process, out).print("Discontinuous layer %d at (%d,%d,%d).\n", layer->index, x+column.x*16, y+column.y*16, z ); return false; @@ -848,7 +854,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) if (max_level[i+1] != min_level[i]-1) { - out.printerr( + WARN(process, out).print( "Gap or overlap with next layer %d at (%d,%d,%d-%d).\n", i+1, x+column.x*16, y+column.y*16, max_level[i+1], min_level[i] ); @@ -891,7 +897,7 @@ bool VeinGenerator::adjust_layer_depth(df::coord2d column) } } - out.printerr( + WARN(process, out).print( "Layer height change in layer %d at (%d,%d,%d): %d instead of %d.\n", i, x+column.x*16, y+column.y*16, max_level[i], size, biome->layers[i]->thickness @@ -914,6 +920,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) for (int y = 0; y < 16; y++) { df::coord2d tile(x,y); + GeoLayer *layer = mapLayer(b, tile); if (!layer) continue; @@ -932,7 +939,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) if (unsigned(key.first) >= materials.size() || unsigned(key.second) >= NUM_INCLUSIONS) { - out.printerr("Invalid vein code: %d %d - aborting.\n",key.first,key.second); + WARN(process, out).print("Invalid vein code: %d %d - aborting.\n",key.first,key.second); return false; } @@ -941,7 +948,7 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) if (status == -1) { // Report first occurence of unreasonable vein spec - out.printerr( + WARN(process, out).print( "Unexpected vein %s %s - ", MaterialInfo(0,key.first).getToken().c_str(), ENUM_KEY_STR(inclusion_type, key.second).c_str() @@ -949,9 +956,9 @@ bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) status = materials[key.first].default_type; if (status < 0) - out.printerr("will be left in place.\n"); + WARN(process, out).print("will be left in place.\n"); else - out.printerr( + WARN(process, out).print( "correcting to %s.\n", ENUM_KEY_STR(inclusion_type, df::inclusion_type(status)).c_str() ); @@ -1090,7 +1097,7 @@ void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z) if (!ok) { - out.printerr( + WARN(process, out).print( "Couldn't write %d vein at (%d,%d,%d)\n", mat, x+column.x*16, y+column.y*16, z ); @@ -1281,7 +1288,7 @@ bool GeoLayer::form_veins(color_ostream &out) if (parent_id >= (int)refs.size()) { - out.printerr("Forward vein reference in biome %d.\n", biome->info.geo_index); + WARN(process, out).print("Forward vein reference in biome %d.\n", biome->info.geo_index); return false; } @@ -1301,7 +1308,7 @@ bool GeoLayer::form_veins(color_ostream &out) if (vptr->parent) ctx = "only be in "+MaterialInfo(0,vptr->parent_mat()).getToken(); - out.printerr( + 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(), @@ -1357,13 +1364,13 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (best.empty()) { - out.printerr( + 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() ); - return false; + return true; } for (auto it = best.begin(); size > 0 && it != best.end(); ++it) @@ -1391,7 +1398,7 @@ bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from) if (size > 0) { - out.printerr( + 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(), @@ -1541,7 +1548,7 @@ bool VeinGenerator::place_veins(bool verbose) if (!isStoneInorganic(key.first)) { - out.printerr( + WARN(process, out).print( "Invalid vein material: %s\n", MaterialInfo(0, key.first).getToken().c_str() ); @@ -1551,7 +1558,7 @@ bool VeinGenerator::place_veins(bool verbose) if (!is_valid_enum_item(key.second)) { - out.printerr("Invalid vein type: %d\n", key.second); + WARN(process, out).print("Invalid vein type: %d\n", key.second); return false; } @@ -1564,13 +1571,13 @@ bool VeinGenerator::place_veins(bool verbose) sort(queue.begin(), queue.end(), vein_cmp); // Place tiles - out.print("Processing... (%zu)", queue.size()); + TRACE(process,out).print("Processing... (%zu)", queue.size()); for (size_t j = 0; j < queue.size(); j++) { if (queue[j]->parent && !queue[j]->parent->placed) { - out.printerr( + 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() @@ -1582,9 +1589,11 @@ bool VeinGenerator::place_veins(bool verbose) if (verbose) { if (j > 0) - out.print("done."); + { + TRACE(process, out).print("done."); + } - out.print( + TRACE(process, out).print( "\nVein layer %zu of %zu: %s %s (%.2f%%)... ", j+1, queue.size(), MaterialInfo(0,queue[j]->vein.first).getToken().c_str(), @@ -1594,14 +1603,13 @@ bool VeinGenerator::place_veins(bool verbose) } else { - out.print("\rVein layer %zu of %zu... ", j+1, queue.size()); - out.flush(); + TRACE(process, out).print("\rVein layer %zu of %zu... ", j+1, queue.size()); } queue[j]->place_tiles(); } - out.print("done.\n"); + TRACE(process, out).print("done.\n"); return true; } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4029b8e2e..eee446db6 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -21,18 +21,20 @@ endif() option(BUILD_DEV_PLUGINS "Build developer plugins." OFF) if(BUILD_DEV_PLUGINS) - #add_subdirectory(devel) + add_subdirectory(devel) endif() -install(DIRECTORY lua/ - DESTINATION ${DFHACK_LUA_DESTINATION}/plugins - FILES_MATCHING PATTERN "*.lua") -install(DIRECTORY raw/ - DESTINATION ${DFHACK_DATA_DESTINATION}/raw - FILES_MATCHING PATTERN "*.txt") -install(DIRECTORY raw/ - DESTINATION ${DFHACK_DATA_DESTINATION}/raw - FILES_MATCHING PATTERN "*.diff") +if(INSTALL_DATA_FILES) + install(DIRECTORY lua/ + DESTINATION ${DFHACK_LUA_DESTINATION}/plugins + FILES_MATCHING PATTERN "*.lua") + install(DIRECTORY raw/ + DESTINATION ${DFHACK_DATA_DESTINATION}/raw + FILES_MATCHING PATTERN "*.txt") + install(DIRECTORY raw/ + DESTINATION ${DFHACK_DATA_DESTINATION}/raw + FILES_MATCHING PATTERN "*.diff") +endif() # Protobuf file(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto) @@ -73,102 +75,105 @@ set_source_files_properties( Brushes.h PROPERTIES HEADER_FILE_ONLY TRUE ) # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. -#dfhack_plugin(3dveins 3dveins.cpp) -dfhack_plugin(add-spatter add-spatter.cpp) -dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) -dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) -dfhack_plugin(autoclothing autoclothing.cpp LINK_LIBRARIES lua) -dfhack_plugin(design design.cpp LINK_LIBRARIES lua) -dfhack_plugin(autodump autodump.cpp) -dfhack_plugin(autofarm autofarm.cpp) -#dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) -add_subdirectory(autolabor) -dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) -dfhack_plugin(autoslab autoslab.cpp) -dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) -#dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) -#dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -add_subdirectory(buildingplan) -dfhack_plugin(changeitem changeitem.cpp) -dfhack_plugin(changelayer changelayer.cpp) -dfhack_plugin(changevein changevein.cpp) -add_subdirectory(channel-safely) -dfhack_plugin(cleanconst cleanconst.cpp) -dfhack_plugin(cleaners cleaners.cpp) -dfhack_plugin(cleanowned cleanowned.cpp) -dfhack_plugin(confirm confirm.cpp LINK_LIBRARIES lua) -dfhack_plugin(createitem createitem.cpp) -dfhack_plugin(cursecheck cursecheck.cpp) -dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) -dfhack_plugin(deramp deramp.cpp) -dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) -dfhack_plugin(dig dig.cpp) -dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) -#dfhack_plugin(digFlood digFlood.cpp) -#add_subdirectory(diggingInvaders) -#dfhack_plugin(dwarfvet dwarfvet.cpp) -#dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) -#add_subdirectory(embark-assistant) -#dfhack_plugin(embark-tools embark-tools.cpp) -dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) -dfhack_plugin(fastdwarf fastdwarf.cpp) -dfhack_plugin(faststart faststart.cpp) -dfhack_plugin(filltraffic filltraffic.cpp) -#dfhack_plugin(fix-unit-occupancy fix-unit-occupancy.cpp) -#dfhack_plugin(fixveins fixveins.cpp) -dfhack_plugin(flows flows.cpp) -#dfhack_plugin(follow follow.cpp) -#dfhack_plugin(forceequip forceequip.cpp) -#dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) -dfhack_plugin(getplants getplants.cpp) -dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) -#dfhack_plugin(infiniteSky infiniteSky.cpp) -#dfhack_plugin(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) -#dfhack_plugin(jobutils jobutils.cpp) -dfhack_plugin(lair lair.cpp) -dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) -dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) -dfhack_plugin(logistics logistics.cpp LINK_LIBRARIES lua) -#dfhack_plugin(manipulator manipulator.cpp) -#dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) -dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) -#dfhack_plugin(mode mode.cpp) -#dfhack_plugin(mousequery mousequery.cpp) -dfhack_plugin(nestboxes nestboxes.cpp) -dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) -dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) -dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) -#dfhack_plugin(petcapRemover petcapRemover.cpp) -#dfhack_plugin(plants plants.cpp) -dfhack_plugin(probe probe.cpp) -dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) -#dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) -dfhack_plugin(regrass regrass.cpp) -add_subdirectory(remotefortressreader) -#dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) -#add_subdirectory(rendermax) -dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) -#dfhack_plugin(search search.cpp) -dfhack_plugin(seedwatch seedwatch.cpp LINK_LIBRARIES lua) -dfhack_plugin(showmood showmood.cpp) -#dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) -#dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) -#dfhack_plugin(steam-engine steam-engine.cpp) -#add_subdirectory(spectate) -#dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) -add_subdirectory(stockpiles) -#dfhack_plugin(stocks stocks.cpp) -dfhack_plugin(strangemood strangemood.cpp) -dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua) -dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) -#dfhack_plugin(title-folder title-folder.cpp) -#dfhack_plugin(trackstop trackstop.cpp) -#dfhack_plugin(tubefill tubefill.cpp) -#add_subdirectory(tweak) -#dfhack_plugin(workflow workflow.cpp LINK_LIBRARIES lua) -dfhack_plugin(work-now work-now.cpp) -dfhack_plugin(xlsxreader xlsxreader.cpp LINK_LIBRARIES lua xlsxio_read_STATIC zip expat) -#dfhack_plugin(zone zone.cpp) +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(autobutcher autobutcher.cpp LINK_LIBRARIES lua) + dfhack_plugin(autochop autochop.cpp LINK_LIBRARIES lua) + dfhack_plugin(autoclothing autoclothing.cpp LINK_LIBRARIES lua) + dfhack_plugin(design design.cpp LINK_LIBRARIES lua) + dfhack_plugin(autodump autodump.cpp) + dfhack_plugin(autofarm autofarm.cpp) + #dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) + add_subdirectory(autolabor) + dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) + dfhack_plugin(autoslab autoslab.cpp) + dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) + dfhack_plugin(burrow burrow.cpp LINK_LIBRARIES lua) + #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) + add_subdirectory(buildingplan) + dfhack_plugin(changeitem changeitem.cpp) + dfhack_plugin(changelayer changelayer.cpp) + dfhack_plugin(changevein changevein.cpp) + add_subdirectory(channel-safely) + dfhack_plugin(cleanconst cleanconst.cpp) + dfhack_plugin(cleaners cleaners.cpp) + dfhack_plugin(cleanowned cleanowned.cpp) + dfhack_plugin(confirm confirm.cpp LINK_LIBRARIES lua) + dfhack_plugin(createitem createitem.cpp) + dfhack_plugin(cursecheck cursecheck.cpp) + dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) + dfhack_plugin(deramp deramp.cpp) + dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) + dfhack_plugin(dig dig.cpp) + dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) + #dfhack_plugin(digFlood digFlood.cpp) + #add_subdirectory(diggingInvaders) + dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) + #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) + #add_subdirectory(embark-assistant) + #dfhack_plugin(embark-tools embark-tools.cpp) + dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) + dfhack_plugin(fastdwarf fastdwarf.cpp) + dfhack_plugin(faststart faststart.cpp) + dfhack_plugin(filltraffic filltraffic.cpp) + #dfhack_plugin(fix-unit-occupancy fix-unit-occupancy.cpp) + #dfhack_plugin(fixveins fixveins.cpp) + dfhack_plugin(flows flows.cpp) + #dfhack_plugin(follow follow.cpp) + #dfhack_plugin(forceequip forceequip.cpp) + #dfhack_plugin(generated-creature-renamer generated-creature-renamer.cpp) + dfhack_plugin(getplants getplants.cpp) + dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) + #dfhack_plugin(infiniteSky infiniteSky.cpp) + #dfhack_plugin(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) + #dfhack_plugin(jobutils jobutils.cpp) + dfhack_plugin(lair lair.cpp) + dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) + dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) + dfhack_plugin(logistics logistics.cpp LINK_LIBRARIES lua) + #dfhack_plugin(manipulator manipulator.cpp) + #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) + dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) + #dfhack_plugin(mode mode.cpp) + #dfhack_plugin(mousequery mousequery.cpp) + dfhack_plugin(nestboxes nestboxes.cpp) + dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) + dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) + dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) + #dfhack_plugin(petcapRemover petcapRemover.cpp) + #dfhack_plugin(plants plants.cpp) + dfhack_plugin(preserve-tombs preserve-tombs.cpp) + dfhack_plugin(probe probe.cpp) + dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) + #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) + dfhack_plugin(regrass regrass.cpp) + add_subdirectory(remotefortressreader) + #dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) + #add_subdirectory(rendermax) + dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) + dfhack_plugin(seedwatch seedwatch.cpp LINK_LIBRARIES lua) + dfhack_plugin(showmood showmood.cpp) + #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) + dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) + #dfhack_plugin(steam-engine steam-engine.cpp) + add_subdirectory(spectate) + #dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) + add_subdirectory(stockpiles) + dfhack_plugin(stocks stocks.cpp LINK_LIBRARIES lua) + dfhack_plugin(strangemood strangemood.cpp) + dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua) + dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) + #dfhack_plugin(title-folder title-folder.cpp) + #dfhack_plugin(trackstop trackstop.cpp) + dfhack_plugin(tubefill tubefill.cpp) + #add_subdirectory(tweak) + dfhack_plugin(workflow workflow.cpp LINK_LIBRARIES lua) + dfhack_plugin(work-now work-now.cpp) + dfhack_plugin(xlsxreader xlsxreader.cpp LINK_LIBRARIES lua xlsxio_read_STATIC zip expat) + dfhack_plugin(zone zone.cpp LINK_LIBRARIES lua) +endif(BUILD_SUPPORTED) # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index a0cea765f..e058d2281 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -115,42 +115,44 @@ macro(dfhack_plugin) endif() endif() - add_library(${PLUGIN_NAME} MODULE ${PLUGIN_SOURCES}) - ide_folder(${PLUGIN_NAME} "Plugins") + if(BUILD_LIBRARY AND BUILD_PLUGINS) + add_library(${PLUGIN_NAME} MODULE ${PLUGIN_SOURCES}) + ide_folder(${PLUGIN_NAME} "Plugins") + + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/include") + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/proto") + target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") + target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/depends/xgetopt") + + if(NUM_PROTO) + add_dependencies(${PLUGIN_NAME} generate_proto_${PLUGIN_NAME}) + target_link_libraries(${PLUGIN_NAME} dfhack protobuf-lite dfhack-version ${PLUGIN_LINK_LIBRARIES}) + else() + target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${PLUGIN_LINK_LIBRARIES}) + endif() - target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/include") - target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/proto") - target_include_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proto") - target_include_directories(${PLUGIN_NAME} PRIVATE "${dfhack_SOURCE_DIR}/library/depends/xgetopt") + add_dependencies(${PLUGIN_NAME} dfhack-version) - if(NUM_PROTO) - add_dependencies(${PLUGIN_NAME} generate_proto_${PLUGIN_NAME}) - target_link_libraries(${PLUGIN_NAME} dfhack protobuf-lite dfhack-version ${PLUGIN_LINK_LIBRARIES}) - else() - target_link_libraries(${PLUGIN_NAME} dfhack dfhack-version ${PLUGIN_LINK_LIBRARIES}) - endif() + # Make sure the source is generated before the executable builds. + add_dependencies(${PLUGIN_NAME} generate_proto) - add_dependencies(${PLUGIN_NAME} dfhack-version) + if(UNIX) + set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") + else() + set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_MSVC}") + endif() + set_target_properties(${PLUGIN_NAME} PROPERTIES COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS}") - # Make sure the source is generated before the executable builds. - add_dependencies(${PLUGIN_NAME} generate_proto) + if(APPLE) + set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dylib PREFIX "") + elseif(UNIX) + set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.so PREFIX "") + else() + set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dll) + endif() - if(UNIX) - set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_GCC}") - else() - set(PLUGIN_COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS} ${PLUGIN_COMPILE_FLAGS_MSVC}") + install(TARGETS ${PLUGIN_NAME} + LIBRARY DESTINATION ${DFHACK_PLUGIN_DESTINATION} + RUNTIME DESTINATION ${DFHACK_PLUGIN_DESTINATION}) endif() - set_target_properties(${PLUGIN_NAME} PROPERTIES COMPILE_FLAGS "${PLUGIN_COMPILE_FLAGS}") - - if(APPLE) - set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dylib PREFIX "") - elseif(UNIX) - set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.so PREFIX "") - else() - set_target_properties(${PLUGIN_NAME} PROPERTIES SUFFIX .plug.dll) - endif() - - install(TARGETS ${PLUGIN_NAME} - LIBRARY DESTINATION ${DFHACK_PLUGIN_DESTINATION} - RUNTIME DESTINATION ${DFHACK_PLUGIN_DESTINATION}) endmacro() diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 536c74f0f..d3d8487de 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -746,6 +746,8 @@ static bool isInappropriateUnit(df::unit *unit) { static bool isProtectedUnit(df::unit *unit) { return Units::isWar(unit) // ignore war dogs etc || 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 // (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() diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index a214f5c94..3c8638cea 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -131,7 +131,7 @@ static command_result autodump_main(color_ostream &out, vector & parame return CR_FAILURE; } df::tiletype ttype = MC.tiletypeAt(pos_cursor); - if(!DFHack::isWalkable(ttype) || DFHack::isOpenTerrain(ttype)) + if(!DFHack::isWalkable(ttype)) { out.printerr("Cursor should be placed over a floor.\n"); return CR_FAILURE; diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 44253b2f9..e1511153c 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -436,7 +436,7 @@ DFhackCExport command_result plugin_init(color_ostream& out, std::vector ()); + autofarmInstance = std::move(std::make_unique()); autofarmInstance->load_state(out); return CR_OK; } diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 72bb4d84e..8be035214 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -305,6 +305,7 @@ static void cleanup_state() { enable_autolabor = false; labor_infos.clear(); + game->external_flag &= ~1; // reinstate DF's work detail system } static void reset_labor(df::unit_labor labor) @@ -326,6 +327,8 @@ static void init_state() if (!enable_autolabor) return; + game->external_flag |= 1; // bypass DF's work detail system + auto cfg_haulpct = World::GetPersistentData("autolabor/haulpct"); if (cfg_haulpct.isValid()) { @@ -413,8 +416,17 @@ static void enable_plugin(color_ostream &out) cleanup_state(); init_state(); +} + +static void disable_plugin(color_ostream& out) +{ + if (config.isValid()) + setOptionEnabled(CF_ENABLED, false); - game->external_flag |= 1; // shut down DF's work detail system + enable_autolabor = false; + out << "Disabling autolabor." << std::endl; + + cleanup_state(); } DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) @@ -1081,12 +1093,7 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) } else if(!enable && enable_autolabor) { - enable_autolabor = false; - setOptionEnabled(CF_ENABLED, false); - - game->external_flag &= ~1; // reenable DF's work detail system - - out << "Autolabor is disabled." << std::endl; + disable_plugin(out); } return CR_OK; diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp index e78314bfd..82333c822 100644 --- a/plugins/autoslab.cpp +++ b/plugins/autoslab.cpp @@ -151,24 +151,6 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) return CR_OK; } -// Name functions taken from manipulator.cpp -static std::string get_first_name(df::unit *unit) -{ - return Translation::capitalize(unit->name.first_name); -} - -static std::string get_last_name(df::unit *unit) -{ - df::language_name name = unit->name; - std::string ret = ""; - for (int i = 0; i < 2; i++) - { - if (name.words[i] >= 0) - ret += *world->raws.language.translations[name.language]->words[name.words[i]]; - } - return Translation::capitalize(ret); -} - // Queue up a single order to engrave the slab for the given unit static void createSlabJob(df::unit *unit) { @@ -212,7 +194,7 @@ static void checkslabs(color_ostream &out) ) { createSlabJob(ghost); - auto fullName = get_first_name(ghost) + " " + get_last_name(ghost); + auto fullName = Translation::TranslateName(&ghost->name, false); out.print("Added slab order for ghost %s\n", fullName.c_str()); } } diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index ce424f9c6..7a1416855 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -216,9 +216,8 @@ static void load_material_cache() { } static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { - // comment out until we can get heat safety working as intended - // if (cur_heat_safety.count(key)) - // return cur_heat_safety.at(key); + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); return HEAT_SAFETY_ANY; } @@ -675,13 +674,14 @@ static void scheduleCycle(color_ostream &out) { } static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, - int32_t custom, int index, bool ignore_filters, vector *item_ids = NULL, - map *counts = NULL) { + int32_t custom, int index, bool ignore_filters, bool ignore_quality, HeatSafety *heat_override = NULL, + vector *item_ids = NULL, map *counts = NULL) +{ DEBUG(status,out).print( - "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + "entering scanAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); - HeatSafety heat = get_heat_safety_filter(key); + HeatSafety heat = heat_override ? *heat_override : get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); if (index < 0 || job_items.size() <= (size_t)index) return 0; @@ -704,6 +704,10 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ filter.setMaterials(set()); special.clear(); } + if (ignore_quality) { + filter.setMinQuality(df::item_quality::Ordinary); + filter.setMaxQuality(df::item_quality::Artifact); + } if (itemPassesScreen(out, item) && matchesFilters(item, jitem, heat, filter, special)) { if (item_ids) item_ids->emplace_back(item->id); @@ -733,7 +737,25 @@ static int getAvailableItems(lua_State *L) { "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); vector item_ids; - scanAvailableItems(*out, type, subtype, custom, index, true, &item_ids); + scanAvailableItems(*out, type, subtype, custom, index, true, false, NULL, &item_ids); + Lua::PushVector(L, item_ids); + return 1; +} + +static int getAvailableItemsByHeat(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + HeatSafety heat = (HeatSafety)luaL_checkint(L, 5); + DEBUG(status,*out).print( + "entering getAvailableItemsByHeat building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector item_ids; + scanAvailableItems(*out, type, subtype, custom, index, true, true, &heat, &item_ids); Lua::PushVector(L, item_ids); return 1; } @@ -756,7 +778,30 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); - return scanAvailableItems(out, type, subtype, custom, index, false); + int count = scanAvailableItems(out, type, subtype, custom, index, false, false); + if (count) + return count; + + // nothing in stock; return how many are waiting in line as a negative + BuildingTypeKey key(type, subtype, custom); + auto &job_items = get_job_items(out, key); + if (index < 0 || job_items.size() <= (size_t)index) + return 0; + auto &jitem = job_items[index]; + + for (auto &entry : planned_buildings) { + auto &pb = entry.second; + // don't actually remove bad buildings from the list while we're + // actively iterating through that list + auto bld = pb.getBuildingIfValidOrRemoveIfNot(out, true); + if (!bld || bld->jobs.size() != 1) + continue; + for (auto pb_jitem : bld->jobs[0]->job_items) { + if (pb_jitem->item_type == jitem->item_type && pb_jitem->item_subtype == jitem->item_subtype) + count -= pb_jitem->quantity; + } + } + return count; } static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { @@ -946,24 +991,21 @@ static int getMaterialFilter(lua_State *L) { return 0; const auto &mat_filter = filters[index].getMaterials(); map counts; - scanAvailableItems(*out, type, subtype, custom, index, false, NULL, &counts); + scanAvailableItems(*out, type, subtype, custom, index, false, false, NULL, NULL, &counts); HeatSafety heat = get_heat_safety_filter(key); - df::job_item jitem_cur_heat = getJobItemWithHeatSafety( - get_job_items(*out, key)[index], heat); - df::job_item jitem_fire = getJobItemWithHeatSafety( - get_job_items(*out, key)[index], HEAT_SAFETY_FIRE); - df::job_item jitem_magma = getJobItemWithHeatSafety( - get_job_items(*out, key)[index], HEAT_SAFETY_MAGMA); + const df::job_item *jitem = get_job_items(*out, key)[index]; // name -> {count=int, enabled=bool, category=string, heat=string} map> ret; for (auto & entry : mat_cache) { auto &mat = entry.second.first; - if (!mat.matches(jitem_cur_heat)) + if (!mat.matches(jitem)) + continue; + if (!matchesHeatSafety(mat.type, mat.index, heat)) continue; string heat_safety = ""; - if (mat.matches(jitem_magma)) + if (matchesHeatSafety(mat.type, mat.index, HEAT_SAFETY_MAGMA)) heat_safety = "magma-safe"; - else if (mat.matches(jitem_fire)) + else if (matchesHeatSafety(mat.type, mat.index, HEAT_SAFETY_FIRE)) heat_safety = "fire-safe"; auto &name = entry.first; auto &cat = entry.second.second; @@ -1213,6 +1255,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getGlobalSettings), DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_COMMAND(getAvailableItemsByHeat), DFHACK_LUA_COMMAND(setMaterialMaskFilter), DFHACK_LUA_COMMAND(getMaterialMaskFilter), DFHACK_LUA_COMMAND(setMaterialFilter), diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 9bfd38731..a17f408a8 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -55,7 +55,7 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item, bool ignore_filters); bool itemPassesScreen(DFHack::color_ostream& out, df::item* item); -df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat); +bool matchesHeatSafety(int16_t mat_type, int32_t mat_index, HeatSafety heat); bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter, const std::set &special); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld, bool unsuspend_on_finalize); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 45cafe474..0a3f0d867 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -12,7 +12,6 @@ #include "df/item.h" #include "df/item_slabst.h" #include "df/job.h" -#include "df/map_block.h" #include "df/world.h" #include @@ -48,63 +47,102 @@ struct BadFlags { // up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. static bool isAccessible(color_ostream& out, df::item* item) { df::coord item_pos = Items::getPosition(item); - df::map_block* block = Maps::getTileBlock(item_pos); - bool is_walkable = false; - if (block) { - uint16_t walkability_group = index_tile(block->walkable, item_pos); - is_walkable = walkability_group != 0; - TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", - item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); - } + uint16_t walkability_group = Maps::getWalkableGroup(item_pos); + bool is_walkable = walkability_group != 0; + TRACE(cycle, out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "(probably) " : "not "); return is_walkable; } +// as of v50, soap, coal, and ash are no longer valid building materials +static bool isUnusableBar(color_ostream& out, df::item* item) { + if (item->getType() != df::item_type::BAR) + return false; + + MaterialInfo minfo(item); + string token = minfo.getToken(); + if (token.starts_with("COAL:") || token == "ASH") + return true; + + df::job_item_flags2 ok; + df::job_item_flags2 mask; + minfo.getMatchBits(ok, mask); + return ok.bits.soap; +} + bool itemPassesScreen(color_ostream& out, df::item* item) { static const BadFlags bad_flags; return !(item->flags.whole & bad_flags.whole) && !item->isAssignedToStockpile() - && isAccessible(out, item); + && isAccessible(out, item) + && !isUnusableBar(out, item); } -df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat) { - df::job_item jitem = *job_item; - if (heat >= HEAT_SAFETY_MAGMA) { - jitem.flags2.bits.magma_safe = true; - jitem.flags2.bits.fire_safe = false; - } else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe) - jitem.flags2.bits.fire_safe = true; - return jitem; +bool matchesHeatSafety(int16_t mat_type, int32_t mat_index, HeatSafety heat) { + if (heat == HEAT_SAFETY_ANY) + return true; + + MaterialInfo minfo(mat_type, mat_index); + df::job_item_flags2 ok; + df::job_item_flags2 mask; + minfo.getMatchBits(ok, mask); + + if (heat >= HEAT_SAFETY_MAGMA) + return ok.bits.magma_safe; + if (heat == HEAT_SAFETY_FIRE) + return ok.bits.fire_safe || ok.bits.magma_safe; + return false; } -bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter, const std::set &specials) { +bool matchesFilters(df::item * item, const df::job_item * jitem, HeatSafety heat, const ItemFilter &item_filter, const std::set &specials) { // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) + if (jitem->item_type > -1 && jitem->item_type != item->getType()) return false; - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) + if (jitem->item_subtype > -1 && + jitem->item_subtype != item->getSubtype()) return false; - if (job_item->flags2.bits.building_material && !item->isBuildMat()) + if (jitem->flags2.bits.building_material && !item->isBuildMat()) return false; - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + if ((jitem->flags1.bits.empty || jitem->flags2.bits.lye_milk_free)) { + auto gref = Items::getGeneralRef(item, df::general_ref_type::CONTAINS_ITEM); + if (gref) { + if (jitem->flags1.bits.empty) + return false; + if (auto contained_item = gref->getItem(); contained_item) { + MaterialInfo mi; + mi.decode(contained_item); + if (mi.getToken() != "WATER") + return false; + } + } + } + + if (jitem->metal_ore > -1 && !item->isMetalOre(jitem->metal_ore)) return false; - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) + if (jitem->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(jitem->has_tool_use)) return false; if (item->getType() == df::item_type::SLAB && specials.count("engraved") && static_cast(item)->engraving_type != df::slab_engraving_type::Memorial) return false; - df::job_item jitem = getJobItemWithHeatSafety(job_item, heat); + if (item->getType() == df::item_type::CAGE && specials.count("empty") + && (Items::getGeneralRef(item, df::general_ref_type::CONTAINS_UNIT) + || Items::getGeneralRef(item, df::general_ref_type::CONTAINS_ITEM))) + return false; + + if (!matchesHeatSafety(item->getMaterial(), item->getMaterialIndex(), heat)) + return false; return Job::isSuitableItem( - &jitem, item->getType(), item->getSubtype()) + jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( - &jitem, item->getMaterial(), item->getMaterialIndex(), + jitem, item->getMaterial(), item->getMaterialIndex(), item->getType()) && item_filter.matches(item); } diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index a20d7b29a..b16da01cb 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -109,8 +109,7 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafe PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), vector_ids(deserialize_vector_ids(out, bld_config)), - //heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), // until this works - heat_safety(HEAT_SAFETY_ANY), + heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), item_filters(get_item_filters(out, bld_config)), specials(get_specials(out, bld_config)), bld_config(bld_config) { } diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp new file mode 100644 index 000000000..8e4bfef2e --- /dev/null +++ b/plugins/burrow.cpp @@ -0,0 +1,687 @@ +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "TileTypes.h" + +#include "modules/Burrows.h" +#include "modules/EventManager.h" +#include "modules/Job.h" +#include "modules/Persistence.h" +#include "modules/World.h" + +#include "df/block_burrow.h" +#include "df/burrow.h" +#include "df/map_block.h" +#include "df/plotinfost.h" +#include "df/tile_designation.h" +#include "df/unit.h" +#include "df/world.h" + +using std::vector; +using std::string; +using namespace DFHack; + +DFHACK_PLUGIN("burrow"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(window_z); +REQUIRE_GLOBAL(world); + +// logging levels can be dynamically controlled with the `debugfilter` command. +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(burrow, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(burrow, event, DebugCategory::LINFO); +} + +static std::unordered_map active_dig_jobs; + +static command_result do_command(color_ostream &out, vector ¶meters); +static void init_diggers(color_ostream& out); +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); + commands.push_back( + PluginCommand("burrow", + "Quickly adjust burrow tiles and units.", + do_command)); + return CR_OK; +} + +static void reset() { + active_dig_jobs.clear(); +} + +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"); + reset(); + if (enable) { + init_diggers(out); + EventManager::registerListener(EventManager::EventType::JOB_STARTED, EventManager::EventHandler(jobStartedHandler, 0), plugin_self); + EventManager::registerListener(EventManager::EventType::JOB_COMPLETED, EventManager::EventHandler(jobCompletedHandler, 0), plugin_self); + } else { + EventManager::unregisterAll(plugin_self); + } + } + else { + DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + DEBUG(status, out).print("shutting down %s\n", plugin_name); + reset(); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) + reset(); + return CR_OK; +} + +static bool call_burrow_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling %s lua function: '%s'\n", plugin_name, fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.burrow", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!call_burrow_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// listener logic +// + +static void init_diggers(color_ostream& out) { + if (!Core::getInstance().isWorldLoaded()) { + DEBUG(status, out).print("world not yet loaded; not scanning jobs\n"); + return; + } + + std::vector pvec; + int start_id = 0; + if (Job::listNewlyCreated(&pvec, &start_id)) { + for (auto job : pvec) { + if (Job::getWorker(job)) + jobStartedHandler(out, job); + } + } +} + +static void jobStartedHandler(color_ostream& out, void* ptr) { + DEBUG(event, out).print("entering jobStartedHandler\n"); + + df::job *job = (df::job *)ptr; + auto type = ENUM_ATTR(job_type, type, job->job_type); + if (type != job_type_class::Digging) + 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()); + df::tiletype *tt = Maps::getTileType(pos); + if (tt) + active_dig_jobs[pos] = *tt; +} + +static void add_walls_to_burrow(color_ostream &out, df::burrow* b, + const df::coord & pos1, const df::coord & pos2) +{ + for (int z = pos1.z; z <= pos2.z; z++) { + for (int y = pos1.y; y <= pos2.y; y++) { + for (int x = pos1.x; x <= pos2.x; x++) { + df::coord pos(x,y,z); + df::tiletype *tt = Maps::getTileType(pos); + if (tt && isWallTerrain(*tt)) + Burrows::setAssignedTile(b, pos, true); + } + } + } +} + +static void expand_burrows(color_ostream &out, const df::coord & pos, df::tiletype prev_tt, df::tiletype tt) { + if (!isWalkable(tt) && tileShape(tt) != tiletype_shape::RAMP_TOP) + return; + + bool changed = false; + for (auto b : plotinfo->burrows.list) { + if (!b->name.ends_with('+') || !Burrows::isAssignedTile(b, pos)) + continue; + + if (!isWalkable(prev_tt)) { + changed = true; + add_walls_to_burrow(out, b, pos+df::coord(-1,-1,0), pos+df::coord(1,1,0)); + + if (isWalkableUp(tt)) + Burrows::setAssignedTile(b, pos+df::coord(0,0,1), true); + + if (tileShape(tt) == tiletype_shape::RAMP) + add_walls_to_burrow(out, b, pos+df::coord(-1,-1,1), pos+df::coord(1,1,1)); + } + + if (LowPassable(tt) && !LowPassable(prev_tt)) { + changed = true; + Burrows::setAssignedTile(b, pos-df::coord(0,0,1), true); + if (tileShape(tt) == tiletype_shape::RAMP_TOP) + add_walls_to_burrow(out, b, pos+df::coord(-1,-1,-1), pos+df::coord(1,1,-1)); + } + } + + if (changed) + Job::checkDesignationsNow(); +} + +static void jobCompletedHandler(color_ostream& out, void* ptr) { + DEBUG(event, out).print("entering jobCompletedHandler\n"); + + df::job *job = (df::job *)ptr; + auto type = ENUM_ATTR(job_type, type, job->job_type); + if (type != job_type_class::Digging) + 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()); + + df::tiletype prev_tt = active_dig_jobs[pos]; + df::tiletype *tt = Maps::getTileType(pos); + + if (tt && *tt && *tt != prev_tt) + expand_burrows(out, pos, prev_tt, *tt); + + active_dig_jobs.erase(pos); +} + +///////////////////////////////////////////////////// +// Lua API +// + +static void get_bool_field(lua_State *L, int idx, const char *name, bool *dest) { + lua_getfield(L, idx, name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + *dest = lua_toboolean(L, -1); + lua_pop(L, 1); +} + +static void get_opts(lua_State *L, int idx, bool &zlevel) { + if (lua_gettop(L) < idx) + return; + get_bool_field(L, idx, "zlevel", &zlevel); +} + +static bool get_int_field(lua_State *L, int idx, const char *name, int16_t *dest) { + lua_getfield(L, idx, name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return false; + } + *dest = lua_tointeger(L, -1); + lua_pop(L, 1); + return true; +} + +static bool get_bounds(lua_State *L, int idx, df::coord &pos1, df::coord &pos2) { + return get_int_field(L, idx, "x1", &pos1.x) && + get_int_field(L, idx, "y1", &pos1.y) && + get_int_field(L, idx, "z1", &pos1.z) && + get_int_field(L, idx, "x2", &pos2.x) && + get_int_field(L, idx, "y2", &pos2.y) && + get_int_field(L, idx, "z2", &pos2.z); +} + +static df::burrow* get_burrow(lua_State *L, int idx) { + df::burrow *burrow = NULL; + if (lua_isuserdata(L, idx)) + burrow = Lua::GetDFObject(L, idx); + else if (lua_isstring(L, idx)) + burrow = Burrows::findByName(luaL_checkstring(L, idx), true); + else if (lua_isinteger(L, idx)) + burrow = df::burrow::find(luaL_checkinteger(L, idx)); + return burrow; +} + +static void copyTiles(df::burrow *target, df::burrow *source, bool enable) { + CHECK_NULL_POINTER(target); + CHECK_NULL_POINTER(source); + + if (source == target) { + if (!enable) + Burrows::clearTiles(target); + return; + } + + vector pvec; + Burrows::listBlocks(&pvec, source); + + for (auto block : pvec) { + auto smask = Burrows::getBlockMask(source, block); + if (!smask) + continue; + + auto tmask = Burrows::getBlockMask(target, block, enable); + if (!tmask) + continue; + + if (enable) { + for (int j = 0; j < 16; j++) + tmask->tile_bitmask[j] |= smask->tile_bitmask[j]; + } else { + for (int j = 0; j < 16; j++) + tmask->tile_bitmask[j] &= ~smask->tile_bitmask[j]; + + if (!tmask->has_assignments()) + Burrows::deleteBlockMask(target, block, tmask); + } + } +} + +static void setTilesByDesignation(df::burrow *target, df::tile_designation d_mask, + df::tile_designation d_value, bool enable) { + CHECK_NULL_POINTER(target); + + auto &blocks = world->map.map_blocks; + + for (auto block : blocks) { + df::block_burrow *mask = NULL; + + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + if ((block->designation[x][y].whole & d_mask.whole) != d_value.whole) + continue; + + if (!mask) + mask = Burrows::getBlockMask(target, block, enable); + if (!mask) + goto next_block; + + mask->setassignment(x, y, enable); + } + } + + if (mask && !enable && !mask->has_assignments()) + Burrows::deleteBlockMask(target, block, mask); + + next_block:; + } +} + +static bool setTilesByKeyword(df::burrow *target, std::string name, bool enable) { + CHECK_NULL_POINTER(target); + + df::tile_designation mask; + df::tile_designation value; + + if (name == "ABOVE_GROUND") + mask.bits.subterranean = true; + else if (name == "SUBTERRANEAN") + mask.bits.subterranean = value.bits.subterranean = true; + else if (name == "LIGHT") + mask.bits.light = value.bits.light = true; + else if (name == "DARK") + mask.bits.light = true; + else if (name == "OUTSIDE") + mask.bits.outside = value.bits.outside = true; + else if (name == "INSIDE") + mask.bits.outside = true; + else if (name == "HIDDEN") + mask.bits.hidden = value.bits.hidden = true; + else if (name == "REVEALED") + mask.bits.hidden = true; + else + return false; + + setTilesByDesignation(target, mask, value, enable); + return true; +} + +static void copyUnits(df::burrow *target, df::burrow *source, bool enable) { + CHECK_NULL_POINTER(target); + CHECK_NULL_POINTER(source); + + if (source == target) { + if (!enable) + Burrows::clearUnits(target); + return; + } + + for (size_t i = 0; i < source->units.size(); i++) { + if (auto unit = df::unit::find(source->units[i])) + Burrows::setAssignedUnit(target, unit, enable); + } +} + +static int burrow_tiles_clear(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_clear\n"); + + lua_pushnil(L); // first key + while (lua_next(L, 1)) { + df::burrow * burrow = get_burrow(L, -1); + if (burrow) + Burrows::clearTiles(burrow); + lua_pop(L, 1); // remove value, leave key + } + + return 0; +} + +static void tiles_set_add_remove(lua_State *L, bool do_set, bool enable) { + df::burrow *target = get_burrow(L, 1); + if (!target) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return; + } + + if (do_set) + Burrows::clearTiles(target); + + lua_pushnil(L); // first key + while (lua_next(L, 2)) { + if (!lua_isstring(L, -1) || !setTilesByKeyword(target, luaL_checkstring(L, -1), enable)) { + if (auto burrow = get_burrow(L, -1)) + copyTiles(target, burrow, enable); + } + lua_pop(L, 1); // remove value, leave key + } +} + +static int burrow_tiles_set(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_set\n"); + tiles_set_add_remove(L, true, true); + return 0; +} + +static int burrow_tiles_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_add\n"); + tiles_set_add_remove(L, false, true); + return 0; +} + +static int burrow_tiles_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_remove\n"); + tiles_set_add_remove(L, false, false); + return 0; +} + +static void box_fill(lua_State *L, bool enable) { + df::burrow *burrow = get_burrow(L, 1); + if (!burrow) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return; + } + + df::coord pos_start, pos_end; + if (!get_bounds(L, 2, pos_start, pos_end)) { + luaL_argerror(L, 2, "invalid box bounds"); + return; + } + + for (int32_t z = pos_start.z; z <= pos_end.z; ++z) { + for (int32_t y = pos_start.y; y <= pos_end.y; ++y) { + for (int32_t x = pos_start.x; x <= pos_end.x; ++x) { + df::coord pos(x, y, z); + Burrows::setAssignedTile(burrow, pos, enable); + } + } + } +} + +static int burrow_tiles_box_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_box_add\n"); + box_fill(L, true); + return 0; +} + +static int burrow_tiles_box_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_box_remove\n"); + box_fill(L, false); + return 0; +} + +// ramp tops inherit walkability group of the tile below +static uint16_t get_walk_group(const df::coord & pos) { + uint16_t walk = Maps::getWalkableGroup(pos); + if (walk) + return walk; + if (auto tt = Maps::getTileType(pos)) { + if (tileShape(*tt) == df::tiletype_shape::RAMP_TOP) { + df::coord pos_below(pos); + --pos_below.z; + walk = Maps::getWalkableGroup(pos_below); + } + } + return walk; +} + +static void flood_fill(lua_State *L, bool enable) { + df::coord start_pos; + bool zlevel = false; + + df::burrow *burrow = get_burrow(L, 1); + if (!burrow) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return; + } + + Lua::CheckDFAssign(L, &start_pos, 2); + get_opts(L, 3, zlevel); + + df::tile_designation *start_des = Maps::getTileDesignation(start_pos); + if (!start_des) { + luaL_argerror(L, 2, "invalid starting coordinates"); + return; + } + uint16_t start_walk = Maps::getWalkableGroup(start_pos); + + std::stack flood; + flood.emplace(start_pos); + + while(!flood.empty()) { + const df::coord pos = flood.top(); + flood.pop(); + + df::tile_designation *des = Maps::getTileDesignation(pos); + if(!des || + des->bits.outside != start_des->bits.outside || + des->bits.hidden != start_des->bits.hidden) + { + continue; + } + + uint16_t walk = get_walk_group(pos); + if (!start_walk && walk) + continue; + + if (pos != start_pos && enable == Burrows::isAssignedTile(burrow, pos)) + continue; + + Burrows::setAssignedTile(burrow, pos, enable); + + // only go one tile outside of a walkability group + if (start_walk && start_walk != walk) + continue; + + flood.emplace(pos.x-1, pos.y-1, pos.z); + flood.emplace(pos.x, pos.y-1, pos.z); + flood.emplace(pos.x+1, pos.y-1, pos.z); + flood.emplace(pos.x-1, pos.y, pos.z); + flood.emplace(pos.x+1, pos.y, pos.z); + flood.emplace(pos.x-1, pos.y+1, pos.z); + flood.emplace(pos.x, pos.y+1, pos.z); + flood.emplace(pos.x+1, pos.y+1, pos.z); + + if (!zlevel) { + df::coord pos_above(pos); + ++pos_above.z; + df::tiletype *tt = Maps::getTileType(pos); + df::tiletype *tt_above = Maps::getTileType(pos_above); + if (tt_above && LowPassable(*tt_above)) + flood.emplace(pos_above); + if (tt && LowPassable(*tt)) + flood.emplace(pos.x, pos.y, pos.z-1); + } + } +} + +static int burrow_tiles_flood_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_flood_add\n"); + flood_fill(L, true); + return 0; +} + +static int burrow_tiles_flood_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_flood_remove\n"); + flood_fill(L, false); + return 0; +} + +static int burrow_units_clear(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_clear\n"); + + int32_t count = 0; + lua_pushnil(L); // first key + while (lua_next(L, 1)) { + df::burrow * burrow = get_burrow(L, -1); + if (burrow) { + count += burrow->units.size(); + Burrows::clearUnits(burrow); + } + lua_pop(L, 1); // remove value, leave key + } + + Lua::Push(L, count); + return 1; +} + +static void units_set_add_remove(lua_State *L, bool do_set, bool enable) { + df::burrow *target = get_burrow(L, 1); + if (!target) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return; + } + + if (do_set) + Burrows::clearUnits(target); + + lua_pushnil(L); // first key + while (lua_next(L, 2)) { + if (auto burrow = get_burrow(L, -1)) + copyUnits(target, burrow, enable); + lua_pop(L, 1); // remove value, leave key + } +} + +static int burrow_units_set(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_set\n"); + units_set_add_remove(L, true, true); + return 0; +} + +static int burrow_units_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_add\n"); + units_set_add_remove(L, false, true); + return 0; +} + +static int burrow_units_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_remove\n"); + units_set_add_remove(L, false, false); + return 0; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(burrow_tiles_clear), + DFHACK_LUA_COMMAND(burrow_tiles_set), + DFHACK_LUA_COMMAND(burrow_tiles_add), + DFHACK_LUA_COMMAND(burrow_tiles_remove), + DFHACK_LUA_COMMAND(burrow_tiles_box_add), + DFHACK_LUA_COMMAND(burrow_tiles_box_remove), + DFHACK_LUA_COMMAND(burrow_tiles_flood_add), + DFHACK_LUA_COMMAND(burrow_tiles_flood_remove), + DFHACK_LUA_COMMAND(burrow_units_clear), + DFHACK_LUA_COMMAND(burrow_units_set), + DFHACK_LUA_COMMAND(burrow_units_add), + DFHACK_LUA_COMMAND(burrow_units_remove), + DFHACK_LUA_END +}; diff --git a/plugins/burrows.cpp b/plugins/burrows.cpp deleted file mode 100644 index c39253488..000000000 --- a/plugins/burrows.cpp +++ /dev/null @@ -1,682 +0,0 @@ -#include "Core.h" -#include "Console.h" -#include "Export.h" -#include "PluginManager.h" -#include "Error.h" - -#include "DataFuncs.h" -#include "LuaTools.h" - -#include "modules/Gui.h" -#include "modules/Job.h" -#include "modules/Maps.h" -#include "modules/MapCache.h" -#include "modules/World.h" -#include "modules/Units.h" -#include "modules/Burrows.h" -#include "TileTypes.h" - -#include "DataDefs.h" -#include "df/plotinfost.h" -#include "df/world.h" -#include "df/unit.h" -#include "df/burrow.h" -#include "df/map_block.h" -#include "df/block_burrow.h" -#include "df/job.h" -#include "df/job_list_link.h" - -#include "MiscUtils.h" - -#include - -using std::vector; -using std::string; -using std::endl; -using namespace DFHack; -using namespace df::enums; -using namespace dfproto; - -DFHACK_PLUGIN("burrows"); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(gamemode); - -/* - * Initialization. - */ - -static command_result burrow(color_ostream &out, vector & parameters); - -static void init_map(color_ostream &out); -static void deinit_map(color_ostream &out); - -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand("burrow", - "Quick commands for burrow control.", - burrow)); - - if (Core::getInstance().isMapLoaded()) - init_map(out); - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - deinit_map(out); - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - deinit_map(out); - if (gamemode && - *gamemode == game_mode::DWARF) - init_map(out); - break; - case SC_MAP_UNLOADED: - deinit_map(out); - break; - default: - break; - } - - return CR_OK; -} - -/* - * State change tracking. - */ - -static int name_burrow_id = -1; - -static void handle_burrow_rename(color_ostream &out, df::burrow *burrow); - -DEFINE_LUA_EVENT_1(onBurrowRename, handle_burrow_rename, df::burrow*); - -static void detect_burrow_renames(color_ostream &out) -{ - if (plotinfo->main.mode == ui_sidebar_mode::Burrows && - plotinfo->burrows.in_edit_name_mode && - plotinfo->burrows.sel_id >= 0) - { - name_burrow_id = plotinfo->burrows.sel_id; - } - else if (name_burrow_id >= 0) - { - auto burrow = df::burrow::find(name_burrow_id); - name_burrow_id = -1; - if (burrow) - onBurrowRename(out, burrow); - } -} - -struct DigJob { - int id; - df::job_type job; - df::coord pos; - df::tiletype old_tile; -}; - -static int next_job_id_save = 0; -static std::map diggers; - -static void handle_dig_complete(color_ostream &out, df::job_type job, df::coord pos, - df::tiletype old_tile, df::tiletype new_tile, df::unit *worker); - -DEFINE_LUA_EVENT_5(onDigComplete, handle_dig_complete, - df::job_type, df::coord, df::tiletype, df::tiletype, df::unit*); - -static void detect_digging(color_ostream &out) -{ - for (auto it = diggers.begin(); it != diggers.end();) - { - auto worker = df::unit::find(it->first); - - if (!worker || !worker->job.current_job || - worker->job.current_job->id != it->second.id) - { - //out.print("Dig job %d expired.\n", it->second.id); - - df::coord pos = it->second.pos; - - if (auto block = Maps::getTileBlock(pos)) - { - df::tiletype new_tile = block->tiletype[pos.x&15][pos.y&15]; - - //out.print("Tile %d -> %d\n", it->second.old_tile, new_tile); - - if (new_tile != it->second.old_tile) - { - onDigComplete(out, it->second.job, pos, it->second.old_tile, new_tile, worker); - } - } - - auto cur = it; ++it; diggers.erase(cur); - } - else - ++it; - } - - std::vector jvec; - - if (Job::listNewlyCreated(&jvec, &next_job_id_save)) - { - for (size_t i = 0; i < jvec.size(); i++) - { - auto job = jvec[i]; - auto type = ENUM_ATTR(job_type, type, job->job_type); - if (type != job_type_class::Digging) - continue; - - auto worker = Job::getWorker(job); - if (!worker) - continue; - - df::coord pos = job->pos; - auto block = Maps::getTileBlock(pos); - if (!block) - continue; - - auto &info = diggers[worker->id]; - - //out.print("New dig job %d.\n", job->id); - - info.id = job->id; - info.job = job->job_type; - info.pos = pos; - info.old_tile = block->tiletype[pos.x&15][pos.y&15]; - } - } -} - -DFHACK_PLUGIN_IS_ENABLED(active); - -static bool auto_grow = false; -static std::vector grow_burrows; - -DFhackCExport command_result plugin_onupdate(color_ostream &out) -{ - if (!active) - return CR_OK; - - detect_burrow_renames(out); - - if (auto_grow) - detect_digging(out); - - return CR_OK; -} - -/* - * Config and processing - */ - -static std::map name_lookup; - -static void parse_names() -{ - auto &list = plotinfo->burrows.list; - - grow_burrows.clear(); - name_lookup.clear(); - - for (size_t i = 0; i < list.size(); i++) - { - auto burrow = list[i]; - - std::string name = burrow->name; - - if (!name.empty()) - { - name_lookup[name] = burrow->id; - - if (name[name.size()-1] == '+') - { - grow_burrows.push_back(burrow->id); - name.resize(name.size()-1); - } - - if (!name.empty()) - name_lookup[name] = burrow->id; - } - } -} - -static void reset_tracking() -{ - diggers.clear(); - next_job_id_save = 0; -} - -static void init_map(color_ostream &out) -{ - auto config = World::GetPersistentData("burrows/config"); - if (config.isValid()) - { - auto_grow = !!(config.ival(0) & 1); - } - - parse_names(); - name_burrow_id = -1; - - reset_tracking(); - active = true; - - if (auto_grow && !grow_burrows.empty()) - out.print("Auto-growing %zu burrows.\n", grow_burrows.size()); -} - -static void deinit_map(color_ostream &out) -{ - active = false; - auto_grow = false; - reset_tracking(); -} - -static PersistentDataItem create_config(color_ostream &out) -{ - bool created; - auto rv = World::GetPersistentData("burrows/config", &created); - if (created && rv.isValid()) - rv.ival(0) = 0; - if (!rv.isValid()) - out.printerr("Could not write configuration."); - return rv; -} - -static void enable_auto_grow(color_ostream &out, bool enable) -{ - if (enable == auto_grow) - return; - - auto config = create_config(out); - if (!config.isValid()) - return; - - if (enable) - config.ival(0) |= 1; - else - config.ival(0) &= ~1; - - auto_grow = enable; - - if (enable) - reset_tracking(); -} - -static void handle_burrow_rename(color_ostream &out, df::burrow *burrow) -{ - parse_names(); -} - -static void add_to_burrows(std::vector &burrows, df::coord pos) -{ - for (size_t i = 0; i < burrows.size(); i++) - Burrows::setAssignedTile(burrows[i], pos, true); -} - -static void add_walls_to_burrows(color_ostream &out, std::vector &burrows, - MapExtras::MapCache &mc, df::coord pos1, df::coord pos2) -{ - for (int x = pos1.x; x <= pos2.x; x++) - { - for (int y = pos1.y; y <= pos2.y; y++) - { - for (int z = pos1.z; z <= pos2.z; z++) - { - df::coord pos(x,y,z); - - auto tile = mc.tiletypeAt(pos); - - if (isWallTerrain(tile)) - add_to_burrows(burrows, pos); - } - } - } -} - -static void handle_dig_complete(color_ostream &out, df::job_type job, df::coord pos, - df::tiletype old_tile, df::tiletype new_tile, df::unit *worker) -{ - if (!isWalkable(new_tile)) - return; - - std::vector to_grow; - - for (size_t i = 0; i < grow_burrows.size(); i++) - { - auto b = df::burrow::find(grow_burrows[i]); - if (b && Burrows::isAssignedTile(b, pos)) - to_grow.push_back(b); - } - - //out.print("%d to grow.\n", to_grow.size()); - - if (to_grow.empty()) - return; - - MapExtras::MapCache mc; - bool changed = false; - - if (!isWalkable(old_tile)) - { - changed = true; - add_walls_to_burrows(out, to_grow, mc, pos+df::coord(-1,-1,0), pos+df::coord(1,1,0)); - - if (isWalkableUp(new_tile)) - add_to_burrows(to_grow, pos+df::coord(0,0,1)); - - if (tileShape(new_tile) == tiletype_shape::RAMP) - { - add_walls_to_burrows(out, to_grow, mc, - pos+df::coord(-1,-1,1), pos+df::coord(1,1,1)); - } - } - - if (LowPassable(new_tile) && !LowPassable(old_tile)) - { - changed = true; - add_to_burrows(to_grow, pos-df::coord(0,0,1)); - - if (tileShape(new_tile) == tiletype_shape::RAMP_TOP) - { - add_walls_to_burrows(out, to_grow, mc, - pos+df::coord(-1,-1,-1), pos+df::coord(1,1,-1)); - } - } - - if (changed && worker && !worker->job.current_job) - Job::checkDesignationsNow(); -} - -static void renameBurrow(color_ostream &out, df::burrow *burrow, std::string name) -{ - CHECK_NULL_POINTER(burrow); - - // The event makes this absolutely necessary - CoreSuspender suspend; - - burrow->name = name; - onBurrowRename(out, burrow); -} - -static df::burrow *findByName(color_ostream &out, std::string name, bool silent = false) -{ - int id = -1; - if (name_lookup.count(name)) - id = name_lookup[name]; - auto rv = df::burrow::find(id); - if (!rv && !silent) - out.printerr("Burrow not found: '%s'\n", name.c_str()); - return rv; -} - -static void copyUnits(df::burrow *target, df::burrow *source, bool enable) -{ - CHECK_NULL_POINTER(target); - CHECK_NULL_POINTER(source); - - if (source == target) - { - if (!enable) - Burrows::clearUnits(target); - - return; - } - - for (size_t i = 0; i < source->units.size(); i++) - { - auto unit = df::unit::find(source->units[i]); - - if (unit) - Burrows::setAssignedUnit(target, unit, enable); - } -} - -static void copyTiles(df::burrow *target, df::burrow *source, bool enable) -{ - CHECK_NULL_POINTER(target); - CHECK_NULL_POINTER(source); - - if (source == target) - { - if (!enable) - Burrows::clearTiles(target); - - return; - } - - std::vector pvec; - Burrows::listBlocks(&pvec, source); - - for (size_t i = 0; i < pvec.size(); i++) - { - auto block = pvec[i]; - auto smask = Burrows::getBlockMask(source, block); - if (!smask) - continue; - - auto tmask = Burrows::getBlockMask(target, block, enable); - if (!tmask) - continue; - - if (enable) - { - for (int j = 0; j < 16; j++) - tmask->tile_bitmask[j] |= smask->tile_bitmask[j]; - } - else - { - for (int j = 0; j < 16; j++) - tmask->tile_bitmask[j] &= ~smask->tile_bitmask[j]; - - if (!tmask->has_assignments()) - Burrows::deleteBlockMask(target, block, tmask); - } - } -} - -static void setTilesByDesignation(df::burrow *target, df::tile_designation d_mask, - df::tile_designation d_value, bool enable) -{ - CHECK_NULL_POINTER(target); - - auto &blocks = world->map.map_blocks; - - for (size_t i = 0; i < blocks.size(); i++) - { - auto block = blocks[i]; - df::block_burrow *mask = NULL; - - for (int x = 0; x < 16; x++) - { - for (int y = 0; y < 16; y++) - { - if ((block->designation[x][y].whole & d_mask.whole) != d_value.whole) - continue; - - if (!mask) - mask = Burrows::getBlockMask(target, block, enable); - if (!mask) - goto next_block; - - mask->setassignment(x, y, enable); - } - } - - if (mask && !enable && !mask->has_assignments()) - Burrows::deleteBlockMask(target, block, mask); - - next_block:; - } -} - -static bool setTilesByKeyword(df::burrow *target, std::string name, bool enable) -{ - CHECK_NULL_POINTER(target); - - df::tile_designation mask; - df::tile_designation value; - - if (name == "ABOVE_GROUND") - mask.bits.subterranean = true; - else if (name == "SUBTERRANEAN") - mask.bits.subterranean = value.bits.subterranean = true; - else if (name == "LIGHT") - mask.bits.light = value.bits.light = true; - else if (name == "DARK") - mask.bits.light = true; - else if (name == "OUTSIDE") - mask.bits.outside = value.bits.outside = true; - else if (name == "INSIDE") - mask.bits.outside = true; - else if (name == "HIDDEN") - mask.bits.hidden = value.bits.hidden = true; - else if (name == "REVEALED") - mask.bits.hidden = true; - else - return false; - - setTilesByDesignation(target, mask, value, enable); - return true; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(renameBurrow), - DFHACK_LUA_FUNCTION(findByName), - DFHACK_LUA_FUNCTION(copyUnits), - DFHACK_LUA_FUNCTION(copyTiles), - DFHACK_LUA_FUNCTION(setTilesByKeyword), - DFHACK_LUA_END -}; - -DFHACK_PLUGIN_LUA_EVENTS { - DFHACK_LUA_EVENT(onBurrowRename), - DFHACK_LUA_EVENT(onDigComplete), - DFHACK_LUA_END -}; - -static command_result burrow(color_ostream &out, vector ¶meters) -{ - CoreSuspender suspend; - - if (!active) - { - out.printerr("The plugin cannot be used without map.\n"); - return CR_FAILURE; - } - - string cmd; - if (!parameters.empty()) - cmd = parameters[0]; - - if (cmd == "enable" || cmd == "disable") - { - if (parameters.size() < 2) - return CR_WRONG_USAGE; - - bool state = (cmd == "enable"); - - for (size_t i = 1; i < parameters.size(); i++) - { - string &option = parameters[i]; - - if (option == "auto-grow") - enable_auto_grow(out, state); - else - return CR_WRONG_USAGE; - } - } - else if (cmd == "clear-units") - { - if (parameters.size() < 2) - return CR_WRONG_USAGE; - - for (size_t i = 1; i < parameters.size(); i++) - { - auto target = findByName(out, parameters[i]); - if (!target) - return CR_WRONG_USAGE; - - Burrows::clearUnits(target); - } - } - else if (cmd == "set-units" || cmd == "add-units" || cmd == "remove-units") - { - if (parameters.size() < 3) - return CR_WRONG_USAGE; - - auto target = findByName(out, parameters[1]); - if (!target) - return CR_WRONG_USAGE; - - if (cmd == "set-units") - Burrows::clearUnits(target); - - bool enable = (cmd != "remove-units"); - - for (size_t i = 2; i < parameters.size(); i++) - { - auto source = findByName(out, parameters[i]); - if (!source) - return CR_WRONG_USAGE; - - copyUnits(target, source, enable); - } - } - else if (cmd == "clear-tiles") - { - if (parameters.size() < 2) - return CR_WRONG_USAGE; - - for (size_t i = 1; i < parameters.size(); i++) - { - auto target = findByName(out, parameters[i]); - if (!target) - return CR_WRONG_USAGE; - - Burrows::clearTiles(target); - } - } - else if (cmd == "set-tiles" || cmd == "add-tiles" || cmd == "remove-tiles") - { - if (parameters.size() < 3) - return CR_WRONG_USAGE; - - auto target = findByName(out, parameters[1]); - if (!target) - return CR_WRONG_USAGE; - - if (cmd == "set-tiles") - Burrows::clearTiles(target); - - bool enable = (cmd != "remove-tiles"); - - for (size_t i = 2; i < parameters.size(); i++) - { - if (setTilesByKeyword(target, parameters[i], enable)) - continue; - - auto source = findByName(out, parameters[i]); - if (!source) - return CR_WRONG_USAGE; - - copyTiles(target, source, enable); - } - } - else - { - if (!parameters.empty() && cmd != "?") - out.printerr("Invalid command: %s\n", cmd.c_str()); - return CR_WRONG_USAGE; - } - - return CR_OK; -} diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index aa7a24461..67f8742b3 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -103,7 +103,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool WARN(manager).print(" has %d access\n", access); cavein_possible = config.riskaverse; cavein_candidates.emplace(pos, access); - least_access = min(access, least_access); + least_access = std::min(access, least_access); } } else if (config.insta_dig && isEntombed(miner_pos, pos)) { manage_one(pos, true, false); @@ -141,7 +141,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool 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 - auto b = max(0, cavein_candidates[pos] - least_access); + 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); evT->priority[Coord(local)] = v; diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 910e0ee7c..a297bf700 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -120,8 +120,7 @@ df::coord simulate_fall(const df::coord &pos) { while (Maps::ensureTileBlock(resting_pos)) { df::tiletype tt = *Maps::getTileType(resting_pos); - df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt)); - if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open) + if (isWalkable(tt)) break; --resting_pos.z; } diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index a29f5a04d..362fd927a 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -23,7 +23,7 @@ namespace CSP { inline uint32_t calc_distance(df::coord p1, df::coord p2) { // calculate chebyshev (chessboard) distance uint32_t distance = abs(p2.z - p1.z); - distance += max(abs(p2.x - p1.x), abs(p2.y - p1.y)); + distance += std::max(abs(p2.x - p1.x), abs(p2.y - p1.y)); return distance; } diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 3fece8bfe..c13f7826c 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -147,14 +147,14 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) out.print( "[%d] %s (wear level %d)", item->id, - description.c_str(), + DF2CONSOLE(description).c_str(), item->getWear() ); df::unit *owner = Items::getOwner(item); if (owner) - out.print(", owner %s", Translation::TranslateName(&owner->name,false).c_str()); + out.print(", owner %s", DF2CONSOLE(Translation::TranslateName(&owner->name,false)).c_str()); if (!dry_run) { diff --git a/plugins/confirm.cpp b/plugins/confirm.cpp index 1dfb6809d..bd6e41b64 100644 --- a/plugins/confirm.cpp +++ b/plugins/confirm.cpp @@ -411,7 +411,7 @@ public: Screen::paintTile(corner_ur, x2, y1); Screen::paintTile(corner_dl, x1, y2); Screen::paintTile(corner_dr, x2, y2); - string title = " " + get_title() + " "; + string title = ' ' + get_title() + ' '; Screen::paintString(Screen::Pen(' ', COLOR_DARKGREY, COLOR_BLACK), x2 - 6, y1, "DFHack"); Screen::paintString(Screen::Pen(' ', COLOR_BLACK, COLOR_GREY), diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 499814adc..ebc6eb7fe 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -178,6 +178,7 @@ command_result df_createitem (color_ostream &out, vector & parameters) case item_type::BUCKET: case item_type::ANIMALTRAP: case item_type::BOX: + case item_type::BAG: case item_type::BIN: case item_type::BACKPACK: case item_type::QUIVER: diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index febf169ca..210669a75 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -14,13 +14,13 @@ dfhack_plugin(frozen frozen.cpp) dfhack_plugin(kittens kittens.cpp LINK_LIBRARIES ${CMAKE_THREAD_LIBS_INIT} COMPILE_FLAGS_MSVC "/wd4316") dfhack_plugin(memview memview.cpp memutils.cpp LINK_LIBRARIES lua) dfhack_plugin(onceExample onceExample.cpp) -dfhack_plugin(renderer-msg renderer-msg.cpp) -dfhack_plugin(rprobe rprobe.cpp) -dfhack_plugin(stepBetween stepBetween.cpp) +# dfhack_plugin(renderer-msg renderer-msg.cpp) +# dfhack_plugin(rprobe rprobe.cpp) +# dfhack_plugin(stepBetween stepBetween.cpp) dfhack_plugin(stockcheck stockcheck.cpp) dfhack_plugin(stripcaged stripcaged.cpp) dfhack_plugin(tilesieve tilesieve.cpp) -dfhack_plugin(zoom zoom.cpp) +# dfhack_plugin(zoom zoom.cpp) if(UNIX) dfhack_plugin(ref-index ref-index.cpp) diff --git a/plugins/devel/check-structures-sanity/CMakeLists.txt b/plugins/devel/check-structures-sanity/CMakeLists.txt index f3eab5da6..69e326309 100644 --- a/plugins/devel/check-structures-sanity/CMakeLists.txt +++ b/plugins/devel/check-structures-sanity/CMakeLists.txt @@ -5,4 +5,4 @@ set(PLUGIN_SRCS validate.cpp ) -dfhack_plugin(check-structures-sanity ${PLUGIN_SRCS} LINK_LIBRARIES lua) +dfhack_plugin(check-structures-sanity ${PLUGIN_SRCS} LINK_LIBRARIES lua COMPILE_FLAGS_GCC "-O0 -ggdb3" COMPILE_FLAGS_MSVC "/Od") diff --git a/plugins/devel/check-structures-sanity/dispatch.cpp b/plugins/devel/check-structures-sanity/dispatch.cpp index 05dcc0377..c0d4764f9 100644 --- a/plugins/devel/check-structures-sanity/dispatch.cpp +++ b/plugins/devel/check-structures-sanity/dispatch.cpp @@ -1,5 +1,7 @@ #include "check-structures-sanity.h" +#include + #include "df/large_integer.h" Checker::Checker(color_ostream & out) : @@ -375,6 +377,14 @@ void Checker::dispatch_container(const QueueItem & item, const CheckedStructure { // TODO: check DfArray } + else if (base_container.starts_with("map<")) + { + // TODO: check map + } + else if (base_container.starts_with("unordered_map<")) + { + // TODO: check unordered_map + } else { UNEXPECTED; @@ -815,7 +825,7 @@ void Checker::check_stl_vector(const QueueItem & item, type_identity *item_ident auto vec_items = validate_vector_size(item, CheckedStructure(item_identity)); // skip bad pointer vectors - if (item.path.length() > 4 && item.path.substr(item.path.length() - 4) == ".bad" && item_identity->type() == IDTYPE_POINTER) + if ((item.path.ends_with(".bad") || item.path.ends_with(".temp_save")) && item_identity->type() == IDTYPE_POINTER) { return; } @@ -845,69 +855,51 @@ void Checker::check_stl_string(const QueueItem & item) #else struct string_data { - struct string_data_inner + uintptr_t start; + size_t length; + union { - size_t length; + char local_data[16]; size_t capacity; - int32_t refcount; - } *ptr; + }; }; #endif auto string = reinterpret_cast(item.ptr); #ifdef WIN32 - bool is_local = string->capacity < 16; + const bool is_gcc = false; + const bool is_local = string->capacity < 16; +#else + const bool is_gcc = true; + const bool is_local = string->start == reinterpret_cast(&string->local_data[0]); +#endif const char *start = is_local ? &string->local_data[0] : reinterpret_cast(string->start); ptrdiff_t length = string->length; ptrdiff_t capacity = string->capacity; -#else - if (!is_valid_dereference(QueueItem(item, "?ptr?", string->ptr), 1)) + + (void)start; + if (length < 0) { - // nullptr is NOT okay here - FAIL("invalid string pointer " << stl_sprintf("%p", string->ptr)); - return; + FAIL("string length is negative (" << length << ")"); } - if (!is_valid_dereference(QueueItem(item, "?hdr?", string->ptr - 1), sizeof(*string->ptr))) + 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)); return; } - const char *start = reinterpret_cast(string->ptr); - ptrdiff_t length = (string->ptr - 1)->length; - ptrdiff_t capacity = (string->ptr - 1)->capacity; -#endif - - (void)start; - if (length < 0) + else if (is_local && length >= 16) { - FAIL("string length is negative (" << length << ")"); + FAIL("string length is too large for small string (" << length << ")"); } - else if (capacity < 0) + else if ((!is_gcc || !is_local) && capacity < 0) { FAIL("string capacity is negative (" << capacity << ")"); } - else if (capacity < length) + else if ((!is_gcc || !is_local) && capacity < length) { FAIL("string capacity (" << capacity << ") is less than length (" << length << ")"); } - -#ifndef WIN32 - const std::string empty_string; - auto empty_string_data = reinterpret_cast(&empty_string); - if (sizes && string->ptr != empty_string_data->ptr) - { - size_t allocated_size = get_allocated_size(QueueItem(item, "?hdr?", string->ptr - 1)); - size_t expected_size = sizeof(*string->ptr) + capacity + 1; - - if (!allocated_size) - { - FAIL("pointer does not appear to be a string"); - } - else if (allocated_size != expected_size) - { - FAIL("allocated string data size (" << allocated_size << ") does not match expected size (" << expected_size << ")"); - } - } -#endif } void Checker::check_possible_pointer(const QueueItem & item, const CheckedStructure & cs) { diff --git a/plugins/devel/check-structures-sanity/types.cpp b/plugins/devel/check-structures-sanity/types.cpp index 86d691f1d..3e6ae5a2d 100644 --- a/plugins/devel/check-structures-sanity/types.cpp +++ b/plugins/devel/check-structures-sanity/types.cpp @@ -162,7 +162,7 @@ type_identity *Checker::wrap_in_stl_ptr_vector(type_identity *base) { return it->second.get(); } - return (wrappers[base] = dts::make_unique(base, nullptr)).get(); + return (wrappers[base] = std::make_unique(base, nullptr)).get(); } type_identity *Checker::wrap_in_pointer(type_identity *base) @@ -173,7 +173,7 @@ type_identity *Checker::wrap_in_pointer(type_identity *base) { return it->second.get(); } - return (wrappers[base] = dts::make_unique(base)).get(); + return (wrappers[base] = std::make_unique(base)).get(); } std::map> known_types_by_size; diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp index 1cd56255b..028d4af6d 100644 --- a/plugins/dig-now.cpp +++ b/plugins/dig-now.cpp @@ -832,8 +832,7 @@ static DFCoord simulate_fall(const DFCoord &pos) { while (Maps::ensureTileBlock(resting_pos)) { df::tiletype tt = *Maps::getTileType(resting_pos); - df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt)); - if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open) + if (isWalkable(tt)) break; --resting_pos.z; } diff --git a/plugins/dig.cpp b/plugins/dig.cpp index 879d0dd52..7be7b815f 100644 --- a/plugins/dig.cpp +++ b/plugins/dig.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "Core.h" #include "Console.h" @@ -1072,14 +1073,13 @@ command_result digv (color_ostream &out, vector & parameters) con.printerr("I won't dig the borders. That would be cheating!\n"); return CR_FAILURE; } - MapExtras::MapCache * MCache = new MapExtras::MapCache; + std::unique_ptr MCache = std::make_unique(); df::tile_designation des = MCache->designationAt(xy); df::tiletype tt = MCache->tiletypeAt(xy); int16_t veinmat = MCache->veinMaterialAt(xy); if( veinmat == -1 ) { con.printerr("This tile is not a vein.\n"); - delete MCache; return CR_FAILURE; } con.print("%d/%d/%d tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, des.whole); @@ -1192,7 +1192,6 @@ command_result digv (color_ostream &out, vector & parameters) } } MCache->WriteAll(); - delete MCache; return CR_OK; } @@ -1259,7 +1258,7 @@ command_result digl (color_ostream &out, vector & parameters) con.printerr("I won't dig the borders. That would be cheating!\n"); return CR_FAILURE; } - MapExtras::MapCache * MCache = new MapExtras::MapCache; + std::unique_ptr MCache = std::make_unique(); df::tile_designation des = MCache->designationAt(xy); df::tiletype tt = MCache->tiletypeAt(xy); int16_t veinmat = MCache->veinMaterialAt(xy); @@ -1267,7 +1266,6 @@ command_result digl (color_ostream &out, vector & parameters) if( veinmat != -1 ) { con.printerr("This is a vein. Use digv instead!\n"); - delete MCache; return CR_FAILURE; } con.print("%d/%d/%d tiletype: %d, basemat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, basemat, des.whole); @@ -1408,7 +1406,6 @@ command_result digl (color_ostream &out, vector & parameters) } } MCache->WriteAll(); - delete MCache; return CR_OK; } @@ -1424,9 +1421,13 @@ command_result digtype (color_ostream &out, vector & parameters) return CR_FAILURE; } + uint32_t zMin = 0; uint32_t xMax,yMax,zMax; Maps::getSize(xMax,yMax,zMax); + bool hidden = false; + bool automine = true; + int32_t targetDigType = -1; for (string parameter : parameters) { if ( parameter == "clear" ) @@ -1443,8 +1444,16 @@ command_result digtype (color_ostream &out, vector & parameters) targetDigType = tile_dig_designation::DownStair; else if ( parameter == "up" ) targetDigType = tile_dig_designation::UpStair; - else if ( parameter == "-z" ) + else if ( parameter == "-z" || parameter == "--cur-zlevel" ) + {zMax = *window_z + 1; zMin = *window_z;} + else if ( parameter == "--zdown" || parameter == "-d") zMax = *window_z + 1; + else if ( parameter == "--zup" || parameter == "-u") + zMin = *window_z; + else if ( parameter == "--hidden" || parameter == "-h") + hidden = true; + else if ( parameter == "--no-auto" || parameter == "-a" ) + automine = false; else { out.printerr("Invalid parameter: '%s'.\n", parameter.c_str()); @@ -1462,14 +1471,21 @@ command_result digtype (color_ostream &out, vector & parameters) return CR_FAILURE; } DFHack::DFCoord xy ((uint32_t)cx,(uint32_t)cy,cz); - MapExtras::MapCache * mCache = new MapExtras::MapCache; + std::unique_ptr mCache = std::make_unique(); df::tile_designation baseDes = mCache->designationAt(xy); + + if (baseDes.bits.hidden && !hidden) { + out.printerr("Cursor is pointing at a hidden tile. Point the cursor at a visible tile when using the --hidden option.\n"); + return CR_FAILURE; + } + + df::tile_occupancy baseOcc = mCache->occupancyAt(xy); + df::tiletype tt = mCache->tiletypeAt(xy); int16_t veinmat = mCache->veinMaterialAt(xy); if( veinmat == -1 ) { out.printerr("This tile is not a vein.\n"); - delete mCache; return CR_FAILURE; } out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole); @@ -1485,8 +1501,12 @@ command_result digtype (color_ostream &out, vector & parameters) baseDes.bits.dig = tile_dig_designation::Default; } } + // Auto dig only works on default dig designation. Setting dig_auto for any other designation + // prevents dwarves from digging that tile at all. + if (baseDes.bits.dig == tile_dig_designation::Default && automine) baseOcc.bits.dig_auto = true; + else baseOcc.bits.dig_auto = false; - for( uint32_t z = 0; z < zMax; z++ ) + for( uint32_t z = zMin; z < zMax; z++ ) { for( uint32_t x = 1; x < tileXMax-1; x++ ) { @@ -1506,18 +1526,22 @@ command_result digtype (color_ostream &out, vector & parameters) if ( !mCache->testCoord(current) ) { out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z); - delete mCache; return CR_FAILURE; } df::tile_designation designation = mCache->designationAt(current); + + if (designation.bits.hidden && !hidden) continue; + + df::tile_occupancy occupancy = mCache->occupancyAt(current); designation.bits.dig = baseDes.bits.dig; - mCache->setDesignationAt(current, designation,priority); + occupancy.bits.dig_auto = baseOcc.bits.dig_auto; + mCache->setDesignationAt(current, designation, priority); + mCache->setOccupancyAt(current, occupancy); } } } mCache->WriteAll(); - delete mCache; return CR_OK; } diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 2b546bfad..27e5b87e0 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -951,7 +951,7 @@ public: { df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr; Screen::dismiss(this); - Screen::show(dts::make_unique(selected_unit), plugin_self); + Screen::show(std::make_unique(selected_unit), plugin_self); } else if (input->count(interface_key::CUSTOM_SHIFT_Z)) { @@ -1676,7 +1676,7 @@ private: static void open_stats_screen() { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } static void add_work_history(df::unit *unit, activity_type type) { @@ -1776,12 +1776,12 @@ static command_result dwarfmonitor_cmd(color_ostream &, vector & parame if (cmd == 's' || cmd == 'S') { CoreSuspender guard; if(Maps::IsValid()) - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } else if (cmd == 'p' || cmd == 'P') { CoreSuspender guard; if(Maps::IsValid()) - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } else return CR_WRONG_USAGE; diff --git a/plugins/dwarfvet.cpp b/plugins/dwarfvet.cpp index 75f015975..dcd8e0f8c 100644 --- a/plugins/dwarfvet.cpp +++ b/plugins/dwarfvet.cpp @@ -20,735 +20,170 @@ * THE SOFTWARE. **/ -#include "Console.h" -#include "Core.h" -#include "DataDefs.h" -#include "Export.h" +#include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" -#include "modules/EventManager.h" + +#include "modules/Persistence.h" #include "modules/Units.h" -#include "modules/Buildings.h" -#include "modules/Maps.h" -#include "modules/Job.h" +#include "modules/World.h" -#include "df/animal_training_level.h" -#include "df/building_type.h" -#include "df/caste_raw.h" -#include "df/caste_raw_flags.h" -#include "df/creature_raw.h" -#include "df/job.h" -#include "df/general_ref_unit_workerst.h" -#include "df/profession.h" -#include "df/plotinfost.h" -#include "df/unit.h" -#include "df/unit_health_info.h" -#include "df/unit_health_flags.h" #include "df/world.h" -#include -#include -#include - using namespace DFHack; -using namespace DFHack::Units; -using namespace DFHack::Buildings; - -using namespace std; +using std::string; +using std::vector; DFHACK_PLUGIN("dwarfvet"); -DFHACK_PLUGIN_IS_ENABLED(dwarfvet_enabled); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); -static unordered_set tracked_units; -static int32_t howOften = 100; - -struct hospital_spot { - int32_t x; - int32_t y; - int32_t z; -}; - -class Patient { - public: - // Constructor/Deconstrctor - Patient(int32_t id, size_t spot_index, int32_t x, int32_t y, int32_t z); - int32_t getID() { return this->id; }; - size_t getSpotIndex() { return this->spot_index; }; - int32_t returnX() { return this->spot_in_hospital.x; }; - int32_t returnY() { return this->spot_in_hospital.y; }; - int32_t returnZ() { return this->spot_in_hospital.z; }; - - private: - struct hospital_spot spot_in_hospital; - int32_t id; - size_t spot_index; -}; - -Patient::Patient(int32_t id, size_t spot_index, int32_t x, int32_t y, int32_t z){ - this->id = id; - this->spot_index = spot_index; - this->spot_in_hospital.x = x; - this->spot_in_hospital.y = y; - this->spot_in_hospital.z = z; +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(dwarfvet, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(dwarfvet, cycle, DebugCategory::LINFO); } -class AnimalHospital { - - public: - // Constructor - AnimalHospital(df::building *, color_ostream &out); - ~AnimalHospital(); - int32_t getID() { return id; } - bool acceptPatient(int32_t id, color_ostream&); - void processPatients(color_ostream &out); - void dischargePatient(Patient * patient, color_ostream &out); - void calculateHospital(bool force, color_ostream &out); - void reportUsage(color_ostream &out); - - // GC - bool to_be_deleted; - - private: - int spots_open; - int32_t id; - int32_t x1; - int32_t x2; - int32_t y1; - int32_t y2; - int32_t z; - int height; - int length; - - // Doing an actual array in C++ is *annoying*, bloody copy constructors */ - vector spots_in_use; - vector building_in_hospital_notification; /* If present, we already notified about this */ - vector accepted_patients; +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, }; - -AnimalHospital::AnimalHospital(df::building * building, color_ostream &out) { - // Copy in what we need to know - id = building->id; - x1 = building->x1; - x2 = building->x2; - y1 = building->y1; - y2 = building->y2; - z = building->z; - - // Determine how many spots we have for animals - this->length = x2-x1+1; - this->height = y2-y1+1; - - // And calculate the hospital! - this->calculateHospital(true, out); +static int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); } - -AnimalHospital::~AnimalHospital() { - // Go through and delete all the patients - for (Patient* accepted_patient : this->accepted_patients) { - delete accepted_patient; - } +static bool get_config_bool(int index) { + return get_config_val(index) == 1; } - -bool AnimalHospital::acceptPatient(int32_t id, color_ostream &out) { - // This function determines if we can accept a patient, and if we will. - this->calculateHospital(true, out); - - // First, do we have room? - if (!spots_open) return false; - - // Yup, let's find the next open spot, - // and give it to our patient - int spot_cur = 0; - for (vector::iterator spot = this->spots_in_use.begin(); spot != this->spots_in_use.end(); spot++) { - if (*spot == false) { - *spot = true; - break; - } - spot_cur++; - } - - spots_open--; - - // Convert the spot into x/y/z cords. - int offset_y = spot_cur/length; - int offset_x = spot_cur%length; - - // Create the patient! - Patient * patient = new Patient(id, - spot_cur, - this->x1+offset_x, - this->y1+offset_y, - this->z - ); - - accepted_patients.push_back(patient); - return true; +static void set_config_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; } - -// Before any use of the hospital, we need to make calculate open spots -// and such. This can change (i.e. stuff built in hospital) and -// such so it should be called on each function. -void AnimalHospital::reportUsage(color_ostream &out) { - // Debugging tool to see parts of the hospital in use - int length_cursor = this->length; - - for (bool spot : this->spots_in_use) { - if (spot) out.print("X"); - else out.print("-"); - length_cursor--; - if (length_cursor <= 0) { - out.print("\n"); - length_cursor = this->length; - } - } - out.print("\n"); - +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); } -void AnimalHospital::calculateHospital(bool force, color_ostream &out) { - // Only calculate out the hospital if we actually have a patient in it - // (acceptPatient will forcibly rerun this to make sure everything OK - - // Should reduce FPS impact of each calculation tick when the hospitals - // are not in use - //if (!force || (spots_open == length*height)) { - // Hospital is idle, don't recalculate - // return; - //} +static const int32_t CYCLE_TICKS = 2459; // a prime number that's around 2 days +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - // Calculate out the total area of the hospital - // This can change if a hospital has been resized - this->spots_open = length*height; - this->spots_in_use.assign(this->spots_open, false); - - // The spots_in_use maps one to one with a spot - // starting at the upper-left hand corner, then - // across, then down. i.e. - // - // given hospital zone: - // - // UU - // uU - // - // where U is in use, and u isn't, the array - // would be t,t,f,t - - // Walk the building array and see what stuff is in the hospital, - // then walk the patient array and remark those spots as used. - - // If a patient is in an invalid spot, reassign it - for (df::building *building : world->buildings.all) { - - // Check that we're not comparing ourselves; - if (building->id == this->id) { - continue; - } - - // Check if the building is on our z level, if it isn't - // then it can't overlap the hospital (until Toady implements - // multi-z buildings - if (building->z != this->z) { - continue; - } - - // DF defines activity zones multiple times in the building structure - // If axises agree with each other, we're looking at a reflection of - // ourselves - if (building->x1 == this->x1 && - building->x2 == this->x2 && - building->y1 == this->y1 && - building->y2 == this->y2) { - continue; - } - - // Check for X/Y overlap - // I can't believe I had to look this up -_-; - // http://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other - if ((this->x1 > building->x2 || - building->x1 > this->x2 || - this->y1 > building->y2 || - building->y1 > this->y2)) { - continue; - } - - // Crap, building overlaps, we need to figure out where it is in the hospital - // NOTE: under some conditions, this generates a false warning. Not a lot I can do about it - - // Mark spots used by that building as used; FIXME: handle special logic for traction benches and such - int overlap_x1 = std::max(building->x1, this->x1); - int overlap_y1 = std::max(building->y1, this->y1); - int overlap_x2 = std::min(building->x2, this->x2); - int overlap_y2 = std::min(building->y2, this->y2); - for (int x = overlap_x1; x <= overlap_x2; x++) { - for (int y = overlap_y1; y <= overlap_y2; y++) { - int spot_index = (x - this->x1) + (this->length * (y - this->y1)); - spots_in_use.at(spot_index) = true; - } - } - } +static command_result do_command(color_ostream &out, vector ¶meters); +static void dwarfvet_cycle(color_ostream &out); +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { + commands.push_back(PluginCommand( + plugin_name, + "Allow animals to be treated at hospitals.", + do_command)); + return CR_OK; } -// Self explanatory -void AnimalHospital::dischargePatient(Patient * patient, color_ostream &out) { - int32_t id = patient->getID(); - - // Remove them from the hospital - - // We can safely iterate here because once we delete the unit - // we no longer use the iterator - for (vector::iterator accepted_patient = this->accepted_patients.begin(); accepted_patient != this->accepted_patients.end(); accepted_patient++) { - if ( (*accepted_patient)->getID() == id) { - out.print("Discharging unit %d from hospital %d\n", id, this->id); - // Reclaim their spot - spots_in_use.at((*accepted_patient)->getSpotIndex()) = false; - this->spots_open++; - delete (*accepted_patient); - this->accepted_patients.erase(accepted_patient); - break; - } +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - // And the master list - tracked_units.erase(id); - - return; -} - -void AnimalHospital::processPatients(color_ostream &out) { - // Where the magic happens - for (Patient *patient : this->accepted_patients) { - int id = patient->getID(); - df::unit *real_unit = df::unit::find(id); - - // Check to make sure the unit hasn't expired before assigning a job, or if they've been healed - if (!real_unit || !Units::isActive(real_unit) || !real_unit->health->flags.bits.needs_healthcare) { - // discharge the patient from the hospital - this->dischargePatient(patient, out); - return; - } - - // Give the unit a job if they don't have any - if (!real_unit->job.current_job) { - // Create REST struct - df::job * job = new df::job; - DFHack::Job::linkIntoWorld(job); - - job->pos.x = patient->returnX(); - job->pos.y = patient->returnY(); - job->pos.z = patient->returnZ(); - job->flags.bits.special = 1; - job->job_type = df::enums::job_type::Rest; - df::general_ref *ref = df::allocate(); - ref->setID(real_unit->id); - job->general_refs.push_back(ref); - real_unit->job.current_job = job; - job->wait_timer = 1600; - out.print("Telling intelligent unit %d to report to the hospital!\n", real_unit->id); - } + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } + return CR_OK; } +DFhackCExport command_result plugin_load_data(color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); -static vector animal_hospital_zones; - -void delete_animal_hospital_vector(color_ostream &out) { - if (dwarfvet_enabled) { - out.print("Clearing all animal hospitals\n"); - } - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - delete animal_hospital; + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); } - animal_hospital_zones.clear(); -} - -command_result dwarfvet(color_ostream &out, std::vector & parameters); - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back(PluginCommand( - "dwarfvet", - "Allows animals to be treated at animal hospitals", - dwarfvet)); - return CR_OK; -} -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ + is_enabled = get_config_bool(CONFIG_IS_ENABLED); + DEBUG(status,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); return CR_OK; } -bool isActiveAnimalHospital(df::building * building) { - if (Buildings::isHospital(building) && Buildings::isAnimalTraining(building) && Buildings::isActive(building)) { - return true; +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(status,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } } - - return false; + return CR_OK; } -bool compareAnimalHospitalZones(df::building * hospital1, df::building * hospital2) { - // We compare hospitals by checking if positions are identical, not by ID - // since activity zones can easily be changed in size - - if ( hospital1->x1 == hospital2->x1 && - hospital1->x2 == hospital2->x2 && - hospital1->y1 == hospital2->y1 && - hospital1->y2 == hospital2->y2 && - hospital1->z == hospital2->z) { - return true; - } - - return false; +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + dwarfvet_cycle(out); + return CR_OK; } -void tickHandler(color_ostream& out, void* data) { - if ( !dwarfvet_enabled ) - return; - CoreSuspender suspend; - int32_t own_race_id = df::global::plotinfo->race_id; - - /** - * Generate a list of animal hospitals on the map - * - * Since activity zones can change any instant given user interaction - * we need to be constantly on the lookout for changed zones, and update - * our cached list on the fly if necessary. - **/ - - vector hospitals_on_map; - - // Because C++ iterators suck, we're going to build a temporary vector with the AHZ, and then - // copy it for my own bloody sanity (and compilance with the STL spec) - vector ahz_scratch; - - // Holding area for things to be added to the scratch - vector to_be_added; - - - // Walk the building tree, and generate a list of animal hospitals on the map - for (df::building* building : df::building::get_vector()) { - if (isActiveAnimalHospital(building)) { - hospitals_on_map.push_back(building); - } - } - - int count_of_hospitals = hospitals_on_map.size(); - int hospitals_cached = animal_hospital_zones.size(); - //out.print ("count_of_Hospitals: %d, hospitals_cached: %d\n", count_of_hospitals, hospitals_cached); - // It's possible our hospital cache is empty, if so, simply copy it, and jump to the main logic - if (!hospitals_cached && count_of_hospitals) { - out.print("Populating hospital cache:\n"); - for (df::building *current_hospital : hospitals_on_map) { - AnimalHospital * hospital = new AnimalHospital(current_hospital, out); - out.print(" Found animal hospital %d at x1: %d, y1: %d, z: %d from valid hospital list\n", - hospital->getID(), - current_hospital->x1, - current_hospital->y1, - current_hospital->z - ); - animal_hospital_zones.push_back(hospital); - } - - goto processUnits; - } - - if (!count_of_hospitals && !hospitals_cached) { - // No hospitals found, cache is empty, just return - goto cleanup; - } - - // Now walk our list of known hospitals, do a bit of checking, then compare - // TODO: this doesn't handle zone resizes at all - - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - // If a zone is changed at all, DF seems to reallocate it. - // - // Each AnimalHospital has a "to_be_deleted" bool. We're going to set that to true, and clear it if we can't - // find a matching hospital. This limits the number of times we need to walk through the AHZ list to twice, and - // lets us cleanly report it later - // - // Surviving hospitals will be copied to scratch which will become the new AHZ vector - - animal_hospital->to_be_deleted = true; - for (df::building *current_hospital : hospitals_on_map) { - - /* Keep the hospital if its still valid */ - if (animal_hospital->getID() == current_hospital->id) { - ahz_scratch.push_back(animal_hospital); - animal_hospital->to_be_deleted = false; - break; - } - - } - } - - // Report what we're deleting by checking the to_be_deleted bool. - // - // Whatsever left is added to the pending add list - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - if (animal_hospital->to_be_deleted) { - out.print("Hospital #%d removed\n", animal_hospital->getID()); - delete animal_hospital; - } - } +static bool call_dwarfvet_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling dwarfvet lua function: '%s'\n", fn_name); - /* Now we need to walk the scratch and add anything that is a hospital and wasn't in the vector */ + CoreSuspender guard; - for (df::building *current_hospital : hospitals_on_map) { - bool new_hospital = true; - - for (AnimalHospital *animal_hospital : ahz_scratch) { - if (animal_hospital->getID() == current_hospital->id) { - // Next if we're already here - new_hospital = false; - break; - } - } - - // Add it if its new - if (new_hospital == true) to_be_added.push_back(current_hospital); - } - - /* Now add it to the scratch AHZ */ - for (df::building *current_hospital : to_be_added) { - // Add it to the vector - out.print("Adding new hospital #id: %d at x1 %d y1: %d z: %d\n", - current_hospital->id, - current_hospital->x1, - current_hospital->y1, - current_hospital->z - ); - AnimalHospital * hospital = new AnimalHospital(current_hospital, out); - ahz_scratch.push_back(hospital); - } + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - /* Copy the scratch to the AHZ */ - animal_hospital_zones = ahz_scratch; + if (!out) + out = &Core::getInstance().getConsole(); - // We always recheck the cache instead of counts because someone might have removed then added a hospital -/* if (hospitals_cached != count_of_hospitals) { - out.print("Hospitals on the map changed, rebuilding cache\n"); - - for (vector::iterator current_hospital = hospitals_on_map.begin(); current_hospital != hospitals_on_map.end(); current_hospital++) { - bool add_hospital = true; - - for (vector::iterator map_hospital = animal_hospital_zones.begin(); map_hospital != animal_hospital_zones.end(); map_hospital++) { - if (compareAnimalHospitalZones(*map_hospital, *current_hospital)) { - // Same hospital, we're good - add_hospital = false; - break; - } - } - - // Add it to the list - if (add_hospital) { - out.print("Adding zone at x1: %d, y1: %d to valid hospital list\n", (*current_hospital)->x1, (*current_hospital)->y1); - animal_hospital_zones.push_back(*current_hospital); - } - } - } -*/ -processUnits: - /* Code borrowed from petcapRemover.cpp */ - for (df::unit *unit : df::unit::get_vector()) { - /* As hilarious as it would be, lets not treat FB :) */ - if ( !Units::isActive(unit) || unit->flags1.bits.active_invader || unit->flags2.bits.underworld || unit->flags2.bits.visitor_uninvited || unit->flags2.bits.visitor ) { - continue; - } - - if ( !Units::isTamable(unit)) { - continue; - } - - /** - * So, for a unit to be elligable for the hospital, all the following must be true - * - * 1. It must be a member of our civilization - * 2. It must be tame (semi-wild counts for this) - * 2.1 If its not a dwarf, AND untame clear its civ out so traps work - * 3. It must have a health struct (which is generated by combat) - * 4. health->needs_healthcare must be set to true - * 5. If health->requires_recovery is set, the creature can't move under its own power - * and a Recover Wounded or Pen/Pasture job MUST be created by hand - TODO - * 6. An open spot in the "Animal Hospital" (activity zone with hospital+animal training set) - * must be available - * - * I apologize if this excessively verbose, but the healthcare system is stupidly conplex - * and there's tons of edgecases to watch out for, and I want someone else to ACTUALLY - * beside me able to understand what's going on - */ - - // 1. Make sure its our own civ - if (!Units::isOwnCiv(unit)) { - continue; - } - - // 2. Check for tameness - if (unit->training_level == df::animal_training_level::WildUntamed) { - // We don't IMMEDIATELY continue here, if the unit is - // part of our civ, it indiciates it WAS tamed, and reverted - // from SemiWild. Clear its civ flag so it looses TRAPAVOID - // - // Unfortunately, dwarves (or whatever is CIV_SELECTABLE) - // also have a default taming level of WildUntamed so - // check for this case - // - // Furthermore, it MIGHT be a werebeast, so check THAT too - // and exclude those as well. - // - // Finally, this breaks makeown. I might need to write a patch - // to set the tameness of "makeowned" units so dwarfvet can notice - // it - - if (unit->race == own_race_id || unit->enemy.normal_race == own_race_id) { - continue; - } else { - unit->civ_id = -1; - out.print ("Clearing civ on unit: %d", unit->id); - } - } - - // 3. Check for health struct - if (!unit->health) { - // Unit has not been injured ever; health struct MIA - continue; - } - - // 4. Check the healthcare flags - if (unit->health->flags.bits.needs_healthcare) { - /** - * So, for dwarves to care for a unit it must be resting in - * in a hospital zone. Since non-dwarves never take jobs - * this why animal healthcare doesn't work for animals despite - * animal caretaker being coded in DF itself - * - * How a unit gets there is dependent on several factors. If - * a unit can move under its own power, it will take the rest - * job, with a position of a bed in the hospital, then move - * into that bed and fall asleep. This triggers a doctor to - * treat the unit. - * - * If a unit *can't* move, it will set needs_recovery, which - * creates a "Recover Wounded" job in the job list, and then - * create the "Rest" job as listed above. Another dwarf with - * the right labors will go recover the unit, then the above - * logic kicks off. - * - * The necessary flags seem to be properly set for all units - * on the map, so in theory, we just need to make the jobs and - * we're in business, but from a realism POV, I don't think - * non-sentient animals would be smart enough to go to the - * hospital on their own, so instead, we're going to do the following - * - * If a unit CAN_THINK, and can move let it act like a dwarf, - * it will try and find an open spot in the hospital, and if so, - * go there to be treated. In vanilla, the only tamable animal - * with CAN_THINK are Gremlins, so this is actually an edge case - * but its the easiest to code. - * - * TODO: figure out exact logic for non-thinking critters. - */ - - // Now we need to find if this unit can be accepted at a hospital - bool awareOfUnit = tracked_units.count(unit->id); - // New unit for dwarfvet to be aware of! - if (!awareOfUnit) { - // The master list handles all patients which are accepted - // Check if this is a unit we're already aware of - - for (auto animal_hospital : animal_hospital_zones) { - if (animal_hospital->acceptPatient(unit->id, out)) { - out.print("Accepted patient %d at hospital %d\n", unit->id, animal_hospital->getID()); - tracked_units.insert(unit->id); - break; - } - } - } - } - } - - // The final step, process all patients! - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - animal_hospital->calculateHospital(true, out); - animal_hospital->processPatients(out); - } - -cleanup: - EventManager::unregisterAll(plugin_self); - EventManager::EventHandler handle(tickHandler, howOften); - EventManager::registerTick(handle, howOften, plugin_self); + return Lua::CallLuaModuleFunction(*out, L, "plugins.dwarfvet", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } -command_result dwarfvet (color_ostream &out, std::vector & parameters) -{ +static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; - for ( size_t a = 0; a < parameters.size(); a++ ) { - if ( parameters[a] == "enable" ) { - out.print("dwarfvet enabled!\n"); - dwarfvet_enabled = true; - } - if ( parameters[a] == "disable") { - out.print("dwarvet disabled!\n"); - dwarfvet_enabled = false; - } - if ( parameters[a] == "report") { - out.print("Current animal hospitals are:\n"); - for (df::building *building : df::building::get_vector()) { - if (isActiveAnimalHospital(building)) { - out.print(" at x1: %d, x2: %d, y1: %d, y2: %d, z: %d\n", building->x1, building->x2, building->y1, building->y2, building->z); - } - } - return CR_OK; - } - if ( parameters[a] == "report-usage") { - out.print("Current animal hospitals are:\n"); - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - animal_hospital->calculateHospital(true, out); - animal_hospital->reportUsage(out); - } - return CR_OK; - } + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - if ( !dwarfvet_enabled ) { - return CR_OK; + bool show_help = false; + if (!call_dwarfvet_lua(&out, "parse_commandline", 1, 1, + [&](lua_State *L) { + Lua::PushVector(L, parameters); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } - EventManager::unregisterAll(plugin_self); - EventManager::EventHandler handle(tickHandler, howOften); - EventManager::registerTick(handle, howOften, plugin_self); - - return CR_OK; + return show_help ? CR_WRONG_USAGE : CR_OK; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable && !dwarfvet_enabled) { - dwarfvet_enabled = true; - } - else if (!enable && dwarfvet_enabled) { - delete_animal_hospital_vector(out); - dwarfvet_enabled = false; - } +static void dwarfvet_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; - return CR_OK; + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + call_dwarfvet_lua(&out, "checkup"); } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) - { - case DFHack::SC_MAP_LOADED: - break; - case DFHack::SC_MAP_UNLOADED: - delete_animal_hospital_vector(out); - dwarfvet_enabled = false; - break; - default: - break; - } - return CR_OK; -} +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(dwarfvet_cycle), + DFHACK_LUA_END +}; diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index 6501f7f21..9a76794c2 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -1805,7 +1805,7 @@ void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist:: embark_assist::finder_ui::ui_setup(find_callback, max_inorganic); } if (!fileresult) { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } else { diff --git a/plugins/embark-assistant/help_ui.cpp b/plugins/embark-assistant/help_ui.cpp index a49a9b5e0..804617d92 100644 --- a/plugins/embark-assistant/help_ui.cpp +++ b/plugins/embark-assistant/help_ui.cpp @@ -411,5 +411,5 @@ namespace embark_assist{ //=============================================================================== void embark_assist::help_ui::init(DFHack::Plugin *plugin_self) { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } diff --git a/plugins/embark-tools.cpp b/plugins/embark-tools.cpp index 924def798..19e4ef7b6 100644 --- a/plugins/embark-tools.cpp +++ b/plugins/embark-tools.cpp @@ -694,7 +694,7 @@ struct choose_start_site_hook : df::viewscreen_choose_start_sitest void display_settings() { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } inline bool is_valid_page() diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 136ad7a9d..ef2ca9422 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -103,7 +103,7 @@ static void find_active_keybindings(color_ostream &out, df::viewscreen *screen, } for (int i = 1; i <= 12; i++) { - valid_keys.push_back("F" + int_to_string(i)); + valid_keys.push_back('F' + int_to_string(i)); } valid_keys.push_back("`"); diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index d62ee8c36..d4d2f0afe 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -44,6 +44,7 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_TRADE = 2, STOCKPILE_CONFIG_DUMP = 3, STOCKPILE_CONFIG_TRAIN = 4, + STOCKPILE_CONFIG_MELT_MASTERWORKS = 5, }; static int get_config_val(PersistentDataItem& c, int index) { @@ -81,6 +82,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock set_config_bool(c, STOCKPILE_CONFIG_TRADE, false); set_config_bool(c, STOCKPILE_CONFIG_DUMP, false); set_config_bool(c, STOCKPILE_CONFIG_TRAIN, false); + set_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS, false); return c; } @@ -122,6 +124,7 @@ static df::building_stockpilest* find_stockpile(int32_t stockpile_number) { static void validate_stockpile_configs(color_ostream& out, unordered_map &cache) { + vector to_remove; for (auto& entry : watched_stockpiles) { int stockpile_number = entry.first; PersistentDataItem &c = entry.second; @@ -129,13 +132,15 @@ static void validate_stockpile_configs(color_ostream& out, if (!bld || ( !get_config_bool(c, STOCKPILE_CONFIG_MELT) && !get_config_bool(c, STOCKPILE_CONFIG_TRADE) && - !get_config_bool(c, STOCKPILE_CONFIG_DUMP) && - !get_config_bool(c, STOCKPILE_CONFIG_TRAIN))) { - remove_stockpile_config(out, stockpile_number); + !get_config_bool(c, STOCKPILE_CONFIG_DUMP) && + !get_config_bool(c, STOCKPILE_CONFIG_TRAIN))) { + to_remove.push_back(stockpile_number); continue; } cache.emplace(bld, c); } + for (int stockpile_number : to_remove) + remove_stockpile_config(out, stockpile_number); } // remove this function once saves from 50.08 are no longer compatible @@ -256,8 +261,8 @@ public: class MeltStockProcessor : public StockProcessor { public: - MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats) - : StockProcessor("melt", stockpile_number, enabled, stats) { } + MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats, bool melt_masterworks) + : StockProcessor("melt", stockpile_number, enabled, stats), melt_masterworks(melt_masterworks) { } bool is_designated(color_ostream &out, df::item *item) override { return item->flags.bits.melt; @@ -291,7 +296,9 @@ public: } } - if (item->getQuality() >= df::item_quality::Masterful) + if (!melt_masterworks && item->getQuality() >= df::item_quality::Masterful) + return false; + if (item->flags.bits.artifact) return false; return true; @@ -302,6 +309,9 @@ public: item->flags.bits.melt = 1; return true; } + + private: + const bool melt_masterworks; }; class TradeStockProcessor: public StockProcessor { @@ -317,48 +327,26 @@ public: } bool can_designate(color_ostream& out, df::item* item) override { - return Items::canTradeWithContents(item); + return Items::canTradeAnyWithContents(item); } bool designate(color_ostream& out, df::item* item) override { if (!depot) return false; - - auto href = df::allocate(); - if (!href) - return false; - - auto job = new df::job(); - job->job_type = df::job_type::BringItemToDepot; - job->pos = df::coord(depot->centerx, depot->centery, depot->z); - - // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { - delete job; - delete href; - return false; - } - - // job <-> building link - href->building_id = depot->id; - depot->jobs.push_back(job); - job->general_refs.push_back(href); - - // add to job list - Job::linkIntoWorld(job); - - return true; + return Items::markForTrade(item, depot); } private: df::building_tradedepotst * const depot; static df::building_tradedepotst * get_active_trade_depot() { - // at least one caravan must be approaching or ready to trade + // at least one non-tribute caravan must be approaching or ready to trade if (!plotinfo->caravans.size()) return NULL; bool found = false; for (auto caravan : plotinfo->caravans) { + if (caravan->flags.bits.tribute) + continue; auto trade_state = caravan->trade_state; auto time_remaining = caravan->time_remaining; if ((trade_state == df::caravan_state::T_trade_state::Approaching || @@ -411,13 +399,14 @@ public: bool is_designated(color_ostream& out, df::item* item) override { auto unit = get_caged_unit(item); - return unit && has_training_assignment(unit); + return unit && Units::isMarkedForTraining(unit); } bool can_designate(color_ostream& out, df::item* item) override { auto unit = get_caged_unit(item); - return unit && Units::isTamable(unit) && !Units::isTame(unit) - && !has_training_assignment(unit); + return unit && !Units::isInvader(unit) && + Units::isTamable(unit) && !Units::isTame(unit) && + !Units::isMarkedForTraining(unit); } bool designate(color_ostream& out, df::item* item) override { @@ -442,11 +431,6 @@ private: return NULL; return gref->getUnit(); } - - static bool has_training_assignment(df::unit* unit) { - return binsearch_index(df::global::plotinfo->equipment.training_assignments, - &df::training_assignment::animal_id, unit->id) > -1; - } }; static const struct BadFlags { @@ -542,11 +526,12 @@ static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_cou int32_t stockpile_number = bld->stockpile_number; bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool melt_masterworks = get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS); bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN); - MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats); + MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); DumpStockProcessor dump_stock_processor(stockpile_number, dump, dump_stats); TrainStockProcessor train_stock_processor(stockpile_number, train, train_stats); @@ -578,7 +563,7 @@ static int logistics_getStockpileData(lua_State *L) { for (auto bld : df::global::world->buildings.other.STOCKPILE) { int32_t stockpile_number = bld->stockpile_number; - MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats); + MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats, false); TradeStockProcessor trade_stock_processor(stockpile_number, false, trade_stats); DumpStockProcessor dump_stock_processor(stockpile_number, false, dump_stats); TrainStockProcessor train_stock_processor(stockpile_number, false, train_stats); @@ -604,12 +589,14 @@ static int logistics_getStockpileData(lua_State *L) { PersistentDataItem &c = entry.second; bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool melt_masterworks = get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS); bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN); unordered_map config; config.emplace("melt", melt ? "true" : "false"); + config.emplace("melt_masterworks", melt_masterworks ? "true" : "false"); config.emplace("trade", trade ? "true" : "false"); config.emplace("dump", dump ? "true" : "false"); config.emplace("train", train ? "true" : "false"); @@ -656,11 +643,13 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) if (watched_stockpiles.count(stockpile_number)) { PersistentDataItem &c = watched_stockpiles[stockpile_number]; stockpile_config.emplace("melt", get_config_bool(c, STOCKPILE_CONFIG_MELT)); + stockpile_config.emplace("melt_masterworks", get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS)); stockpile_config.emplace("trade", get_config_bool(c, STOCKPILE_CONFIG_TRADE)); stockpile_config.emplace("dump", get_config_bool(c, STOCKPILE_CONFIG_DUMP)); stockpile_config.emplace("train", get_config_bool(c, STOCKPILE_CONFIG_TRAIN)); } else { stockpile_config.emplace("melt", false); + stockpile_config.emplace("melt_masterworks", false); stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); @@ -689,9 +678,9 @@ static int logistics_getStockpileConfigs(lua_State *L) { return 1; } -static void logistics_setStockpileConfig(color_ostream& out, int stockpile_number, bool melt, bool trade, bool dump, bool train) { - DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d\n", - stockpile_number, melt, trade, dump, train); +static void logistics_setStockpileConfig(color_ostream& out, int stockpile_number, bool melt, bool trade, bool dump, bool train, bool melt_masterworks) { + DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d, melt_masterworks=%d\n", + stockpile_number, melt, trade, dump, train, melt_masterworks); if (!find_stockpile(stockpile_number)) { out.printerr("invalid stockpile number: %d\n", stockpile_number); @@ -700,6 +689,7 @@ static void logistics_setStockpileConfig(color_ostream& out, int stockpile_numbe auto &c = ensure_stockpile_config(out, stockpile_number); set_config_bool(c, STOCKPILE_CONFIG_MELT, melt); + set_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS, melt_masterworks); set_config_bool(c, STOCKPILE_CONFIG_TRADE, trade); set_config_bool(c, STOCKPILE_CONFIG_DUMP, dump); set_config_bool(c, STOCKPILE_CONFIG_TRAIN, train); diff --git a/plugins/lua/autobutcher.lua b/plugins/lua/autobutcher.lua index f357f8fb1..51417bf9f 100644 --- a/plugins/lua/autobutcher.lua +++ b/plugins/lua/autobutcher.lua @@ -19,7 +19,7 @@ end local function process_args(opts, args) if args[1] == 'help' then opts.help = true - return + return {} end return argparse.processArgsGetopt(args, { @@ -67,7 +67,7 @@ function parse_commandline(opts, ...) process_races(opts, positionals, 6) elseif command == 'ticks' then local ticks = tonumber(positionals[2]) - if not is_positive_int(arg) then + if not is_positive_int(ticks) then qerror('number of ticks must be a positive integer: ' .. ticks) else opts.ticks = ticks diff --git a/plugins/lua/autochop.lua b/plugins/lua/autochop.lua index cda91b32d..239db9ec8 100644 --- a/plugins/lua/autochop.lua +++ b/plugins/lua/autochop.lua @@ -51,7 +51,7 @@ function parse_commandline(...) local args, opts = {...}, {} local positionals = process_args(opts, args) - if opts.help then + if opts.help or not positionals then return false end diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 57dee31fc..6aa154d2d 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -207,7 +207,7 @@ end -- returns the name of the output file for the given context function get_filename(opts, phase, ordinal) local fullname = 'dfhack-config/blueprints/' .. opts.name - local _,_,basename = fullname:find('/([^/]+)/?$') + local _,_,basename = opts.name:find('([^/]+)/*$') if not basename then -- should not happen since opts.name should already be validated error(('could not parse basename out of "%s"'):format(fullname)) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d64317eb0..7e034b988 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -14,6 +14,7 @@ local _ENV = mkmodule('plugins.buildingplan') local argparse = require('argparse') local inspector = require('plugins.buildingplan.inspectoroverlay') +local mechanisms = require('plugins.buildingplan.mechanisms') local pens = require('plugins.buildingplan.pens') local planner = require('plugins.buildingplan.planneroverlay') require('dfhack.buildings') @@ -134,12 +135,14 @@ function reload_modules() reload('plugins.buildingplan.itemselection') reload('plugins.buildingplan.planneroverlay') reload('plugins.buildingplan.inspectoroverlay') + reload('plugins.buildingplan.mechanisms') reload('plugins.buildingplan') end OVERLAY_WIDGETS = { planner=planner.PlannerOverlay, inspector=inspector.InspectorOverlay, + mechanisms=mechanisms.MechanismOverlay, } return _ENV diff --git a/plugins/lua/buildingplan/inspectoroverlay.lua b/plugins/lua/buildingplan/inspectoroverlay.lua index 5262eccc8..3c6f0ed5e 100644 --- a/plugins/lua/buildingplan/inspectoroverlay.lua +++ b/plugins/lua/buildingplan/inspectoroverlay.lua @@ -124,19 +124,19 @@ local function mouse_is_over_resume_button(rect) end function InspectorOverlay:onInput(keys) - if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding(true)) then return false end - if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then + if keys._MOUSE_L and mouse_is_over_resume_button(self.frame_parent_rect) then return true - elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + elseif keys._MOUSE_L or keys._MOUSE_R or keys.LEAVESCREEN then self:reset() end return InspectorOverlay.super.onInput(self, keys) end function InspectorOverlay:render(dc) - if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding(true)) then return end if reset_inspector_flag then diff --git a/plugins/lua/buildingplan/itemselection.lua b/plugins/lua/buildingplan/itemselection.lua index 84e866502..bc836bc33 100644 --- a/plugins/lua/buildingplan/itemselection.lua +++ b/plugins/lua/buildingplan/itemselection.lua @@ -21,6 +21,29 @@ function get_automaterial_selection(building_type) return tracker.list[#tracker.list] end +local function get_artifact_name(item) + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local artifact = df.artifact_record.find(gref.artifact_id) + if not artifact then return end + return dfhack.TranslateName(artifact.name) +end + +function get_item_description(item_id, item, safety_label) + item = item or df.item.find(item_id) + if not item then + return ('No %s safe mechanisms available'):format(safety_label:lower()) + end + local desc = item.flags.artifact and get_artifact_name(item) or + dfhack.items.getDescription(item, 0, true) + local wear_level = item:getWear() + if wear_level == 1 then desc = ('x%sx'):format(desc) + elseif wear_level == 2 then desc = ('X%sX'):format(desc) + elseif wear_level == 3 then desc = ('XX%sXX'):format(desc) + end + return desc +end + local function sort_by_type(a, b) local ad, bd = a.data, b.data return ad.item_type < bd.item_type or @@ -57,7 +80,7 @@ ItemSelection.ATTRS{ frame_title='Choose items', frame={w=56, h=24, l=4, t=7}, resizable=true, - index=DEFAULT_NIL, + get_available_items_fn=DEFAULT_NIL, desc=DEFAULT_NIL, quantity=DEFAULT_NIL, autoselect=DEFAULT_NIL, @@ -99,9 +122,9 @@ function ItemSelection:init() text_pen=BUILD_TEXT_PEN, text_hpen=BUILD_TEXT_HPEN, text={ - ' Use filter ', NEWLINE, - ' for remaining ', NEWLINE, - ' items ', + ' ', NEWLINE, + ' Autoselect ', NEWLINE, + ' ', }, on_click=self:callback('submit'), visible=function() return self.num_selected < self.quantity end, @@ -151,7 +174,6 @@ function ItemSelection:init() widgets.FilteredList{ view_id='flist', frame={t=0, b=0}, - case_sensitive=false, choices=choices, icon_width=2, on_submit=self:callback('toggle_group'), @@ -239,13 +261,12 @@ local function make_search_key(str) end function ItemSelection:get_choices(sort_fn) - local item_ids = require('plugins.buildingplan').getAvailableItems(uibs.building_type, - uibs.building_subtype, uibs.custom_type, self.index-1) + local item_ids = self.get_available_items_fn() local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) if not item then goto continue end - local desc = dfhack.items.getDescription(item, 0, true) + local desc = get_item_description(item_id, item) if buckets[desc] then local bucket = buckets[desc] table.insert(bucket.data.item_ids, item_id) @@ -366,10 +387,10 @@ function ItemSelection:submit(choices) end function ItemSelection:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then self.on_cancel() return true - elseif keys._MOUSE_L_DOWN then + elseif keys._MOUSE_L then local list = self.subviews.flist.list local idx = list:getIdxUnderMouse() if idx then @@ -397,7 +418,7 @@ ItemSelectionScreen.ATTRS { pass_pause=false, pass_mouse_clicks=false, defocusable=false, - index=DEFAULT_NIL, + get_available_items_fn=DEFAULT_NIL, desc=DEFAULT_NIL, quantity=DEFAULT_NIL, autoselect=DEFAULT_NIL, @@ -408,7 +429,7 @@ ItemSelectionScreen.ATTRS { function ItemSelectionScreen:init() self:addviews{ ItemSelection{ - index=self.index, + get_available_items_fn=self.get_available_items_fn, desc=self.desc, quantity=self.quantity, autoselect=self.autoselect, diff --git a/plugins/lua/buildingplan/mechanisms.lua b/plugins/lua/buildingplan/mechanisms.lua new file mode 100644 index 000000000..163255ca3 --- /dev/null +++ b/plugins/lua/buildingplan/mechanisms.lua @@ -0,0 +1,172 @@ +local _ENV = mkmodule('plugins.buildingplan.mechanisms') + +local itemselection = require('plugins.buildingplan.itemselection') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local view_sheets = df.global.game.main_interface.view_sheets + +-------------------------------- +-- MechanismOverlay +-- + +MechanismOverlay = defclass(MechanismOverlay, overlay.OverlayWidget) +MechanismOverlay.ATTRS{ + default_pos={x=5,y=5}, + default_enabled=true, + viewscreens='dwarfmode/LinkingLever', + frame={w=57, h=13}, +} + +function MechanismOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={t=5, l=1, r=1, h=1}, + subviews={ + widgets.Label{ + frame={t=0, l=1}, + text='Mechanism safety:' + }, + widgets.CycleHotkeyLabel{ + view_id='safety_lever', + frame={t=0, l=20, w=15}, + key='CUSTOM_G', + label='Lever:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + initial_option=0, + on_change=self:callback('choose_mechanism', 'lever', true), + }, + widgets.CycleHotkeyLabel{ + view_id='safety_target', + frame={t=0, l=38, w=16}, + key='CUSTOM_SHIFT_G', + label='Target:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + initial_option=0, + on_change=self:callback('choose_mechanism', 'target', true), + }, + } + }, + widgets.HotkeyLabel{ + frame={t=7, l=8, w=49, h=2}, + key='CUSTOM_M', + label=function() + return itemselection.get_item_description(view_sheets.linking_lever_mech_lever_id, + nil, + self.subviews.safety_lever:getOptionLabel()) + end, + auto_height=false, + enabled=function() return view_sheets.linking_lever_mech_lever_id ~= -1 end, + on_activate=self:callback('choose_mechanism', 'lever', false), + }, + widgets.HotkeyLabel{ + frame={t=10, l=8, w=49, h=2}, + key='CUSTOM_SHIFT_M', + label=function() + return itemselection.get_item_description(view_sheets.linking_lever_mech_target_id, + nil, + self.subviews.safety_target:getOptionLabel()) + end, + auto_height=false, + enabled=function() return view_sheets.linking_lever_mech_target_id ~= -1 end, + on_activate=self:callback('choose_mechanism', 'target', false), + }, + } +end + +local item_selection_dlg +local function reset_dlg() + if item_selection_dlg then + if item_selection_dlg:isActive() then + item_selection_dlg:dismiss() + end + item_selection_dlg = nil + end +end + +local function get_available_items(safety, other_mechanism) + local item_ids = require('plugins.buildingplan').getAvailableItemsByHeat( + df.building_type.Trap, df.trap_type.Lever, -1, 0, safety) + for idx,item_id in ipairs(item_ids) do + if item_id == other_mechanism then + table.remove(item_ids, idx) + break + end + end + return item_ids +end + +function MechanismOverlay:save_id(which, item_id) + local saved_id = ('saved_%s_id'):format(which) + local ui_id = ('linking_lever_mech_%s_id'):format(which) + view_sheets[ui_id] = item_id + self[saved_id] = item_id +end + +function MechanismOverlay:choose_mechanism(which, autoselect) + local widget_id = 'safety_' .. which + local safety = self.subviews[widget_id]:getOptionValue() + local ui_other_id = ('linking_lever_mech_%s_id'):format(which == 'lever' and 'target' or 'lever') + local available_item_ids = get_available_items(safety, view_sheets[ui_other_id]) + + if autoselect then + self:save_id(which, available_item_ids[1] or -1) + return + end + + -- to integrate with ItemSelection's last used sorting + df.global.buildreq.building_type = df.building_type.Trap + + local desc = self.subviews[widget_id]:getOptionLabel() + if desc ~= 'Any' then + desc = desc .. ' safe' + end + desc = desc .. ' mechanism' + + item_selection_dlg = item_selection_dlg or itemselection.ItemSelectionScreen{ + get_available_items_fn=function() return available_item_ids end, + desc=desc, + quantity=1, + autoselect=false, + on_cancel=reset_dlg, + on_submit=function(chosen_ids) + self:save_id(which, chosen_ids[1] or available_item_ids[1] -1) + reset_dlg() + end, + }:show() +end + +function MechanismOverlay:onInput(keys) + if MechanismOverlay.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L then + if self:getMousePos() then + -- don't let clicks bleed through the panel + return true + end + -- don't allow the lever to be linked if mechanisms are not set + return view_sheets.linking_lever_mech_lever_id == -1 or + view_sheets.linking_lever_mech_target_id == -1 + end +end + +function MechanismOverlay:onRenderFrame(dc, rect) + MechanismOverlay.super.onRenderFrame(self, dc, rect) + if self.saved_lever_id ~= view_sheets.linking_lever_mech_lever_id then + self:choose_mechanism('lever', true) + end + if self.saved_target_id ~= view_sheets.linking_lever_mech_target_id then + self:choose_mechanism('target', true) + end +end + +return _ENV diff --git a/plugins/lua/buildingplan/pens.lua b/plugins/lua/buildingplan/pens.lua index e69a4c210..2ab76da06 100644 --- a/plugins/lua/buildingplan/pens.lua +++ b/plugins/lua/buildingplan/pens.lua @@ -1,5 +1,8 @@ local _ENV = mkmodule('plugins.buildingplan.pens') +local gui = require('gui') +local textures = require('gui.textures') + GOOD_TILE_PEN, BAD_TILE_PEN = nil, nil VERT_TOP_PEN, VERT_MID_PEN, VERT_BOT_PEN = nil, nil, nil HORI_LEFT_PEN, HORI_MID_PEN, HORI_RIGHT_PEN = nil, nil, nil @@ -9,29 +12,21 @@ MINI_TEXT_PEN, MINI_TEXT_HPEN, MINI_BUTT_PEN, MINI_BUTT_HPEN = nil, nil, nil, ni local to_pen = dfhack.pen.parse -local tp = function(base, offset) - if base == -1 then return nil end - return base + offset -end - function reload_pens() GOOD_TILE_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} BAD_TILE_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} - local tb_texpos = dfhack.textures.getThinBordersTexposStart() - VERT_TOP_PEN = to_pen{tile=tp(tb_texpos, 10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} - VERT_MID_PEN = to_pen{tile=tp(tb_texpos, 4), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} - VERT_BOT_PEN = to_pen{tile=tp(tb_texpos, 11), ch=193, fg=COLOR_GREY, bg=COLOR_BLACK} + VERT_TOP_PEN = to_pen { tile = curry(textures.tp_border_thin, 11), ch = 194, fg = COLOR_GREY, bg = COLOR_BLACK } + VERT_MID_PEN = to_pen { tile = curry(textures.tp_border_thin, 5), ch = 179, fg = COLOR_GREY, bg = COLOR_BLACK } + VERT_BOT_PEN = to_pen { tile = curry(textures.tp_border_thin, 12), ch = 193, fg = COLOR_GREY, bg = COLOR_BLACK } - local mb_texpos = dfhack.textures.getMediumBordersTexposStart() - HORI_LEFT_PEN = to_pen{tile=tp(mb_texpos, 12), ch=195, fg=COLOR_GREY, bg=COLOR_BLACK} - HORI_MID_PEN = to_pen{tile=tp(mb_texpos, 5), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} - HORI_RIGHT_PEN = to_pen{tile=tp(mb_texpos, 13), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} + HORI_LEFT_PEN = to_pen { tile = curry(textures.tp_border_medium, 13), ch = 195, fg = COLOR_GREY, bg = COLOR_BLACK } + HORI_MID_PEN = to_pen { tile = curry(textures.tp_border_medium, 6), ch = 196, fg = COLOR_GREY, bg = COLOR_BLACK } + HORI_RIGHT_PEN = to_pen { tile = curry(textures.tp_border_medium, 14), ch = 180, fg = COLOR_GREY, bg = COLOR_BLACK } - local cp_texpos = dfhack.textures.getControlPanelTexposStart() - BUTTON_START_PEN = to_pen{tile=tp(cp_texpos, 13), ch='[', fg=COLOR_YELLOW} - BUTTON_END_PEN = to_pen{tile=tp(cp_texpos, 15), ch=']', fg=COLOR_YELLOW} - SELECTED_ITEM_PEN = to_pen{tile=tp(cp_texpos, 9), ch=string.char(251), fg=COLOR_YELLOW} + BUTTON_START_PEN = to_pen { tile = curry(textures.tp_control_panel, 14), ch = '[', fg = COLOR_YELLOW } + BUTTON_END_PEN = to_pen { tile = curry(textures.tp_control_panel, 16), ch = ']', fg = COLOR_YELLOW } + SELECTED_ITEM_PEN = to_pen { tile = curry(textures.tp_control_panel, 10), ch = string.char(251), fg = COLOR_YELLOW } MINI_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREY} MINI_TEXT_HPEN = to_pen{fg=COLOR_BLACK, bg=COLOR_WHITE} diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 803e9ae99..06872ea09 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -162,11 +162,27 @@ local function is_slab() return uibs.building_type == df.building_type.Slab end +local function is_cage() + return uibs.building_type == df.building_type.Cage +end + local function is_stairs() return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end +local function is_single_level_stairs() + if not is_stairs() then return false end + local _, _, dimz = get_cur_area_dims() + return dimz == 1 +end + +local function is_multi_level_stairs() + if not is_stairs() then return false end + local _, _, dimz = get_cur_area_dims() + return dimz > 1 +end + local direction_panel_frame = {t=4, h=13, w=46, r=28} local direction_panel_types = utils.invert{ @@ -272,7 +288,7 @@ function ItemLine:reset() end function ItemLine:onInput(keys) - if keys._MOUSE_L_DOWN and self:getMousePos() then + if keys._MOUSE_L and self:getMousePos() then self.on_select(self.idx) end return ItemLine.super.onInput(self, keys) @@ -290,10 +306,13 @@ function ItemLine:get_item_line_text() uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) if self.available >= quantity then self.note_pen = COLOR_GREEN - self.note = ' Available now' + self.note = (' %d available now'):format(self.available) + elseif self.available >= 0 then + self.note_pen = COLOR_BROWN + self.note = (' Will link next (need to make %d)'):format(quantity - self.available) else self.note_pen = COLOR_BROWN - self.note = ' Will link later' + self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "â””" @@ -314,7 +333,7 @@ function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) - self.available = math.max(0, self.available - used_quantity) + self.available = self.available - used_quantity end local function get_placement_errors() @@ -351,10 +370,10 @@ function PlannerOverlay:init() } local minimized_panel = widgets.Panel{ - frame={t=0, r=1, w=17, h=1}, + frame={t=0, r=1, w=20, h=1}, subviews={ widgets.Label{ - frame={t=0, r=0, h=1}, + frame={t=0, r=3, h=1}, text={ {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, @@ -363,7 +382,7 @@ function PlannerOverlay:init() on_click=self:callback('toggle_minimized'), }, widgets.Label{ - frame={t=0, r=0, h=1}, + frame={t=0, r=3, h=1}, text={ {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, @@ -371,6 +390,10 @@ function PlannerOverlay:init() visible=self:callback('is_not_minimized'), on_click=self:callback('toggle_minimized'), }, + widgets.HelpButton{ + frame={t=0, r=0}, + command='buildingplan', + } }, } @@ -424,10 +447,10 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={b=5, l=23, w=30}, + frame={b=7, l=1, w=30}, key='CUSTOM_R', - label='Top Stair Type: ', - visible=is_stairs, + label='Top stair type: ', + visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, @@ -436,16 +459,28 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={b=4, l=23, w=30}, + frame={b=6, l=1, w=30}, key='CUSTOM_B', label='Bottom Stair Type:', - visible=is_stairs, + visible=is_multi_level_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Up', value=df.construction_type.UpStair}, }, }, + widgets.CycleHotkeyLabel{ + view_id='stairs_only_subtype', + frame={b=7, l=1, w=30}, + key='CUSTOM_R', + label='Single level stair:', + visible=is_single_level_stairs, + options={ + {label='Up', value=df.construction_type.UpStair}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, widgets.CycleHotkeyLabel { -- TODO: this thing also needs a slider view_id='weapons', frame={b=4, l=1, w=28}, @@ -477,8 +512,18 @@ function PlannerOverlay:init() buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, + widgets.ToggleHotkeyLabel { + view_id='empty', + frame={b=4, l=1, w=22}, + key='CUSTOM_T', + label='Empty only:', + visible=is_cage, + on_change=function(val) + buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) + end, + }, widgets.Label{ - frame={b=2, l=23}, + frame={b=4, l=23}, text_pen=COLOR_DARKGREY, text={ 'Selected area: ', @@ -548,7 +593,6 @@ function PlannerOverlay:init() on_change=function(heat) buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, - visible=false, -- until we can make this work the way it's intended }, }, }, @@ -639,6 +683,7 @@ end function PlannerOverlay:toggle_minimized() self.state.minimized = not self.state.minimized config:write() + self:reset() end function PlannerOverlay:draw_divider_h(dc) @@ -740,7 +785,7 @@ end function PlannerOverlay:onInput(keys) if not is_plannable() then return false end - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then if uibs.selection_pos:isValid() then uibs.selection_pos:clear() return true @@ -759,7 +804,7 @@ function PlannerOverlay:onInput(keys) return true end if self:is_minimized() then return false end - if keys._MOUSE_L_DOWN then + if keys._MOUSE_L then if is_over_options_panel() then return false end local detect_rect = copyall(self.frame_rect) detect_rect.height = self.subviews.main.frame_rect.height + @@ -775,7 +820,9 @@ function PlannerOverlay:onInput(keys) local filters = get_cur_filters() local num_filters = #filters local choose = self.subviews.choose:getOptionValue() - if choose > 0 then + if choose == 0 then + self:place_building(get_placement_data()) + else local bounds = get_selected_bounds() self:save_placement() local autoselect = choose == 2 @@ -786,8 +833,12 @@ function PlannerOverlay:onInput(keys) for idx = num_filters,1,-1 do chosen_items[idx] = {} local filter = filters[idx] + local get_available_items_fn = function() + return require('plugins.buildingplan').getAvailableItems( + uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) + end local selection_screen = itemselection.ItemSelectionScreen{ - index=idx, + get_available_items_fn=get_available_items_fn, desc=require('plugins.buildingplan').get_desc(filter), quantity=get_quantity(filter, is_hollow, bounds), autoselect=autoselect, @@ -806,7 +857,7 @@ function PlannerOverlay:onInput(keys) end end, on_cancel=function() - for i,scr in pairs(active_screens) do + for _,scr in pairs(active_screens) do scr:dismiss() end df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT @@ -820,8 +871,6 @@ function PlannerOverlay:onInput(keys) active_screens[idx] = selection_screen:show() end end - else - self:place_building(get_placement_data()) end return true elseif not is_choosing_area() then @@ -829,7 +878,7 @@ function PlannerOverlay:onInput(keys) end end end - return keys._MOUSE_L or keys.SELECT + return keys._MOUSE_L_DOWN or keys.SELECT end function PlannerOverlay:render(dc) @@ -848,6 +897,8 @@ function PlannerOverlay:onRenderFrame(dc, rect) local buildingplan = require('plugins.buildingplan') self.subviews.engraved:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) + self.subviews.empty:setOption(buildingplan.getSpecials( + uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) self.subviews.choose:setOption(buildingplan.getChooseItems( uibs.building_type, uibs.building_subtype, uibs.custom_type)) self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( @@ -883,7 +934,8 @@ end function PlannerOverlay:get_stairs_subtype(pos, bounds) local subtype = uibs.building_subtype if pos.z == bounds.z1 then - local opt = self.subviews.stairs_bottom_subtype:getOptionValue() + local opt = bounds.z1 == bounds.z2 and self.subviews.stairs_only_subtype:getOptionValue() or + self.subviews.stairs_bottom_subtype:getOptionValue() if opt == 'auto' then local tt = dfhack.maps.getTileType(pos) local shape = df.tiletype.attrs[tt].shape diff --git a/plugins/lua/burrow.lua b/plugins/lua/burrow.lua new file mode 100644 index 000000000..42257dc50 --- /dev/null +++ b/plugins/lua/burrow.lua @@ -0,0 +1,228 @@ +local _ENV = mkmodule('plugins.burrow') + +local argparse = require('argparse') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +--------------------------------- +-- BurrowDesignationOverlay +-- + +local selection_rect = df.global.selection_rect +local if_burrow = df.global.game.main_interface.burrow + +local function is_choosing_area(pos) + return if_burrow.doing_rectangle and + selection_rect.start_z >= 0 and + (pos or dfhack.gui.getMousePos()) +end + +local function reset_selection_rect() + selection_rect.start_x = -30000 + selection_rect.start_y = -30000 + selection_rect.start_z = -30000 +end + +local function clamp(pos) + return xyz2pos( + math.max(0, math.min(df.global.world.map.x_count-1, pos.x)), + math.max(0, math.min(df.global.world.map.y_count-1, pos.y)), + math.max(0, math.min(df.global.world.map.z_count-1, pos.z))) +end + +local function get_bounds(pos1, pos2) + pos1 = clamp(pos1 or dfhack.gui.getMousePos(true)) + pos2 = clamp(pos2 or xyz2pos(selection_rect.start_x, selection_rect.start_y, selection_rect.start_z)) + return { + x1=math.min(pos1.x, pos2.x), + x2=math.max(pos1.x, pos2.x), + y1=math.min(pos1.y, pos2.y), + y2=math.max(pos1.y, pos2.y), + z1=math.min(pos1.z, pos2.z), + z2=math.max(pos1.z, pos2.z), + } +end + +local function get_cur_area_dims() + local bounds = get_bounds() + return bounds.x2 - bounds.x1 + 1, + bounds.y2 - bounds.y1 + 1, + bounds.z2 - bounds.z1 + 1 +end + +BurrowDesignationOverlay = defclass(BurrowDesignationOverlay, overlay.OverlayWidget) +BurrowDesignationOverlay.ATTRS{ + default_pos={x=6,y=9}, + viewscreens='dwarfmode/Burrow/Paint', + default_enabled=true, + frame={w=54, h=1}, +} + +function BurrowDesignationOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={t=0, l=0}, + subviews={ + widgets.Label{ + frame={t=0, l=1}, + text='Double-click to fill. Shift double-click to 3D fill.', + auto_width=true, + visible=function() return not is_choosing_area() end, + }, + widgets.Label{ + frame={t=0, l=1}, + text_pen=COLOR_DARKGREY, + text={ + '3D box select enabled: ', + {text=function() return ('%dx%dx%d'):format(get_cur_area_dims()) end}, + }, + visible=is_choosing_area, + }, + }, + }, + } +end + +local function flood_fill(pos, erasing, do_3d, painting_burrow) + local opts = {zlevel=not do_3d} + if erasing then + burrow_tiles_flood_remove(painting_burrow, pos, opts) + else + burrow_tiles_flood_add(painting_burrow, pos, opts) + end + reset_selection_rect() +end + +local function box_fill(bounds, erasing, painting_burrow) + if bounds.z1 == bounds.z2 then return end + if erasing then + burrow_tiles_box_remove(painting_burrow, bounds) + else + burrow_tiles_box_add(painting_burrow, bounds) + end +end + +function BurrowDesignationOverlay:onInput(keys) + if self:inputToSubviews(keys) then + return true + -- don't perform burrow modifications immediately -- painting_burrow may not yet + -- have been initialized. instead, allow clicks to go through so that vanilla + -- behavior is triggered before we modify the burrow further + elseif keys._MOUSE_L then + local pos = dfhack.gui.getMousePos(true) + if pos then + local now_ms = dfhack.getTickCount() + if not same_xyz(pos, self.saved_pos) then + self.last_click_ms = now_ms + self.saved_pos = pos + else + if now_ms - self.last_click_ms <= widgets.DOUBLE_CLICK_MS then + self.last_click_ms = 0 + self.pending_fn = curry(flood_fill, pos, if_burrow.erasing, dfhack.internal.getModifiers().shift) + return + else + self.last_click_ms = now_ms + end + end + if is_choosing_area(pos) then + self.pending_fn = curry(box_fill, get_bounds(pos), if_burrow.erasing) + return + end + end + end +end + +function BurrowDesignationOverlay:onRenderBody(dc) + BurrowDesignationOverlay.super.onRenderBody(self, dc) + local pending_fn = self.pending_fn + self.pending_fn = nil + if pending_fn and if_burrow.painting_burrow then + pending_fn(if_burrow.painting_burrow) + end +end + +OVERLAY_WIDGETS = { + designation=BurrowDesignationOverlay, +} + +rawset_default(_ENV, dfhack.burrows) + +--------------------------------- +-- commandline handling +-- + +local function set_add_remove(mode, which, params, _) + local target_burrow = table.remove(params, 1) + _ENV[('burrow_%s_%s'):format(mode, which)](target_burrow, params) +end + +local function tiles_box_add_remove(which, params, opts) + local target_burrow = table.remove(params, 1) + local pos1 = argparse.coords(params[1] or 'here', 'pos') + local pos2 = opts.cursor or argparse.coords(params[2] or 'here', 'pos') + local bounds = get_bounds(pos1, pos2) + _ENV['burrow_tiles_box_'..which](target_burrow, bounds) +end + +local function tiles_flood_add_remove(which, params, opts) + local target_burrow = table.remove(params, 1) + local pos = opts.cursor or argparse.coords('here', 'pos') + _ENV['burrow_tiles_flood_'..which](target_burrow, pos, opts) +end + +local function run_command(mode, command, params, opts) + if mode == 'tiles' then + if command == 'clear' then + burrow_tiles_clear(params) + elseif command == 'set' or command == 'add' or command == 'remove' then + set_add_remove('tiles', command, params, opts) + elseif command == 'box-add' or command == 'box-remove' then + tiles_box_add_remove(command:sub(5), params, opts) + elseif command == 'flood-add' or command == 'flood-remove' then + tiles_flood_add_remove(command:sub(7), params, opts) + else + return false + end + elseif mode == 'units' then + if command == 'clear' then + burrow_units_clear(params) + elseif command == 'set' or command == 'add' or command == 'remove' then + set_add_remove('units', command, params, opts) + else + return false + end + else + return false + end + return true +end + +function parse_commandline(...) + local args, opts = {...}, {} + + if args[1] == 'help' then + return false + end + + local positionals = argparse.processArgsGetopt(args, { + {'c', 'cursor', hasArg=true, + handler=function(optarg) opts.cursor = argparse.coords(optarg, 'cursor') end}, + {'h', 'help', handler=function() opts.help = true end}, + {'z', 'cur-zlevel', handler=function() opts.zlevel = true end}, + }) + + if opts.help then + return false + end + + local mode = table.remove(positionals, 1) + local command = table.remove(positionals, 1) + local ret = run_command(mode, command, positionals, opts) + + if not ret then return false end + + print('done') + return true +end + +return _ENV diff --git a/plugins/lua/burrows.lua b/plugins/lua/burrows.lua deleted file mode 100644 index 7c8753bf7..000000000 --- a/plugins/lua/burrows.lua +++ /dev/null @@ -1,23 +0,0 @@ -local _ENV = mkmodule('plugins.burrows') - ---[[ - - Native events: - - * onBurrowRename(burrow) - * onDigComplete(job_type,pos,old_tiletype,new_tiletype) - - Native functions: - - * findByName(name) -> burrow - * copyUnits(dest,src,enable) - * copyTiles(dest,src,enable) - * setTilesByKeyword(dest,kwd,enable) -> success - - 'enable' selects between add and remove modes - ---]] - -rawset_default(_ENV, dfhack.burrows) - -return _ENV diff --git a/plugins/lua/confirm.lua b/plugins/lua/confirm.lua index 1ddf190b4..2fcbfa721 100644 --- a/plugins/lua/confirm.lua +++ b/plugins/lua/confirm.lua @@ -82,7 +82,7 @@ haul_delete_stop.message = "Are you sure you want to delete this stop?" depot_remove = defconf('depot-remove') function depot_remove.intercept_key(key) - if df.global.game.main_interface.current_hover == 299 and + if df.global.game.main_interface.current_hover == 301 and key == MOUSE_LEFT and df.building_tradedepotst:is_instance(dfhack.gui.getSelectedBuilding(true)) then for _, caravan in pairs(df.global.plotinfo.caravans) do @@ -98,7 +98,7 @@ depot_remove.message = "Are you sure you want to remove this depot?\n" .. squad_disband = defconf('squad-disband') function squad_disband.intercept_key(key) - return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 341 + return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 343 end squad_disband.title = "Disband squad" squad_disband.message = "Are you sure you want to disband this squad?" diff --git a/plugins/lua/dig.lua b/plugins/lua/dig.lua new file mode 100644 index 000000000..e24885c08 --- /dev/null +++ b/plugins/lua/dig.lua @@ -0,0 +1,49 @@ +local _ENV = mkmodule('plugins.dig') + +local overlay = require('plugins.overlay') +local pathable = require('plugins.pathable') + +WarmDampOverlay = defclass(WarmDampOverlay, overlay.OverlayWidget) +WarmDampOverlay.ATTRS{ + viewscreens={ + 'dwarfmode/Designate/DIG_DIG', + 'dwarfmode/Designate/DIG_REMOVE_STAIRS_RAMPS', + 'dwarfmode/Designate/DIG_STAIR_UP', + 'dwarfmode/Designate/DIG_STAIR_UPDOWN', + 'dwarfmode/Designate/DIG_STAIR_DOWN', + 'dwarfmode/Designate/DIG_RAMP', + 'dwarfmode/Designate/DIG_CHANNEL', + 'dwarfmode/Designate/DIG_FROM_MARKER', + 'dwarfmode/Designate/DIG_TO_MARKER', + }, + default_enabled=true, + overlay_only=true, +} + +function WarmDampOverlay:onRenderFrame() + pathable.paintScreenWarmDamp() +end + +CarveOverlay = defclass(CarveOverlay, overlay.OverlayWidget) +CarveOverlay.ATTRS{ + viewscreens={ + 'dwarfmode/Designate/SMOOTH', + 'dwarfmode/Designate/ENGRAVE', + 'dwarfmode/Designate/TRACK', + 'dwarfmode/Designate/FORTIFY', + 'dwarfmode/Designate/ERASE', + }, + default_enabled=true, + overlay_only=true, +} + +function CarveOverlay:onRenderFrame() + pathable.paintScreenCarve() +end + +OVERLAY_WIDGETS = { + asciiwarmdamp=WarmDampOverlay, + asciicarve=CarveOverlay, +} + +return _ENV diff --git a/plugins/lua/dwarfvet.lua b/plugins/lua/dwarfvet.lua new file mode 100644 index 000000000..2bda976b7 --- /dev/null +++ b/plugins/lua/dwarfvet.lua @@ -0,0 +1,178 @@ +local _ENV = mkmodule('plugins.dwarfvet') + +local argparse = require('argparse') +local utils = require('utils') + +local function is_valid_animal(unit) + return unit and + dfhack.units.isActive(unit) and + dfhack.units.isAnimal(unit) and + dfhack.units.isFortControlled(unit) and + dfhack.units.isTame(unit) and + not dfhack.units.isDead(unit) +end + +local function get_cur_patients() + local cur_patients = {} + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type ~= df.job_type.Rest then goto continue end + local unit = dfhack.job.getWorker(job) + if is_valid_animal(unit) then + cur_patients[unit] = true + end + ::continue:: + end + return cur_patients +end + +local function get_new_patients(cur_patients) + cur_patients = cur_patients or get_cur_patients() + local new_patients = {} + for _,unit in ipairs(df.global.world.units.active) do + if unit.job.current_job then goto continue end + if cur_patients[unit] or not is_valid_animal(unit) then goto continue end + if not unit.health or not unit.health.flags.needs_healthcare then goto continue end + table.insert(new_patients, unit) + ::continue:: + end + return new_patients +end + +local function print_status() + print(('dwarfvet is %srunning'):format(isEnabled() and '' or 'not ')) + print() + print('The following animals are receiving treatment:') + local cur_patients = get_cur_patients() + if not next(cur_patients) then + print(' None') + else + for unit in pairs(cur_patients) do + print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id)) + end + end + print() + print('The following animals are injured and need treatment:') + local new_patients = get_new_patients(cur_patients) + if #new_patients == 0 then + print(' None') + else + for _,unit in ipairs(new_patients) do + print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id)) + end + end +end + +HospitalZone = defclass(HospitalZone) +HospitalZone.ATTRS{ + zone=DEFAULT_NIL, +} + +local ONE_TILE = xy2pos(1, 1) + +function HospitalZone:find_spot(unit_pos) + self.x = self.x or self.zone.x1 + self.y = self.y or self.zone.y1 + local zone = self.zone + for y=self.y,zone.y2 do + for x=self.x,zone.x2 do + local pos = xyz2pos(x, y, zone.z) + if dfhack.maps.canWalkBetween(unit_pos, pos) and + dfhack.buildings.containsTile(zone, x, y) and + dfhack.buildings.checkFreeTiles(pos, ONE_TILE) + then + return pos + end + end + end +end + +-- TODO: If health.requires_recovery is set, the creature can't move under its own power +-- and a Recover Wounded or Pen/Pasture job must be created by hand +function HospitalZone:assign_spot(unit, unit_pos) + local pos = self:find_spot(unit_pos) + if not pos then return false end + local job = df.new(df.job) + dfhack.job.linkIntoWorld(job, true) + job.pos.x = pos.x + job.pos.y = pos.y + job.pos.z = pos.z + job.flags.special = true + job.job_type = df.job_type.Rest + local gref = df.new(df.general_ref_unit_workerst) + gref.unit_id = unit.id + job.general_refs:insert('#', gref) + unit.job.current_job = job + return true +end + +local function get_hospital_zones() + local hospital_zones = {} + local site = df.global.world.world_data.active_site[0] + for _,location in ipairs(site.buildings) do + if not df.abstract_building_hospitalst:is_instance(location) then goto continue end + for _,bld_id in ipairs(location.contents.building_ids) do + local zone = df.building.find(bld_id) + if zone then + table.insert(hospital_zones, HospitalZone{zone=zone}) + end + end + ::continue:: + end + return hospital_zones +end + +local function distance(zone, pos) + return math.abs(zone.x1 - pos.x) + math.abs(zone.y1 - pos.y) + 50*math.abs(zone.z - pos.z) +end + +function checkup() + local new_patients = get_new_patients() + if #new_patients == 0 then return end + local hospital_zones = get_hospital_zones() + local assigned = 0 + for _,unit in ipairs(new_patients) do + local unit_pos = xyz2pos(dfhack.units.getPosition(unit)) + table.sort(hospital_zones, + function(a, b) return distance(a.zone, unit_pos) < distance(b.zone, unit_pos) end) + for _,hospital_zone in ipairs(hospital_zones) do + if hospital_zone:assign_spot(unit, unit_pos) then + assigned = assigned + 1 + break + end + end + end + print(('dwarfvet scheduled treatment for %d of %d injured animals'):format(assigned, #new_patients)) +end + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function parse_commandline(args) + local opts = {} + local positionals = process_args(opts, args) + + if opts.help or not positionals then + return false + end + + local command = positionals[1] + if not command or command == 'status' then + print_status() + elseif command == 'now' then + dwarfvet_cycle() + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index fca0f1f27..4c33f93ca 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -5,6 +5,9 @@ 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 function get_command(cmdline) local first_word = cmdline:trim():split(' +')[1] if first_word:startswith(':') then first_word = first_word:sub(2) end @@ -24,18 +27,20 @@ end HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) HotspotMenuWidget.ATTRS{ - default_pos={x=2,y=2}, + default_pos={x=5,y=1}, default_enabled=true, - hotspot=true, + version=2, viewscreens={ - -- 'choose_start_site', -- conflicts with vanilla panel layouts + 'adopt_region', 'choose_game_type', + -- 'choose_start_site', -- conflicts with vanilla panel layouts 'dwarfmode', 'export_region', 'game_cleaner', 'initial_prep', - 'legends', - 'loadgame', + -- 'legends', -- conflicts with vanilla export button and info text + -- 'loadgame', -- disable temporarily while we get texture reloading sorted + -- 'new_arena', -- conflicts with vanilla panel layouts -- 'new_region', -- conflicts with vanilla panel layouts 'savegame', 'setupdwarfgame', @@ -43,55 +48,50 @@ HotspotMenuWidget.ATTRS{ 'update_region', 'world' }, - overlay_onupdate_max_freq_seconds=0, frame={w=4, h=3} } function HotspotMenuWidget:init() - self.mouseover = false -end - -function HotspotMenuWidget:overlay_onupdate() - local hasMouse = self:getMousePos() - if hasMouse and not self.mouseover then - self.mouseover = true - return true + local to_pen = dfhack.pen.parse + local function tp(idx, ch) + return to_pen{ + tile=function() return dfhack.textures.getTexposByHandle(logo_textures[idx]) end, + ch=ch, + fg=COLOR_GREY, + } end - self.mouseover = hasMouse + local function tph(idx, ch) + return to_pen{ + tile=function() return dfhack.textures.getTexposByHandle(logo_hovered_textures[idx]) end, + ch=ch, + fg=COLOR_WHITE, + bold=true, + } + end + local function get_tile_token(idx, ch) + return { + tile=tp(idx, ch), + htile=tph(idx, ch), + width=1, + } + end + + self:addviews{ + widgets.Label{ + text={ + get_tile_token(1, 179), get_tile_token(2, 'D'), get_tile_token(3, 'F'), get_tile_token(4, 179), NEWLINE, + get_tile_token(5, 179), get_tile_token(6, 'H'), get_tile_token(7, 'a'), get_tile_token(8, 179), NEWLINE, + get_tile_token(9, 179), get_tile_token(10, 'c'), get_tile_token(11, 'k'), get_tile_token(12, 179), + }, + on_click=function() dfhack.run_command('hotkeys') end, + }, + } end function HotspotMenuWidget:overlay_trigger() return MenuScreen{hotspot=self}:show() end -local dscreen = dfhack.screen - -function HotspotMenuWidget:onRenderBody(dc) - local tpos = dfhack.textures.getDfhackLogoTexposStart() - local x, y = dc.x, dc.y - - if tpos == -1 then - dscreen.paintString(COLOR_WHITE, x, y+0, '!DF!') - dscreen.paintString(COLOR_WHITE, x, y+1, '!Ha!') - dscreen.paintString(COLOR_WHITE, x, y+2, '!ck!') - else - dscreen.paintTile(COLOR_WHITE, x+0, y+0, '!', tpos+0) - dscreen.paintTile(COLOR_WHITE, x+1, y+0, 'D', tpos+1) - dscreen.paintTile(COLOR_WHITE, x+2, y+0, 'F', tpos+2) - dscreen.paintTile(COLOR_WHITE, x+3, y+0, '!', tpos+3) - - dscreen.paintTile(COLOR_WHITE, x+0, y+1, '!', tpos+4) - dscreen.paintTile(COLOR_WHITE, x+1, y+1, 'H', tpos+5) - dscreen.paintTile(COLOR_WHITE, x+2, y+1, 'a', tpos+6) - dscreen.paintTile(COLOR_WHITE, x+3, y+1, '!', tpos+7) - - dscreen.paintTile(COLOR_WHITE, x+0, y+2, '!', tpos+8) - dscreen.paintTile(COLOR_WHITE, x+1, y+2, 'c', tpos+9) - dscreen.paintTile(COLOR_WHITE, x+2, y+2, 'k', tpos+10) - dscreen.paintTile(COLOR_WHITE, x+3, y+2, '!', tpos+11) - end -end - -- register the menu hotspot with the overlay OVERLAY_WIDGETS = {menu=HotspotMenuWidget} @@ -164,10 +164,16 @@ end function Menu:init() local hotkeys, bindings = getHotkeys() + if #hotkeys == 0 then + hotkeys = {''} + bindings = {['']='gui/launcher'} + end local is_inverted = not not self.hotspot.frame.b local choices,list_width = get_choices(hotkeys, bindings, is_inverted) + list_width = math.max(35, list_width) + local list_frame = copyall(self.hotspot.frame) local list_widget_frame = {h=math.min(#choices, MAX_LIST_HEIGHT)} local quickstart_frame = {} @@ -264,24 +270,22 @@ function Menu:onSubmit2(_, choice) end function Menu:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if keys.LEAVESCREEN or keys._MOUSE_R then return false - elseif keys.STANDARDSCROLL_RIGHT then + elseif keys.KEYBOARD_CURSOR_RIGHT then self:onSubmit2(self.subviews.list:getSelected()) return true - elseif keys._MOUSE_L_DOWN then + elseif keys._MOUSE_L then local list = self.subviews.list local x = list:getMousePos() if x == 0 then -- clicked on icon self:onSubmit2(list:getSelected()) - df.global.enabler.mouse_lbut = 0 return true end - if not self:getMouseFramePos() and not self.hotspot:getMousePos() then + if not self:getMouseFramePos() then self.parent_view:dismiss() return true end - df.global.enabler.mouse_lbut = 0 end self:inputToSubviews(keys) return true -- we're modal @@ -292,7 +296,7 @@ function Menu:onRenderFrame(dc, rect) self.initialize() self.initialize = nil end - Menu.super.onRenderFrame(dc, rect) + Menu.super.onRenderFrame(self, dc, rect) end function Menu:getMouseFramePos() @@ -301,7 +305,7 @@ function Menu:getMouseFramePos() end function Menu:onRenderBody(dc) - local panel = self.subviews.list_panel + Menu.super.onRenderBody(self, dc) local list = self.subviews.list local idx = list:getIdxUnderMouse() if idx and idx ~= self.last_mouse_idx then @@ -311,13 +315,6 @@ function Menu:onRenderBody(dc) list:setSelected(idx) self.last_mouse_idx = idx end - if self:getMouseFramePos() then - self.mouseover = true - elseif self.mouseover then - -- once the mouse has entered the list area, leaving the frame should - -- close the menu screen - self.parent_view:dismiss() - end end -- ---------- -- diff --git a/plugins/lua/logistics.lua b/plugins/lua/logistics.lua index 891332236..2f260cc59 100644 --- a/plugins/lua/logistics.lua +++ b/plugins/lua/logistics.lua @@ -29,6 +29,7 @@ function getStockpileData() trade=make_stat('trade', stockpile_number, stats, configs), dump=make_stat('dump', stockpile_number, stats, configs), train=make_stat('train', stockpile_number, stats, configs), + melt_masterworks=configs[stockpile_number] and configs[stockpile_number].melt_masterworks == 'true', }) end table.sort(data, function(a, b) return a.sort_key < b.sort_key end) @@ -41,16 +42,24 @@ local function print_stockpile_data(data) name_len = math.min(40, math.max(name_len, #sp.name)) end + local has_melt_mastworks = false + print('Designated/designatable items in stockpiles:') print() local fmt = '%6s %-' .. name_len .. 's %4s %10s %5s %11s %4s %10s %5s %11s'; print(fmt:format('number', 'name', 'melt', 'melt items', 'trade', 'trade items', 'dump', 'dump items', 'train', 'train items')) local function uline(len) return ('-'):rep(len) end print(fmt:format(uline(6), uline(name_len), uline(4), uline(10), uline(5), uline(11), uline(4), uline(10), uline(5), uline(11))) - local function get_enab(stats) return ('[%s]'):format(stats.enabled and 'x' or ' ') end + local function get_enab(stats, ch) return ('[%s]'):format(stats.enabled and (ch or 'x') or ' ') end local function get_dstat(stats) return ('%d/%d'):format(stats.designated, stats.designated + stats.can_designate) end for _,sp in ipairs(data) do - print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt), get_dstat(sp.melt), get_enab(sp.trade), get_dstat(sp.trade), get_enab(sp.dump), get_dstat(sp.dump), get_enab(sp.train), get_dstat(sp.train))) + has_melt_mastworks = has_melt_mastworks or sp.melt_masterworks + print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt, sp.melt_masterworks and 'X'), get_dstat(sp.melt), + get_enab(sp.trade), get_dstat(sp.trade), get_enab(sp.dump), get_dstat(sp.dump), get_enab(sp.train), get_dstat(sp.train))) + end + if has_melt_mastworks then + print() + print('An "X" in the "melt" column indicates that masterworks in the stockpile will be melted.') end end @@ -75,7 +84,7 @@ local function print_status() print(('Total items marked for melting: %5d'):format(global_stats.total_melt)) print(('Total items marked for trading: %5d'):format(global_stats.total_trade)) print(('Total items marked for dumping: %5d'):format(global_stats.total_dump)) - print(('Total animals marked for training: %5d'):format(global_stats.total_train)) + print(('Total animals marked for training: %2d'):format(global_stats.total_train)) end local function for_stockpiles(opts, fn) @@ -101,7 +110,8 @@ local function do_add_stockpile_config(features, opts) features.melt or config.melt == 1, features.trade or config.trade == 1, features.dump or config.dump == 1, - features.train or config.train == 1) + features.train or config.train == 1, + not not opts.melt_masterworks) end end end) @@ -125,6 +135,7 @@ local function process_args(opts, args) return argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, + {'m', 'melt-masterworks', handler=function() opts.melt_masterworks = true end}, {'s', 'stockpile', hasArg=true, handler=function(arg) opts.sp = arg end}, }) end diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 7a87e529f..d01ad79be 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -62,12 +62,16 @@ local function do_export() }:show() end +local function do_recheck() + dfhack.run_command('orders', 'recheck') +end + OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget) OrdersOverlay.ATTRS{ default_pos={x=53,y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=30, h=4}, + frame={w=43, h=4}, } function OrdersOverlay:init() @@ -95,13 +99,20 @@ function OrdersOverlay:init() }, widgets.HotkeyLabel{ frame={t=0, l=15}, + label='recheck conditions', + key='CUSTOM_CTRL_K', + auto_width=true, + on_activate=do_recheck, + }, + widgets.HotkeyLabel{ + frame={t=1, l=15}, label='sort', key='CUSTOM_CTRL_O', auto_width=true, on_activate=do_sort, }, widgets.HotkeyLabel{ - frame={t=1, l=15}, + frame={t=1, l=28}, label='clear', key='CUSTOM_CTRL_C', auto_width=true, @@ -111,7 +122,7 @@ function OrdersOverlay:init() } local minimized_panel = widgets.Panel{ - frame={t=0, r=0, w=3, h=1}, + frame={t=0, r=4, w=3, h=1}, subviews={ widgets.Label{ frame={t=0, l=0, w=1, h=1}, @@ -138,6 +149,11 @@ function OrdersOverlay:init() self:addviews{ main_panel, minimized_panel, + widgets.HelpButton{ + frame={t=0, r=1}, + command='orders', + visible=function() return not self.minimized end, + }, } end @@ -157,7 +173,91 @@ function OrdersOverlay:render(dc) OrdersOverlay.super.render(self, dc) end +-- Resets the selected work order to the `Checking` state + +local function set_current_inactive() + local scrConditions = df.global.game.main_interface.info.work_orders.conditions + if scrConditions.open then + dfhack.run_command('orders', 'recheck', 'this') + else + qerror("Order conditions is not open") + end +end + +local function can_recheck() + local scrConditions = df.global.game.main_interface.info.work_orders.conditions + local order = scrConditions.wq + return order.status.active and #order.item_conditions > 0 +end + +-- ------------------- +-- RecheckOverlay +-- + +local focusString = 'dwarfmode/Info/WORK_ORDERS/Conditions' + +RecheckOverlay = defclass(RecheckOverlay, overlay.OverlayWidget) +RecheckOverlay.ATTRS{ + default_pos={x=6,y=8}, + default_enabled=true, + viewscreens=focusString, + -- width is the sum of lengths of `[` + `Ctrl+A` + `: ` + button.label + `]` + frame={w=1 + 6 + 2 + 19 + 1, h=3}, +} + +local function areTabsInTwoRows() + -- get the tile above the order status icon + local pen = dfhack.screen.readTile(7, 7, false) + -- in graphics mode, `0` when one row, something else when two (`67` aka 'C' from "Creatures") + -- in ASCII mode, `32` aka ' ' when one row, something else when two (`196` aka '-' from tab frame's top) + return (pen.ch ~= 0 and pen.ch ~= 32) +end + +function RecheckOverlay:updateTextButtonFrame() + local twoRows = areTabsInTwoRows() + if (self._twoRows == twoRows) then return false end + + self._twoRows = twoRows + local frame = twoRows + and {b=0, l=0, r=0, h=1} + or {t=0, l=0, r=0, h=1} + self.subviews.button.frame = frame + + return true +end + +function RecheckOverlay:init() + self:addviews{ + widgets.TextButton{ + view_id = 'button', + -- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()` + label='re-check conditions', + key='CUSTOM_CTRL_A', + on_activate=set_current_inactive, + enabled=can_recheck, + }, + } + + self:updateTextButtonFrame() +end + +function RecheckOverlay:onRenderBody(dc) + if (self.frame_rect.y1 == 7) then + -- only apply this logic if the overlay is on the same row as + -- originally thought: just above the order status icon + + if self:updateTextButtonFrame() then + self:updateLayout() + end + end + + RecheckOverlay.super.onRenderBody(self, dc) +end + +-- ------------------- + OVERLAY_WIDGETS = { + recheck=RecheckOverlay, overlay=OrdersOverlay, } diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index fda1edc95..d8b3c4f81 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -261,7 +261,11 @@ local function load_widget(name, widget_class) next_update_ms=widget.overlay_onupdate and 0 or math.huge, } if not overlay_config[name] then overlay_config[name] = {} end + if widget.version ~= overlay_config[name].version then + overlay_config[name] = {} + end local config = overlay_config[name] + config.version = widget.version if config.enabled == nil then config.enabled = widget.default_enabled end @@ -429,8 +433,12 @@ end -- reduces the next call by a small random amount to introduce jitter into the -- widget processing timings local function do_update(name, db_entry, now_ms, vs) - if db_entry.next_update_ms > now_ms then return end local w = db_entry.widget + if w.overlay_onupdate_max_freq_seconds ~= 0 and + db_entry.next_update_ms > now_ms + then + return + end db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w) if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then if register_trigger_lock_screen(w:overlay_trigger(), name) then @@ -502,10 +510,6 @@ function feed_viewscreen_widgets(vs_name, vs, keys) not _feed_viewscreen_widgets('all', nil, keys) then return false end - gui.markMouseClicksHandled(keys) - if keys._MOUSE_L_DOWN then - df.global.enabler.mouse_lbut = 0 - end return true end @@ -577,7 +581,8 @@ end TitleVersionOverlay = defclass(TitleVersionOverlay, OverlayWidget) TitleVersionOverlay.ATTRS{ - default_pos={x=7, y=2}, + default_pos={x=11, y=1}, + version=2, default_enabled=true, viewscreens='title/Default', frame={w=35, h=5}, @@ -597,6 +602,10 @@ function TitleVersionOverlay:init() table.insert(text, {text='Pre-release build', pen=COLOR_LIGHTRED}) end + for _,t in ipairs(text) do + self.frame.w = math.max(self.frame.w, #t) + end + self:addviews{ widgets.Label{ frame={t=0, l=0}, diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index acd2eef6a..f06f86f0d 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1,5 +1,1314 @@ local _ENV = mkmodule('plugins.sort') +local gui = require('gui') +local overlay = require('plugins.overlay') +local setbelief = reqscript('modtools/set-belief') +local sortoverlay = require('plugins.sort.sortoverlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +local GLOBAL_KEY = 'sort' + +local CH_UP = string.char(30) +local CH_DN = string.char(31) + +local function get_rating(val, baseline, range, highest, high, med, low) + val = val - (baseline or 0) + range = range or 100 + local percentile = (math.min(range, val) * 100) // range + if percentile < (low or 25) then return percentile, COLOR_RED end + if percentile < (med or 50) then return percentile, COLOR_LIGHTRED end + if percentile < (high or 75) then return percentile, COLOR_YELLOW end + if percentile < (highest or 90) then return percentile, COLOR_GREEN end + return percentile, COLOR_LIGHTGREEN +end + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function get_name(unit) + return unit and dfhack.toSearchNormalized(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) +end + +local function sort_by_name_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local name1 = get_name(unit1) + local name2 = get_name(unit2) + return utils.compare_name(name1, name2) +end + +local function sort_by_name_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local name1 = get_name(unit1) + local name2 = get_name(unit2) + return utils.compare_name(name2, name1) +end + +local active_units = df.global.world.units.active +local active_idx_cache = {} +local function get_active_idx_cache() + local num_active_units = #active_units + if num_active_units == 0 or active_idx_cache[active_units[num_active_units-1].id] ~= num_active_units-1 then + active_idx_cache = {} + for i,active_unit in ipairs(active_units) do + active_idx_cache[active_unit.id] = i + end + end + return active_idx_cache +end + +local function is_original_dwarf(unit) + return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 +end + +local WAVE_END_GAP = 10000 + +local function get_most_recent_wave_oldest_active_idx(cache) + local oldest_unit + 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 + return cache[oldest_unit.id] + else + oldest_unit = unit + end + ::continue:: + end +end + +-- return green for most recent wave, red for the first wave, yellow for all others +-- rating is a three digit number that indicates the (potentially approximate) order +local function get_arrival_rating(unit) + local cache = get_active_idx_cache() + local unit_active_idx = cache[unit.id] + if not unit_active_idx then return end + local most_recent_wave_oldest_active_idx = get_most_recent_wave_oldest_active_idx(cache) + if not most_recent_wave_oldest_active_idx then return end + local num_active_units = #active_units + local rating = num_active_units < 1000 and unit_active_idx or ((unit_active_idx * 1000) // #active_units) + if most_recent_wave_oldest_active_idx < unit_active_idx then + return rating, COLOR_LIGHTGREEN + end + if is_original_dwarf(unit) then + return rating, COLOR_RED + end + return rating, COLOR_YELLOW +end + +local function sort_by_arrival_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local cache = get_active_idx_cache() + if not cache[unit_id_1] then return -1 end + if not cache[unit_id_2] then return 1 end + return utils.compare(cache[unit_id_2], cache[unit_id_1]) +end + +local function sort_by_arrival_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local cache = get_active_idx_cache() + if not cache[unit_id_1] then return -1 end + if not cache[unit_id_2] then return 1 end + return utils.compare(cache[unit_id_1], cache[unit_id_2]) +end + +local function get_stress(unit) + return unit and + unit.status.current_soul and + unit.status.current_soul.personality.stress +end + +local function get_stress_rating(unit) + return get_rating(dfhack.units.getStressCategory(unit), 0, 100, 4, 3, 2, 1) +end + +local function sort_by_stress_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) + if happiness1 == happiness2 then + return sort_by_name_desc(unit_id_1, unit_id_2) + end + if not happiness2 then return -1 end + if not happiness1 then return 1 end + return utils.compare(happiness2, happiness1) +end + +local function sort_by_stress_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) + if happiness1 == happiness2 then + return sort_by_name_desc(unit_id_1, unit_id_2) + end + if not happiness2 then return 1 end + if not happiness1 then return -1 end + return utils.compare(happiness1, happiness2) +end + +local function get_skill(skill, unit) + return unit and + unit.status.current_soul and + (utils.binsearch(unit.status.current_soul.skills, skill, 'id')) +end + +local function get_skill_rating(skill, unit) + local uskill = get_skill(skill, unit) + if not uskill then return nil end + return get_rating(uskill.rating, 0, 100, 10, 5, 1, 0) +end + +local MELEE_WEAPON_SKILLS = { + df.job_skill.AXE, + df.job_skill.SWORD, + df.job_skill.MACE, + df.job_skill.HAMMER, + df.job_skill.SPEAR, +} + +local function melee_skill_effectiveness(unit) + -- Physical attributes + local strength = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.STRENGTH) + local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) + local toughness = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.TOUGHNESS) + local endurance = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.ENDURANCE) + local body_size_base = unit.body.size_info.size_base + + -- Mental attributes + local willpower = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.WILLPOWER) + local spatial_sense = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.SPATIAL_SENSE) + local kinesthetic_sense = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.KINESTHETIC_SENSE) + + -- Skills + -- Finding the highest skill + local skill_rating = 0 + for _, skill in ipairs(MELEE_WEAPON_SKILLS) do + local melee_skill = dfhack.units.getNominalSkill(unit, skill, true) + skill_rating = math.max(skill_rating, melee_skill) + end + local melee_combat_rating = dfhack.units.getNominalSkill(unit, df.job_skill.MELEE_COMBAT, true) + + local rating = skill_rating * 27000 + melee_combat_rating * 9000 + + strength * 180 + body_size_base * 100 + kinesthetic_sense * 50 + endurance * 50 + + agility * 30 + toughness * 20 + willpower * 20 + spatial_sense * 20 + return rating +end + +local function get_melee_skill_effectiveness_rating(unit) + return get_rating(melee_skill_effectiveness(unit), 350000, 2750000, 64, 52, 40, 28) +end + +local function make_sort_by_melee_skill_effectiveness_desc() + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) + if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + return utils.compare(rating2, rating1) + end +end + +local function make_sort_by_melee_skill_effectiveness_asc() + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) + if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + return utils.compare(rating1, rating2) + end +end + +local RANGED_WEAPON_SKILLS = { + df.job_skill.CROSSBOW, +} + +-- Function could easily be adapted to different weapon types. +local function ranged_skill_effectiveness(unit) + -- Physical attributes + local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) + + -- Mental attributes + local spatial_sense = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.SPATIAL_SENSE) + local kinesthetic_sense = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.KINESTHETIC_SENSE) + local focus = dfhack.units.getMentalAttrValue(unit, df.mental_attribute_type.FOCUS) + + -- Skills + -- Finding the highest skill + local skill_rating = 0 + for _, skill in ipairs(RANGED_WEAPON_SKILLS) do + local ranged_skill = dfhack.units.getNominalSkill(unit, skill, true) + skill_rating = math.max(skill_rating, ranged_skill) + end + local ranged_combat = dfhack.units.getNominalSkill(unit, df.job_skill.RANGED_COMBAT, true) + + local rating = skill_rating * 24000 + ranged_combat * 8000 + + agility * 15 + spatial_sense * 15 + kinesthetic_sense * 6 + focus * 6 + return rating +end + +local function get_ranged_skill_effectiveness_rating(unit) + return get_rating(ranged_skill_effectiveness(unit), 0, 800000, 72, 52, 31, 11) +end + +local function make_sort_by_ranged_skill_effectiveness_desc() + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = ranged_skill_effectiveness(unit1) + local rating2 = ranged_skill_effectiveness(unit2) + if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + return utils.compare(rating2, rating1) + end +end + +local function make_sort_by_ranged_skill_effectiveness_asc() + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = ranged_skill_effectiveness(unit1) + local rating2 = ranged_skill_effectiveness(unit2) + if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + return utils.compare(rating1, rating2) + end +end + +local function make_sort_by_skill_desc(sort_skill) + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + if unit_id_1 == -1 then return -1 end + if unit_id_2 == -1 then return 1 end + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) + if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not s2 then return -1 end + if not s1 then return 1 end + if s1.rating ~= s2.rating then + return utils.compare(s2.rating, s1.rating) + end + if s1.experience ~= s2.experience then + return utils.compare(s2.experience, s1.experience) + end + return sort_by_name_desc(unit_id_1, unit_id_2) + end +end + +local function make_sort_by_skill_asc(sort_skill) + return function(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + if unit_id_1 == -1 then return -1 end + if unit_id_2 == -1 then return 1 end + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) + if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not s2 then return 1 end + if not s1 then return -1 end + if s1.rating ~= s2.rating then + return utils.compare(s1.rating, s2.rating) + end + if s1.experience ~= s2.experience then + return utils.compare(s1.experience, s2.experience) + end + return sort_by_name_desc(unit_id_1, unit_id_2) + end +end + +-- Statistical rating that is higher for dwarves that are mentally stable +local function get_mental_stability(unit) + local altruism = unit.status.current_soul.personality.traits.ALTRUISM + local anxiety_propensity = unit.status.current_soul.personality.traits.ANXIETY_PROPENSITY + local bravery = unit.status.current_soul.personality.traits.BRAVERY + local cheer_propensity = unit.status.current_soul.personality.traits.CHEER_PROPENSITY + local curious = unit.status.current_soul.personality.traits.CURIOUS + local discord = unit.status.current_soul.personality.traits.DISCORD + local dutifulness = unit.status.current_soul.personality.traits.DUTIFULNESS + 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 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 + local tolerant = unit.status.current_soul.personality.traits.TOLERANT + + local craftsmanship = setbelief.getUnitBelief(unit, df.value_type['CRAFTSMANSHIP']) + local family = setbelief.getUnitBelief(unit, df.value_type['FAMILY']) + local harmony = setbelief.getUnitBelief(unit, df.value_type['HARMONY']) + local independence = setbelief.getUnitBelief(unit, df.value_type['INDEPENDENCE']) + local knowledge = setbelief.getUnitBelief(unit, df.value_type['KNOWLEDGE']) + local leisure_time = setbelief.getUnitBelief(unit, df.value_type['LEISURE_TIME']) + local nature = setbelief.getUnitBelief(unit, df.value_type['NATURE']) + local skill = setbelief.getUnitBelief(unit, df.value_type['SKILL']) + + -- calculate the rating using the defined variables + local rating = (craftsmanship * -0.01) + (family * -0.09) + (harmony * 0.05) + + (independence * 0.06) + (knowledge * -0.30) + (leisure_time * 0.24) + + (nature * 0.27) + (skill * -0.21) + (altruism * 0.13) + + (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) + + (politeness * -0.14) + (privacy * 0.03) + (stress_vulnerability * -0.20) + + (tolerant * -0.11) + + return rating +end + +local function sort_by_mental_stability_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) + if rating1 == rating2 then + -- sorting by stress is opposite + -- more mental stable dwarves should have less stress + return sort_by_stress_asc(unit_id_1, unit_id_2) + end + return utils.compare(rating2, rating1) +end + +local function sort_by_mental_stability_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) + if rating1 == rating2 then + return sort_by_stress_desc(unit_id_1, unit_id_2) + end + return utils.compare(rating1, rating2) +end + +-- Statistical rating that is higher for more potent dwarves in long run melee military training +-- Rating considers fighting melee opponents +-- Wounds are not considered! +local function get_melee_combat_potential(unit) + -- Physical attributes + local strength = unit.body.physical_attrs.STRENGTH.max_value + local agility = unit.body.physical_attrs.AGILITY.max_value + local toughness = unit.body.physical_attrs.TOUGHNESS.max_value + local endurance = unit.body.physical_attrs.ENDURANCE.max_value + local body_size_base = unit.body.size_info.size_base + + -- Mental attributes + local willpower = unit.status.current_soul.mental_attrs.WILLPOWER.max_value + local spatial_sense = unit.status.current_soul.mental_attrs.SPATIAL_SENSE.max_value + local kinesthetic_sense = unit.status.current_soul.mental_attrs.KINESTHETIC_SENSE.max_value + + -- assume highest skill ratings + local skill_rating = df.skill_rating.Legendary5 + local melee_combat_rating = df.skill_rating.Legendary5 + + -- melee combat potential rating + local rating = skill_rating * 27000 + melee_combat_rating * 9000 + + strength * 180 + body_size_base * 100 + kinesthetic_sense * 50 + endurance * 50 + + agility * 30 + toughness * 20 + willpower * 20 + spatial_sense * 20 + return rating +end + +local function get_melee_combat_potential_rating(unit) + return get_rating(get_melee_combat_potential(unit), 350000, 2750000, 64, 52, 40, 28) +end + +local function sort_by_melee_combat_potential_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) + if rating1 == rating2 then + return sort_by_mental_stability_desc(unit_id_1, unit_id_2) + end + return utils.compare(rating2, rating1) +end + +local function sort_by_melee_combat_potential_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) + if rating1 == rating2 then + return sort_by_mental_stability_asc(unit_id_1, unit_id_2) + end + return utils.compare(rating1, rating2) +end + +-- Statistical rating that is higher for more potent dwarves in long run ranged military training +-- Wounds are not considered! +local function get_ranged_combat_potential(unit) + -- Physical attributes + local agility = unit.body.physical_attrs.AGILITY.max_value + + -- Mental attributes + local focus = unit.status.current_soul.mental_attrs.FOCUS.max_value + local spatial_sense = unit.status.current_soul.mental_attrs.SPATIAL_SENSE.max_value + local kinesthetic_sense = unit.status.current_soul.mental_attrs.KINESTHETIC_SENSE.max_value + + -- assume highest skill ratings + local skill_rating = df.skill_rating.Legendary5 + local ranged_combat = df.skill_rating.Legendary5 + + -- ranged combat potential formula + local rating = skill_rating * 24000 + ranged_combat * 8000 + + agility * 15 + spatial_sense * 15 + kinesthetic_sense * 6 + focus * 6 + return rating +end + +local function get_ranged_combat_potential_rating(unit) + return get_rating(get_ranged_combat_potential(unit), 0, 800000, 72, 52, 31, 11) +end + +local function sort_by_ranged_combat_potential_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) + if rating1 == rating2 then + return sort_by_mental_stability_desc(unit_id_1, unit_id_2) + end + return utils.compare(rating2, rating1) +end + +local function sort_by_ranged_combat_potential_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) + if rating1 == rating2 then + return sort_by_mental_stability_asc(unit_id_1, unit_id_2) + end + return utils.compare(rating1, rating2) +end + +local function get_need(unit) + if not unit or not unit.status.current_soul then return end + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == df.need_type.MartialTraining and need.focus_level < 0 then + return -need.focus_level + end + end +end + +local function get_need_rating(unit) + local focus_level = get_need(unit) + if not focus_level then return end + -- convert to stress ratings so we can use stress faces as labels + if focus_level > 100000 then return 0 end + if focus_level > 10000 then return 1 end + if focus_level > 1000 then return 2 end + if focus_level > 100 then return 3 end + return 6 +end + +local function sort_by_need_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_desc(unit_id_1, unit_id_2) + end + if not rating2 then return -1 end + if not rating1 then return 1 end + return utils.compare(rating2, rating1) +end + +local function sort_by_need_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_asc(unit_id_1, unit_id_2) + end + if not rating2 then return 1 end + if not rating1 then return -1 end + return utils.compare(rating1, rating2) +end + +local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc() +local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc() +local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc() +local sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc() +local sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING) +local sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING) +local sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS) +local sort_by_tactics_asc=make_sort_by_skill_asc(df.job_skill.MILITARY_TACTICS) +local sort_by_axe_desc=make_sort_by_skill_desc(df.job_skill.AXE) +local sort_by_axe_asc=make_sort_by_skill_asc(df.job_skill.AXE) +local sort_by_sword_desc=make_sort_by_skill_desc(df.job_skill.SWORD) +local sort_by_sword_asc=make_sort_by_skill_asc(df.job_skill.SWORD) +local sort_by_mace_desc=make_sort_by_skill_desc(df.job_skill.MACE) +local sort_by_mace_asc=make_sort_by_skill_asc(df.job_skill.MACE) +local sort_by_hammer_desc=make_sort_by_skill_desc(df.job_skill.HAMMER) +local sort_by_hammer_asc=make_sort_by_skill_asc(df.job_skill.HAMMER) +local sort_by_spear_desc=make_sort_by_skill_desc(df.job_skill.SPEAR) +local sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR) +local sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW) +local sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW) + +local SORT_LIBRARY = { + {label='melee effectiveness', desc_fn=sort_by_any_melee_desc, asc_fn=sort_by_any_melee_asc, rating_fn=get_melee_skill_effectiveness_rating}, + {label='ranged effectiveness', desc_fn=sort_by_any_ranged_desc, asc_fn=sort_by_any_ranged_asc, rating_fn=get_ranged_skill_effectiveness_rating}, + {label='name', desc_fn=sort_by_name_desc, asc_fn=sort_by_name_asc}, + {label='teacher skill', desc_fn=sort_by_teacher_desc, asc_fn=sort_by_teacher_asc, rating_fn=curry(get_skill_rating, df.job_skill.TEACHING)}, + {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, + {label='arrival order', desc_fn=sort_by_arrival_desc, asc_fn=sort_by_arrival_asc, rating_fn=get_arrival_rating}, + {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating, use_stress_faces=true}, + {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating, use_stress_faces=true}, + {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, + {label='sword skill', desc_fn=sort_by_sword_desc, asc_fn=sort_by_sword_asc, rating_fn=curry(get_skill_rating, df.job_skill.SWORD)}, + {label='mace skill', desc_fn=sort_by_mace_desc, asc_fn=sort_by_mace_asc, rating_fn=curry(get_skill_rating, df.job_skill.MACE)}, + {label='hammer skill', desc_fn=sort_by_hammer_desc, asc_fn=sort_by_hammer_asc, rating_fn=curry(get_skill_rating, df.job_skill.HAMMER)}, + {label='spear skill', desc_fn=sort_by_spear_desc, asc_fn=sort_by_spear_asc, rating_fn=curry(get_skill_rating, df.job_skill.SPEAR)}, + {label='crossbow skill', desc_fn=sort_by_crossbow_desc, asc_fn=sort_by_crossbow_asc, rating_fn=curry(get_skill_rating, df.job_skill.CROSSBOW)}, + {label='melee potential', desc_fn=sort_by_melee_combat_potential_desc, asc_fn=sort_by_melee_combat_potential_asc, rating_fn=get_melee_combat_potential_rating}, + {label='ranged potential', desc_fn=sort_by_ranged_combat_potential_desc, asc_fn=sort_by_ranged_combat_potential_asc, rating_fn=get_ranged_combat_potential_rating}, +} + +local RATING_FNS = {} +local STRESS_FACE_FNS = {} +for _, opt in ipairs(SORT_LIBRARY) do + RATING_FNS[opt.desc_fn] = opt.rating_fn + RATING_FNS[opt.asc_fn] = opt.rating_fn + if opt.use_stress_faces then + STRESS_FACE_FNS[opt.desc_fn] = true + STRESS_FACE_FNS[opt.asc_fn] = true + end +end + +-- ---------------------- +-- SquadAssignmentOverlay +-- + +SquadAssignmentOverlay = defclass(SquadAssignmentOverlay, overlay.OverlayWidget) +SquadAssignmentOverlay.ATTRS{ + default_pos={x=18, y=5}, + default_enabled=true, + viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', + version='2', + frame={w=38, h=31}, +} + +-- allow initial spacebar or two successive spacebars to fall through and +-- pause/unpause the game +local function search_on_char(ch, text) + if ch == ' ' then return text:match('%S$') end + return ch:match('[%l _-]') +end + +function SquadAssignmentOverlay:init() + self.dirty = true + + local sort_options = {} + for _, opt in ipairs(SORT_LIBRARY) do + table.insert(sort_options, { + label=opt.label..CH_DN, + value=opt.desc_fn, + pen=COLOR_GREEN, + }) + table.insert(sort_options, { + label=opt.label..CH_UP, + value=opt.asc_fn, + pen=COLOR_YELLOW, + }) + end + + local main_panel = widgets.Panel{ + frame={l=0, r=0, t=0, b=0}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + autoarrange_subviews=true, + autoarrange_gap=1, + } + main_panel:addviews{ + widgets.EditField{ + view_id='search', + frame={l=0}, + label_text='Search: ', + on_char=search_on_char, + on_change=function() self:refresh_list() end, + }, + widgets.Panel{ + frame={l=0, r=0, h=15}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={t=0, l=0}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options=sort_options, + initial_option=sort_by_any_melee_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_any_melee', + frame={t=2, l=0, w=11}, + options={ + {label='melee eff.', value=sort_noop}, + {label='melee eff.'..CH_DN, value=sort_by_any_melee_desc, pen=COLOR_GREEN}, + {label='melee eff.'..CH_UP, value=sort_by_any_melee_asc, pen=COLOR_YELLOW}, + }, + initial_option=sort_by_any_melee_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_any_melee'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_any_ranged', + frame={t=2, r=8, w=12}, + options={ + {label='ranged eff.', value=sort_noop}, + {label='ranged eff.'..CH_DN, value=sort_by_any_ranged_desc, pen=COLOR_GREEN}, + {label='ranged eff.'..CH_UP, value=sort_by_any_ranged_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_any_ranged'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={t=2, r=0, w=5}, + options={ + {label='name', value=sort_noop}, + {label='name'..CH_DN, value=sort_by_name_desc, pen=COLOR_GREEN}, + {label='name'..CH_UP, value=sort_by_name_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_teacher', + frame={t=4, l=0, w=8}, + options={ + {label='teacher', value=sort_noop}, + {label='teacher'..CH_DN, value=sort_by_teacher_desc, pen=COLOR_GREEN}, + {label='teacher'..CH_UP, value=sort_by_teacher_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_teacher'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_tactics', + frame={t=4, l=10, w=8}, + options={ + {label='tactics', value=sort_noop}, + {label='tactics'..CH_DN, value=sort_by_tactics_desc, pen=COLOR_GREEN}, + {label='tactics'..CH_UP, value=sort_by_tactics_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_tactics'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_arrival', + frame={t=4, r=0, w=14}, + options={ + {label='arrival order', value=sort_noop}, + {label='arrival order'..CH_DN, value=sort_by_arrival_desc, pen=COLOR_GREEN}, + {label='arrival order'..CH_UP, value=sort_by_arrival_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_arrival'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_stress', + frame={t=6, l=0, w=7}, + options={ + {label='stress', value=sort_noop}, + {label='stress'..CH_DN, value=sort_by_stress_desc, pen=COLOR_GREEN}, + {label='stress'..CH_UP, value=sort_by_stress_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_stress'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_need', + frame={t=6, r=0, w=18}, + options={ + {label='need for training', value=sort_noop}, + {label='need for training'..CH_DN, value=sort_by_need_desc, pen=COLOR_GREEN}, + {label='need for training'..CH_UP, value=sort_by_need_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_need'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_axe', + frame={t=8, l=0, w=4}, + options={ + {label='axe', value=sort_noop}, + {label='axe'..CH_DN, value=sort_by_axe_desc, pen=COLOR_GREEN}, + {label='axe'..CH_UP, value=sort_by_axe_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_axe'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_sword', + frame={t=8, w=6}, + options={ + {label='sword', value=sort_noop}, + {label='sword'..CH_DN, value=sort_by_sword_desc, pen=COLOR_GREEN}, + {label='sword'..CH_UP, value=sort_by_sword_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_sword'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_mace', + frame={t=8, r=0, w=5}, + options={ + {label='mace', value=sort_noop}, + {label='mace'..CH_DN, value=sort_by_mace_desc, pen=COLOR_GREEN}, + {label='mace'..CH_UP, value=sort_by_mace_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_mace'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_hammer', + frame={t=10, l=0, w=7}, + options={ + {label='hammer', value=sort_noop}, + {label='hammer'..CH_DN, value=sort_by_hammer_desc, pen=COLOR_GREEN}, + {label='hammer'..CH_UP, value=sort_by_hammer_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_hammer'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_spear', + frame={t=10, w=6}, + options={ + {label='spear', value=sort_noop}, + {label='spear'..CH_DN, value=sort_by_spear_desc, pen=COLOR_GREEN}, + {label='spear'..CH_UP, value=sort_by_spear_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_spear'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_crossbow', + frame={t=10, r=0, w=9}, + options={ + {label='crossbow', value=sort_noop}, + {label='crossbow'..CH_DN, value=sort_by_crossbow_desc, pen=COLOR_GREEN}, + {label='crossbow'..CH_UP, value=sort_by_crossbow_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_crossbow'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_melee_combat_potential', + frame={t=12, l=0, w=16}, + options={ + {label='melee potential', value=sort_noop}, + {label='melee potential'..CH_DN, value=sort_by_melee_combat_potential_desc, pen=COLOR_GREEN}, + {label='melee potential'..CH_UP, value=sort_by_melee_combat_potential_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_melee_combat_potential'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_ranged_combat_potential', + frame={t=12, r=0, w=17}, + options={ + {label='ranged potential', value=sort_noop}, + {label='ranged potential'..CH_DN, value=sort_by_ranged_combat_potential_desc, pen=COLOR_GREEN}, + {label='ranged potential'..CH_UP, value=sort_by_ranged_combat_potential_asc, pen=COLOR_YELLOW}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_ranged_combat_potential'), + }, + }, + }, + widgets.CycleHotkeyLabel{ + view_id='military', + frame={l=0}, + key='CUSTOM_SHIFT_Q', + label='Units in other squads:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='officials', + frame={l=0}, + key='CUSTOM_SHIFT_O', + label='Appointed officials:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='nobles', + frame={l=0, w=20}, + key='CUSTOM_SHIFT_N', + label='Nobility:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={l=0}, + key='CUSTOM_SHIFT_M', + label='Mothers with infants:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='unstable', + frame={l=0}, + key='CUSTOM_SHIFT_F', + label='Weak mental fortitude:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='maimed', + frame={l=0}, + key='CUSTOM_SHIFT_I', + label='Critically injured:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + } + + self:addviews{ + main_panel, + widgets.HelpButton{ + frame={t=0, r=1}, + command='sort', + }, + } +end + +local function normalize_search_key(search_key) + local out = '' + for c in dfhack.toSearchNormalized(search_key):gmatch("[%w%s]") do + out = out .. c:lower() + end + return out +end + +local function is_in_military(unit) + return unit.military.squad_id > -1 +end + +local function is_elected_or_appointed_official(unit) + for _,occupation in ipairs(unit.occupations) do + if occupation.type ~= df.occupation_type.MERCENARY then + return true + end + end + for _, noble_pos in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + if noble_pos.position.flags.ELECTED or + (noble_pos.position.mandate_max == 0 and noble_pos.position.demand_max == 0) + then + return true + end + end + return false +end + +local function is_nobility(unit) + for _, noble_pos in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + if not noble_pos.position.flags.ELECTED and + (noble_pos.position.mandate_max > 0 or noble_pos.position.demand_max > 0) + then + return true + end + end + return false +end + +local function has_infant(unit) + for _, baby in ipairs(df.global.world.units.other.ANY_BABY2) do + if baby.relationship_ids.Mother == unit.id then + return true + end + end + return false +end + +local function is_unstable(unit) + -- stddev percentiles are 61, 48, 35, 23 + -- let's go with one stddev below the mean (35) as the cutoff + local _, color = get_rating(get_mental_stability(unit), -40, 80, 35, 0, 0, 0) + return color ~= COLOR_LIGHTGREEN +end + +local function is_maimed(unit) + return not unit.flags2.vision_good or + unit.status2.limbs_grasp_count < 2 or + unit.status2.limbs_stand_count == 0 +end + +local function filter_matches(unit_id, filter) + if unit_id == -1 then return true end + local unit = df.unit.find(unit_id) + if not unit then return false end + 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 + if filter.officials == 'only' and not is_elected_or_appointed_official(unit) then return false end + if filter.officials == 'exclude' and is_elected_or_appointed_official(unit) then return false end + if filter.nobles == 'only' and not is_nobility(unit) then return false end + if filter.nobles == 'exclude' and is_nobility(unit) then return false end + if filter.infant == 'only' and not has_infant(unit) then return false end + if filter.infant == 'exclude' and has_infant(unit) then return false end + if filter.unstable == 'only' and not is_unstable(unit) then return false end + 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.search == 0 then return true end + local search_key = sortoverlay.get_unit_search_key(unit) + return normalize_search_key(search_key):find(dfhack.toSearchNormalized(filter.search)) +end + +local function is_noop_filter(filter) + return #filter.search == 0 and + filter.military == 'include' and + filter.officials == 'include' and + filter.nobles == 'include' and + filter.infant == 'include' and + filter.unstable == 'include' and + filter.maimed == 'include' +end + +local function is_filter_equal(a, b) + return a.search == b.search and + a.military == b.military and + a.officials == b.officials and + a.nobles == b.nobles and + a.infant == b.infant and + a.unstable == b.unstable and + a.maimed == b.maimed +end + +local unit_selector = df.global.game.main_interface.unit_selector + +-- this function uses the unused itemid and selected vectors to keep state, +-- taking advantage of the fact that they are reset by DF when the list of units changes +local function filter_vector(filter, prev_filter) + local unid_is_filtered = #unit_selector.selected >= 0 and unit_selector.selected[0] ~= 0 + if is_noop_filter(filter) or #unit_selector.selected == 0 then + if not unid_is_filtered then + -- we haven't modified the unid vector; nothing to do here + return + end + -- restore the unid vector + unit_selector.unid:assign(unit_selector.itemid) + -- clear our "we meddled" flag + unit_selector.selected[0] = 0 + return + end + if unid_is_filtered and is_filter_equal(filter, prev_filter) then + -- filter hasn't changed; we don't need to refilter + return + end + if unid_is_filtered then + -- restore the unid vector + unit_selector.unid:assign(unit_selector.itemid) + else + -- save the unid vector and set our meddle flag + unit_selector.itemid:assign(unit_selector.unid) + unit_selector.selected[0] = 1 + end + -- do the actual filtering + for idx=#unit_selector.unid-1,0,-1 do + if not filter_matches(unit_selector.unid[idx], filter) then + unit_selector.unid:erase(idx) + end + end + -- fix up scroll position if it would be off the end of the list + if unit_selector.scroll_position + 10 > #unit_selector.unid then + unit_selector.scroll_position = math.max(0, #unit_selector.unid - 10) + end +end + +local use_stress_faces = false +local rating_annotations = {} + +local function annotate_visible_units(sort_fn) + use_stress_faces = STRESS_FACE_FNS[sort_fn] + rating_annotations = {} + rating_fn = RATING_FNS[sort_fn] + local max_idx = math.min(#unit_selector.unid-1, unit_selector.scroll_position+9) + for idx = unit_selector.scroll_position, max_idx do + local annotation_idx = idx - unit_selector.scroll_position + 1 + local unit = df.unit.find(unit_selector.unid[idx]) + rating_annotations[annotation_idx] = nil + if unit and rating_fn then + local val, color = rating_fn(unit) + if val then + rating_annotations[annotation_idx] = {val=val, color=color} + end + end + end +end + +local SORT_WIDGET_NAMES = { + 'sort', + 'sort_any_melee', + 'sort_any_ranged', + 'sort_name', + 'sort_teacher', + 'sort_tactics', + 'sort_arrival', + 'sort_stress', + 'sort_need', + 'sort_axe', + 'sort_sword', + 'sort_mace', + 'sort_hammer', + 'sort_spear', + 'sort_crossbow', + 'sort_melee_combat_potential', + 'sort_ranged_combat_potential', +} + +function SquadAssignmentOverlay:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs(SORT_WIDGET_NAMES) do + self.subviews[widget_name]:setOption(sort_fn) + end + local filter = { + search=self.subviews.search.text, + military=self.subviews.military:getOptionValue(), + officials=self.subviews.officials:getOptionValue(), + nobles=self.subviews.nobles:getOptionValue(), + infant=self.subviews.infant:getOptionValue(), + unstable=self.subviews.unstable:getOptionValue(), + maimed=self.subviews.maimed:getOptionValue(), + } + filter_vector(filter, self.prev_filter or {}) + self.prev_filter = filter + utils.sort_vector(unit_selector.unid, nil, sort_fn) + annotate_visible_units(sort_fn) + self.saved_scroll_position = unit_selector.scroll_position +end + +function SquadAssignmentOverlay:onInput(keys) + if keys._MOUSE_R or (keys._MOUSE_L and not self:getMouseFramePos()) then + -- if any click is made outside of our window, we may need to refresh our list + self.dirty = true + end + return SquadAssignmentOverlay.super.onInput(self, keys) +end + +function SquadAssignmentOverlay:onRenderFrame(dc, frame_rect) + SquadAssignmentOverlay.super.onRenderFrame(self, dc, frame_rect) + if self.dirty then + self:refresh_list() + self.dirty = false + elseif self.saved_scroll_position ~= unit_selector.scroll_position then + annotate_visible_units(self.subviews.sort:getOptionValue()) + self.saved_scroll_position = unit_selector.scroll_position + end +end + +-- ---------------------- +-- SquadAnnotationOverlay +-- + +SquadAnnotationOverlay = defclass(SquadAnnotationOverlay, overlay.OverlayWidget) +SquadAnnotationOverlay.ATTRS{ + default_pos={x=56, y=5}, + default_enabled=true, + viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', + frame={w=5, h=35}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + frame_background=gui.CLEAR_PEN, +} + +function get_annotation_text(idx) + local elem = rating_annotations[idx] + if not elem or not tonumber(elem.val) then return ' - ' end + + return tostring(math.tointeger(elem.val)) +end + +function get_annotation_color(idx) + local elem = rating_annotations[idx] + return elem and elem.color or nil +end + +local to_pen = dfhack.pen.parse +local DASH_PEN = to_pen{ch='-', fg=COLOR_WHITE, keep_lower=true} + +local FACE_TILES = {} +local function init_face_tiles() + for idx=0,6 do + FACE_TILES[idx] = {} + local face_off = (6 - idx) * 2 + for y=0,1 do + for x=0,1 do + local tile = dfhack.screen.findGraphicsTile('INTERFACE_BITS', 32 + face_off + x, 6 + y) + ensure_key(FACE_TILES[idx], y)[x] = tile + end + end + end + + for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do + local face = {} + ensure_key(face, 0)[0] = to_pen{tile=FACE_TILES[idx-1][0][0], ch=1, fg=color} + ensure_key(face, 0)[1] = to_pen{tile=FACE_TILES[idx-1][0][1], ch='\\', fg=color} + ensure_key(face, 1)[0] = to_pen{tile=FACE_TILES[idx-1][1][0], ch='\\', fg=color} + ensure_key(face, 1)[1] = to_pen{tile=FACE_TILES[idx-1][1][1], ch='/', fg=color} + FACE_TILES[idx-1] = face + end +end +init_face_tiles() + +function get_stress_face_tile(idx, x, y) + local elem = rating_annotations[idx] + if not elem or not elem.val or elem.val < 0 then + return x == 0 and y == 1 and DASH_PEN or gui.CLEAR_PEN + end + local val = math.min(6, elem.val) + return safe_index(FACE_TILES, val, y, x) +end + +function SquadAnnotationOverlay:init() + for idx = 1, 10 do + self:addviews{ + widgets.Label{ + frame={t=idx*3+1, h=1, w=3}, + text={ + { + text=curry(get_annotation_text, idx), + pen=curry(get_annotation_color, idx), + width=3, + rjustify=true, + }, + }, + visible=function() return not use_stress_faces end, + }, + widgets.Label{ + frame={t=idx*3, r=0, h=2, w=2}, + auto_height=false, + text={ + {width=1, tile=curry(get_stress_face_tile, idx, 0, 0)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 0)}, + NEWLINE, + {width=1, tile=curry(get_stress_face_tile, idx, 0, 1)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 1)}, + }, + visible=function() return use_stress_faces end, + }, + } + end +end + +OVERLAY_WIDGETS = { + squad_assignment=SquadAssignmentOverlay, + squad_annotation=SquadAnnotationOverlay, + info=require('plugins.sort.info').InfoOverlay, + workanimals=require('plugins.sort.info').WorkAnimalOverlay, + candidates=require('plugins.sort.info').CandidatesOverlay, + interrogation=require('plugins.sort.info').InterrogationOverlay, + location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, + unit_selector=require('plugins.sort.unitselector').UnitSelectorOverlay, + worker_assignment=require('plugins.sort.unitselector').WorkerAssignmentOverlay, + burrow_assignment=require('plugins.sort.unitselector').BurrowAssignmentOverlay, + slab=require('plugins.sort.slab').SlabOverlay, + world=require('plugins.sort.world').WorldOverlay, +} + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + init_face_tiles() +end + +--[[ local utils = require('utils') local units = require('plugins.sort.units') local items = require('plugins.sort.items') @@ -51,5 +1360,6 @@ function parse_ordering_spec(type,...) end make_sort_order = utils.make_sort_order +]] return _ENV diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua new file mode 100644 index 000000000..7f9b49b7b --- /dev/null +++ b/plugins/lua/sort/info.lua @@ -0,0 +1,701 @@ +local _ENV = mkmodule('plugins.sort.info') + +local gui = require('gui') +local overlay = require('plugins.overlay') +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') +local utils = require('utils') + +local info = df.global.game.main_interface.info +local administrators = info.administrators +local creatures = info.creatures +local justice = info.justice +local objects = info.artifacts +local tasks = info.jobs +local work_details = info.labor.work_details + +-- these sort functions attempt to match the vanilla info panel sort behavior, which +-- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, +-- we'd always sort by name descending as a secondary sort. To match vanilla sorting, +-- if the primary sort is ascending, the secondary name sort will also be ascending. +-- +-- also note that vanilla sorts are not stable, so there might still be some jitter +-- if the player clicks one of the vanilla sort widgets after searching +local function sort_by_name_desc(a, b) + return a.sort_name < b.sort_name +end + +local function sort_by_name_asc(a, b) + return a.sort_name > b.sort_name +end + +local function sort_by_prof_desc(a, b) + if a.profession_list_order1 == b.profession_list_order1 then + return sort_by_name_desc(a, b) + end + return a.profession_list_order1 < b.profession_list_order1 +end + +local function sort_by_prof_asc(a, b) + if a.profession_list_order1 == b.profession_list_order1 then + return sort_by_name_asc(a, b) + end + return a.profession_list_order1 > b.profession_list_order1 +end + +local function sort_by_job_name_desc(a, b) + if a.job_sort_name == b.job_sort_name then + return sort_by_name_desc(a, b) + end + return a.job_sort_name < b.job_sort_name +end + +local function sort_by_job_name_asc(a, b) + if a.job_sort_name == b.job_sort_name then + -- use descending tertiary sort for visual stability + return sort_by_name_desc(a, b) + end + return a.job_sort_name > b.job_sort_name +end + +local function sort_by_job_desc(a, b) + if not not a.jb == not not b.jb then + return sort_by_job_name_desc(a, b) + end + return not not a.jb +end + +local function sort_by_job_asc(a, b) + if not not a.jb == not not b.jb then + return sort_by_job_name_asc(a, b) + end + return not not b.jb +end + +local function sort_by_stress_desc(a, b) + if a.stress == b.stress then + return sort_by_name_desc(a, b) + end + return a.stress > b.stress +end + +local function sort_by_stress_asc(a, b) + if a.stress == b.stress then + return sort_by_name_asc(a, b) + end + return a.stress < b.stress +end + +local function get_sort() + if creatures.sorting_cit_job then + return creatures.sorting_cit_job_is_ascending and sort_by_job_asc or sort_by_job_desc + elseif creatures.sorting_cit_stress then + return creatures.sorting_cit_stress_is_ascending and sort_by_stress_asc or sort_by_stress_desc + elseif creatures.sorting_cit_nameprof_doing_prof then + return creatures.sorting_cit_nameprof_is_ascending and sort_by_prof_asc or sort_by_prof_desc + else + return creatures.sorting_cit_nameprof_is_ascending and sort_by_name_asc or sort_by_name_desc + end +end + +local function get_cri_unit_search_key(cri_unit) + return ('%s %s'):format( + cri_unit.un and sortoverlay.get_unit_search_key(cri_unit.un) or '', + cri_unit.job_sort_name) +end + +local function get_race_name(raw_id) + local raw = df.creature_raw.find(raw_id) + if not raw then return end + return raw.name[1] +end + +-- get name in both dwarvish and English +local function get_artifact_search_key(artifact) + return ('%s %s'):format(dfhack.TranslateName(artifact.name), dfhack.TranslateName(artifact.name, true)) +end + +local function work_details_search(vec, data, text, incremental) + if work_details.selected_work_detail_index ~= data.selected then + data.saved_original = nil + data.selected = work_details.selected_work_detail_index + end + sortoverlay.single_vector_search( + {get_search_key_fn=sortoverlay.get_unit_search_key}, + vec, data, text, incremental) +end + +local function restore_allocated_data(vec, data) + if not data.saved_visible or not data.saved_original then return end + for _,elem in ipairs(data.saved_original) do + if not utils.linear_index(data.saved_visible, elem) then + vec:insert('#', elem) + end + end +end + +local function serialize_skills(unit) + if not unit or not unit.status or not unit.status.current_soul then + return '' + end + local skills = {} + for _, skill in ipairs(unit.status.current_soul.skills) do + if skill.rating > 0 then -- ignore dabbling + table.insert(skills, df.job_skill[skill.id]) + end + end + return table.concat(skills, ' ') +end + +local function get_candidate_search_key(cand) + if not cand.un then return end + return ('%s %s'):format( + sortoverlay.get_unit_search_key(cand.un), + serialize_skills(cand.un)) +end + +-- ---------------------- +-- InfoOverlay +-- + +InfoOverlay = defclass(InfoOverlay, sortoverlay.SortOverlay) +InfoOverlay.ATTRS{ + default_pos={x=64, y=8}, + viewscreens='dwarfmode/Info', + frame={w=40, h=6}, +} + +function get_squad_options() + local options = {{label='Any', value='all', pen=COLOR_GREEN}} + local fort = df.historical_entity.find(df.global.plotinfo.group_id) + if not fort then return options end + for _, squad_id in ipairs(fort.squads) do + table.insert(options, { + label=dfhack.military.getSquadName(squad_id), + value=squad_id, + pen=COLOR_YELLOW, + }) + end + return options +end + +function get_burrow_options() + local options = { + {label='Any', value='all', pen=COLOR_GREEN}, + {label='Unburrowed', value='none', pen=COLOR_LIGHTRED}, + } + for _, burrow in ipairs(df.global.plotinfo.burrows.list) do + table.insert(options, { + label=#burrow.name > 0 and burrow.name or ('Burrow %d'):format(burrow.id + 1), + value=burrow.id, + pen=COLOR_YELLOW, + }) + end + return options +end + +function matches_squad_burrow_filters(unit, subset, target_squad_id, target_burrow_id) + if subset == 'all' then + return true + elseif subset == 'civilian' then + return unit.military.squad_id == -1 + elseif subset == 'military' then + local squad_id = unit.military.squad_id + if squad_id == -1 then return false end + if target_squad_id == 'all' then return true end + return target_squad_id == squad_id + elseif subset == 'burrow' then + if target_burrow_id == 'all' then return #unit.burrows + #unit.inactive_burrows > 0 end + if target_burrow_id == 'none' then return #unit.burrows + #unit.inactive_burrows == 0 end + return utils.binsearch(unit.burrows, target_burrow_id) or + utils.binsearch(unit.inactive_burrows, target_burrow_id) + end + return true +end + +function InfoOverlay:init() + self:addviews{ + widgets.BannerPanel{ + view_id='panel', + frame={l=0, t=0, r=0, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + widgets.BannerPanel{ + view_id='subset_panel', + frame={l=0, t=1, r=0, h=1}, + visible=function() return self:get_key() == 'PET_WA' end, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='subset', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_F', + label='Show:', + options={ + {label='All', value='all', pen=COLOR_GREEN}, + {label='Military', value='military', pen=COLOR_YELLOW}, + {label='Civilians', value='civilian', pen=COLOR_CYAN}, + {label='Burrowed', value='burrow', pen=COLOR_MAGENTA}, + }, + on_change=function(value) + local squad = self.subviews.squad + local burrow = self.subviews.burrow + squad.visible = false + burrow.visible = false + if value == 'military' then + squad.options = get_squad_options() + squad:setOption('all') + squad.visible = true + elseif value == 'burrow' then + burrow.options = get_burrow_options() + burrow:setOption('all') + burrow.visible = true + end + self:do_search(self.subviews.search.text, true) + end, + }, + }, + }, + widgets.BannerPanel{ + view_id='subfilter_panel', + frame={l=0, t=2, r=0, h=1}, + visible=function() + local subset = self.subviews.subset:getOptionValue() + return self:get_key() == 'PET_WA' and (subset == 'military' or subset == 'burrow') + end, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='squad', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_S', + label='Squad:', + options={ + {label='Any', value='all', pen=COLOR_GREEN}, + }, + visible=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + widgets.CycleHotkeyLabel{ + view_id='burrow', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_B', + label='Burrow:', + options={ + {label='Any', value='all', pen=COLOR_GREEN}, + }, + visible=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + }, + }, + } + + local CRI_UNIT_VECS = { + CITIZEN=creatures.cri_unit.CITIZEN, + PET=creatures.cri_unit.PET, + OTHER=creatures.cri_unit.OTHER, + DECEASED=creatures.cri_unit.DECEASED, + } + for key,vec in pairs(CRI_UNIT_VECS) do + self:register_handler(key, vec, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_cri_unit_search_key, + get_sort_fn=get_sort + }), + curry(restore_allocated_data, vec)) + end + + self:register_handler('JOBS', tasks.cri_job, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_cri_unit_search_key}), + curry(restore_allocated_data, tasks.cri_job)) + self:register_handler('PET_OT', creatures.atk_index, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_race_name})) + self:register_handler('PET_AT', creatures.trainer, + curry(sortoverlay.single_vector_search, {get_search_key_fn=sortoverlay.get_unit_search_key})) + self:register_handler('PET_WA', creatures.work_animal_recipient, + curry(sortoverlay.single_vector_search, { + get_search_key_fn=sortoverlay.get_unit_search_key, + matches_filters_fn=self:callback('matches_filters'), + })) + self:register_handler('WORK_DETAILS', work_details.assignable_unit, work_details_search) + + for idx,name in ipairs(df.artifacts_mode_type) do + if idx < 0 then goto continue end + self:register_handler(name, objects.list[idx], + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_artifact_search_key})) + ::continue:: + end +end + +function InfoOverlay:get_key() + if info.current_mode == df.info_interface_mode_type.CREATURES then + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + elseif creatures.assign_work_animal then + return 'PET_WA' + end + end + return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.JOBS then + return 'JOBS' + elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then + return df.artifacts_mode_type[objects.mode] + elseif info.current_mode == df.info_interface_mode_type.LABOR then + if info.labor.mode == df.labor_mode_type.WORK_DETAILS then + return 'WORK_DETAILS' + end + end +end + +local function resize_overlay(self) + local sw = dfhack.screen.getWindowSize() + local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + if overlay_width ~= self.frame.w then + self.frame.w = overlay_width + return true + end +end + +local function is_tabs_in_two_rows() + return dfhack.screen.readTile(64, 6, false).ch == 0 +end + +local 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 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 then + t_offset = t_offset - 1 + end + return l_offset, t_offset +end + +function InfoOverlay:updateFrames() + local ret = resize_overlay(self) + local l, t = get_panel_offsets() + local frame = self.subviews.panel.frame + if frame.l == l and frame.t == t then return ret end + frame.l, frame.t = l, t + local frame2 = self.subviews.subset_panel.frame + frame2.l, frame2.t = l, t + 1 + local frame3 = self.subviews.subfilter_panel.frame + frame3.l, frame3.t = l, t + 2 + return true +end + +function InfoOverlay:onRenderBody(dc) + InfoOverlay.super.onRenderBody(self, dc) + if self:updateFrames() then + self:updateLayout() + end + if self.refresh_search then + self.refresh_search = nil + self:do_search(self.subviews.search.text) + end +end + +function InfoOverlay:onInput(keys) + if keys._MOUSE_L and self:get_key() == 'WORK_DETAILS' then + self.refresh_search = true + end + return InfoOverlay.super.onInput(self, keys) +end + +function InfoOverlay:matches_filters(unit) + return matches_squad_burrow_filters(unit, self.subviews.subset:getOptionValue(), + self.subviews.squad:getOptionValue(), self.subviews.burrow:getOptionValue()) +end + +-- ---------------------- +-- CandidatesOverlay +-- + +CandidatesOverlay = defclass(CandidatesOverlay, sortoverlay.SortOverlay) +CandidatesOverlay.ATTRS{ + default_pos={x=54, y=8}, + viewscreens='dwarfmode/Info/ADMINISTRATORS/Candidates', + frame={w=27, h=3}, +} + +function CandidatesOverlay:init() + self:addviews{ + widgets.BannerPanel{ + view_id='panel', + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('CANDIDATE', administrators.candidate, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_candidate_search_key}), + curry(restore_allocated_data, administrators.candidate)) +end + +function CandidatesOverlay:get_key() + if administrators.choosing_candidate then + return 'CANDIDATE' + end +end + +function CandidatesOverlay:updateFrames() + local t = is_tabs_in_two_rows() and 2 or 0 + local frame = self.subviews.panel.frame + if frame.t == t then return end + frame.t = t + return true +end + +function CandidatesOverlay:onRenderBody(dc) + CandidatesOverlay.super.onRenderBody(self, dc) + if self:updateFrames() then + self:updateLayout() + end +end + +-- ---------------------- +-- WorkAnimalOverlay +-- + +WorkAnimalOverlay = defclass(WorkAnimalOverlay, overlay.OverlayWidget) +WorkAnimalOverlay.ATTRS{ + default_pos={x=-33, y=12}, + viewscreens='dwarfmode/Info/CREATURES/AssignWorkAnimal', + default_enabled=true, + frame={w=29, h=1}, +} + +function WorkAnimalOverlay:init() + self:addviews{ + widgets.Label{ + view_id='annotations', + frame={t=0, l=0}, + text='', + } + } +end + +local function get_work_animal_counts() + local counts = {} + for _,unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isOwnCiv(unit) or + (not dfhack.units.isWar(unit) and not dfhack.units.isHunter(unit)) + then + goto continue + end + local owner_id = unit.relationship_ids.Pet + if owner_id == -1 then goto continue end + counts[owner_id] = (counts[owner_id] or 0) + 1 + ::continue:: + end + return counts +end + +function WorkAnimalOverlay:onRenderFrame(dc, rect) + local _, sh = dfhack.screen.getWindowSize() + local _, t = get_panel_offsets() + local list_height = sh - (17 + t) + local num_elems = list_height // 3 + local max_elem = math.min(#creatures.work_animal_recipient-1, + creatures.scroll_position_work_animal+num_elems-1) + + local annotations = {} + local counts = get_work_animal_counts() + for idx=creatures.scroll_position_work_animal,max_elem do + table.insert(annotations, NEWLINE) + table.insert(annotations, NEWLINE) + local animal_count = counts[creatures.work_animal_recipient[idx].id] + if animal_count and animal_count > 0 then + table.insert(annotations, {text='[', pen=COLOR_RED}) + table.insert(annotations, ('Assigned work animals: %d'):format(animal_count)) + table.insert(annotations, {text=']', pen=COLOR_RED}) + end + table.insert(annotations, NEWLINE) + end + + self.subviews.annotations.frame.t = t + self.subviews.annotations:setText(annotations) + self.frame.h = list_height + t + + WorkAnimalOverlay.super.onRenderFrame(self, dc, rect) +end + +-- ---------------------- +-- InterrogationOverlay +-- + +InterrogationOverlay = defclass(InterrogationOverlay, sortoverlay.SortOverlay) +InterrogationOverlay.ATTRS{ + default_pos={x=47, y=10}, + viewscreens='dwarfmode/Info/JUSTICE', + frame={w=27, h=9}, +} + +function InterrogationOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='panel', + frame={l=0, t=4, h=5, r=0}, + frame_background=gui.CLEAR_PEN, + frame_style=gui.FRAME_MEDIUM, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=0, t=0, r=0}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + widgets.ToggleHotkeyLabel{ + view_id='include_interviewed', + frame={l=0, t=1, w=23}, + key='CUSTOM_SHIFT_I', + label='Interviewed:', + options={ + {label='Include', value=true, pen=COLOR_GREEN}, + {label='Exclude', value=false, pen=COLOR_RED}, + }, + visible=function() return justice.interrogating end, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + widgets.CycleHotkeyLabel{ + view_id='subset', + frame={l=0, t=2, w=28}, + key='CUSTOM_SHIFT_F', + label='Show:', + options={ + {label='All', value='all', pen=COLOR_GREEN}, + {label='Risky visitors', value='risky', pen=COLOR_RED}, + {label='Other visitors', value='visitors', pen=COLOR_LIGHTRED}, + {label='Residents', value='residents', pen=COLOR_YELLOW}, + {label='Citizens', value='citizens', pen=COLOR_CYAN}, + {label='Animals', value='animals', pen=COLOR_BLUE}, + {label='Deceased or missing', value='deceased', pen=COLOR_MAGENTA}, + {label='Others', value='others', pen=COLOR_GRAY}, + }, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + }, + }, + } + + self:register_handler('INTERROGATING', justice.interrogation_list, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=sortoverlay.get_unit_search_key, + get_elem_id_fn=function(unit) return unit.id end, + matches_filters_fn=self:callback('matches_filters'), + }, + justice.interrogation_list_flag)) + self:register_handler('CONVICTING', justice.conviction_list, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=sortoverlay.get_unit_search_key, + matches_filters_fn=self:callback('matches_filters'), + })) +end + +function InterrogationOverlay:reset() + InterrogationOverlay.super.reset(self) + self.subviews.include_interviewed:setOption(true, false) + self.subviews.subset:setOption('all') +end + +function InterrogationOverlay:get_key() + if justice.interrogating then + return 'INTERROGATING' + elseif justice.convicting then + return 'CONVICTING' + end +end + +local RISKY_PROFESSIONS = utils.invert{ + df.profession.THIEF, + df.profession.MASTER_THIEF, + df.profession.CRIMINAL, +} + +local function is_risky(unit) + if RISKY_PROFESSIONS[unit.profession] or RISKY_PROFESSIONS[unit.profession2] then + return true + end + if dfhack.units.getReadableName(unit):endswith('necromancer') then return true end + return not dfhack.units.isAlive(unit) -- detect intelligent undead +end + +function InterrogationOverlay:matches_filters(unit, flag) + if justice.interrogating then + local include_interviewed = self.subviews.include_interviewed:getOptionValue() + if not include_interviewed and flag == 2 then return false end + end + local subset = self.subviews.subset:getOptionValue() + if subset == 'all' then + return true + elseif dfhack.units.isDead(unit) or not dfhack.units.isActive(unit) then + return subset == 'deceased' + elseif dfhack.units.isInvader(unit) or dfhack.units.isOpposedToLife(unit) + or unit.flags2.visitor_uninvited or unit.flags4.agitated_wilderness_creature + then + return subset == 'others' + elseif dfhack.units.isVisiting(unit) then + local risky = is_risky(unit) + return (subset == 'risky' and risky) or (subset == 'visitors' and not risky) + elseif dfhack.units.isAnimal(unit) then + return subset == 'animals' + elseif dfhack.units.isCitizen(unit) then + return subset == 'citizens' + elseif unit.flags2.roaming_wilderness_population_source then + return subset == 'others' + end + return subset == 'residents' +end + +function InterrogationOverlay:render(dc) + local sw = dfhack.screen.getWindowSize() + local info_panel_border = 31 -- from edges of panel to screen edges + local info_panel_width = sw - info_panel_border + local info_panel_center = info_panel_width // 2 + local panel_x_offset = (info_panel_center + 5) - self.frame_rect.x1 + local frame_w = math.min(panel_x_offset + 37, info_panel_width - 56) + local panel_l = panel_x_offset + local panel_t = is_tabs_in_two_rows() and 4 or 0 + + if self.frame.w ~= frame_w or + self.subviews.panel.frame.l ~= panel_l or + self.subviews.panel.frame.t ~= panel_t + then + self.frame.w = frame_w + self.subviews.panel.frame.l = panel_l + self.subviews.panel.frame.t = panel_t + self:updateLayout() + end + + InterrogationOverlay.super.render(self, dc) +end + +return _ENV diff --git a/plugins/lua/sort/locationselector.lua b/plugins/lua/sort/locationselector.lua new file mode 100644 index 000000000..bdacc1630 --- /dev/null +++ b/plugins/lua/sort/locationselector.lua @@ -0,0 +1,143 @@ +local _ENV = mkmodule('plugins.sort.locationselector') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +local location_selector = df.global.game.main_interface.location_selector + +-- ---------------------- +-- LocationSelectorOverlay +-- + +LocationSelectorOverlay = defclass(LocationSelectorOverlay, sortoverlay.SortOverlay) +LocationSelectorOverlay.ATTRS{ + default_pos={x=48, y=6}, + viewscreens='dwarfmode/LocationSelector', + frame={w=26, h=3}, +} + +local function add_spheres(hf, spheres) + if not hf then return end + for _, sphere in ipairs(hf.info.spheres.spheres) do + spheres[sphere] = true + end +end + +local function stringify_spheres(spheres) + local strs = {} + for sphere in pairs(spheres) do + table.insert(strs, df.sphere_type[sphere]) + end + return table.concat(strs, ' ') +end + +local function get_religion_string(religion_id, religion_type) + if religion_id == -1 then return end + local entity + local spheres = {} + if religion_type == 0 then + entity = df.historical_figure.find(religion_id) + add_spheres(entity, spheres) + elseif religion_type == 1 then + entity = df.historical_entity.find(religion_id) + if entity then + for _, deity in ipairs(entity.relations.deities) do + add_spheres(df.historical_figure.find(deity), spheres) + end + end + end + if not entity then return end + return ('%s %s'):format(dfhack.TranslateName(entity.name, true), stringify_spheres(spheres)) +end + +local function get_profession_string(profession) + return df.profession[profession]:gsub('_', ' ') +end + +function LocationSelectorOverlay:init() + local panel = widgets.Panel{ + visible=self:callback('get_key'), + } + panel:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + widgets.BannerPanel{ + frame={l=0, t=2, r=0, h=1}, + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='hide_established', + frame={l=1, t=0, r=1}, + label="Hide established:", + key='CUSTOM_SHIFT_E', + initial_option=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + }, + }, + } + self:addviews{panel} + + self:register_handler('TEMPLE', location_selector.valid_religious_practice_id, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=get_religion_string, + matches_filters_fn=self:callback('matches_temple_filter'), + }, + location_selector.valid_religious_practice)) + self:register_handler('GUILDHALL', location_selector.valid_craft_guild_type, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_profession_string, + matches_filters_fn=self:callback('matches_guildhall_filter'), + })) +end + +function LocationSelectorOverlay:get_key() + if location_selector.choosing_temple_religious_practice then + return 'TEMPLE' + elseif location_selector.choosing_craft_guild then + return 'GUILDHALL' + end +end + +function LocationSelectorOverlay:reset() + LocationSelectorOverlay.super.reset(self) + self.cache = nil + self.subviews.hide_established:setOption(false, false) +end + +function LocationSelectorOverlay:get_cache() + if self.cache then return self.cache end + local cache = {} + for _,location in ipairs(df.global.world.world_data.active_site[0].buildings) do + if df.abstract_building_templest:is_instance(location) then + ensure_keys(cache, 'temple', location.deity_type)[location.deity_data.Religion] = true + elseif df.abstract_building_guildhallst:is_instance(location) then + ensure_keys(cache, 'guildhall')[location.contents.profession] = true + end + end + self.cache = cache + return self.cache +end + +function LocationSelectorOverlay:matches_temple_filter(id, flag) + local hide_established = self.subviews.hide_established:getOptionValue() + return not hide_established or not safe_index(self:get_cache(), 'temple', flag, id) +end + +function LocationSelectorOverlay:matches_guildhall_filter(id) + local hide_established = self.subviews.hide_established:getOptionValue() + return not hide_established or not safe_index(self:get_cache(), 'guildhall', id) +end + +return _ENV diff --git a/plugins/lua/sort/slab.lua b/plugins/lua/sort/slab.lua new file mode 100644 index 000000000..5945763c9 --- /dev/null +++ b/plugins/lua/sort/slab.lua @@ -0,0 +1,114 @@ +local _ENV = mkmodule('plugins.sort.slab') + +local gui = require('gui') +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +local building = df.global.game.main_interface.building + +-- ---------------------- +-- SlabOverlay +-- + +SlabOverlay = defclass(SlabOverlay, sortoverlay.SortOverlay) +SlabOverlay.ATTRS{ + default_pos={x=-40, y=12}, + viewscreens='dwarfmode/ViewSheets/BUILDING/Workshop', + frame={w=57, h=3}, +} + +function SlabOverlay:init() + local panel = widgets.Panel{ + frame_background=gui.CLEAR_PEN, + visible=self:callback('get_key'), + } + panel:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + widgets.BannerPanel{ + frame={l=0, t=2, r=0, h=1}, + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='only_needs_slab', + frame={l=1, t=0, r=1}, + label="Show only citizens who need a slab:", + key='CUSTOM_SHIFT_E', + initial_option=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + }, + }, + } + self:addviews{panel} + + self:register_handler('SLAB', building.filtered_button, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=self:callback('get_search_key'), + matches_filters_fn=self:callback('matches_filters'), + })) +end + +function SlabOverlay:onInput(keys) + if SlabOverlay.super.onInput(self, keys) then return true end + if keys._MOUSE_L and self:get_key() and self:getMousePos() then + return true + end +end + +function SlabOverlay:get_key() + -- DF fails to set building.category back to NONE if there are no units that + -- can be memorialized, so we have to manually check for a populated button vector + if #building.button > 0 and + building.category == df.interface_category_building.SELECT_MEMORIAL_UNIT + then + return 'SLAB' + else + if safe_index(self.state, 'SLAB', 'saved_original') then + -- elements get freed as soon as the screen changes + self.state.SLAB.saved_original = nil + end + end +end + +function SlabOverlay:reset() + SlabOverlay.super.reset(self) + self.subviews.only_needs_slab:setOption(false, false) +end + +local function get_unit(if_button) + local hf = df.historical_figure.find(if_button.spec_id) + return hf and df.unit.find(hf.unit_id) or nil +end + +function SlabOverlay:get_search_key(if_button) + local unit = get_unit(if_button) + if not unit then return if_button.filter_str end + return sortoverlay.get_unit_search_key(unit) +end + +local function needs_slab(if_button) + local unit = get_unit(if_button) + if not unit then return false end + if not dfhack.units.isOwnGroup(unit) then return false end + local info = dfhack.toSearchNormalized(if_button.info) + if not info:find('no slabs engraved', 1, true) then return false end + return info:find('not memorialized', 1, true) or info:find('ghost', 1, true) +end + +function SlabOverlay:matches_filters(if_button) + local only_needs_slab = self.subviews.only_needs_slab:getOptionValue() + return not only_needs_slab or needs_slab(if_button) +end + +return _ENV diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua new file mode 100644 index 000000000..82d7d8fca --- /dev/null +++ b/plugins/lua/sort/sortoverlay.lua @@ -0,0 +1,186 @@ +local _ENV = mkmodule('plugins.sort.sortoverlay') + +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.TranslateName(unit.name, true, true)) -- get English last name +end + +local function copy_to_lua_table(vec) + local tab = {} + for k,v in ipairs(vec) do + tab[k+1] = v + end + return tab +end + +-- ---------------------- +-- SortOverlay +-- + +SortOverlay = defclass(SortOverlay, overlay.OverlayWidget) +SortOverlay.ATTRS{ + default_enabled=true, + hotspot=true, + overlay_onupdate_max_freq_seconds=0, + -- subclasses expected to provide default_pos, viewscreens (single string), and frame + -- viewscreens should be the top-level scope within which the search widget state is maintained + -- once the player leaves that scope, widget state will be reset +} + +function SortOverlay:init() + self.state = {} + self.handlers = {} + -- subclasses expected to provide an EditField widget with view_id='search' +end + +function SortOverlay:register_handler(key, vec, search_fn, cleanup_fn) + self.handlers[key] = { + vec=vec, + search_fn=search_fn, + cleanup_fn=cleanup_fn + } +end + +-- handles reset and clean up when the player exits the handled scope +function SortOverlay:overlay_onupdate() + if self.overlay_onupdate_max_freq_seconds == 0 and + not dfhack.gui.matchFocusString(self.viewscreens, dfhack.gui.getDFViewscreen(true)) + then + for key,data in pairs(self.state) do + local cleanup_fn = safe_index(self.handlers, key, 'cleanup_fn') + if cleanup_fn then + cleanup_fn(data) + end + end + self:reset() + self.overlay_onupdate_max_freq_seconds = 300 + end +end + +function SortOverlay:reset() + self.state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) +end + +-- returns the current context key for dereferencing the handler +-- subclasses must override +function SortOverlay:get_key() + return nil +end + +-- handles saving/restoring search strings when the player moves between different contexts +function SortOverlay:onRenderBody(dc) + if next(self.state) then + local key = self:get_key() + if self.state.cur_key ~= key then + self.state.cur_key = key + local prev_text = key and ensure_key(self.state, key).prev_text or '' + self.subviews.search:setText(prev_text) + self:do_search(self.subviews.search.text, true) + end + end + self.overlay_onupdate_max_freq_seconds = 0 + SortOverlay.super.onRenderBody(self, dc) +end + +function SortOverlay:onInput(keys) + if keys._MOUSE_R and self.subviews.search.focus and self:get_key() then + self.subviews.search:setFocus(false) + return true + end + return SortOverlay.super.onInput(self, keys) +end + +function SortOverlay:do_search(text, force_full_search) + if not force_full_search and not next(self.state) and text == '' then return end + -- the EditField state is guaranteed to be consistent with the current + -- context since when clicking to switch tabs, onRenderBody is always called + -- before this text_input callback, even if a key is pressed before the next + -- graphical frame would otherwise be printed. if this ever becomes untrue, + -- then we can add an on_char handler to the EditField that also checks for + -- context transitions. + local key = self:get_key() + if not key then return end + local prev_text = ensure_key(self.state, key).prev_text + -- some screens reset their contents between context switches; regardless, + -- a switch back to the context should results in an incremental search + local incremental = not force_full_search and prev_text and text:startswith(prev_text) + local handler = self.handlers[key] + handler.search_fn(handler.vec, self.state[key], text, incremental) + self.state[key].prev_text = text +end + +local function filter_vec(fns, flags_vec, vec, text, erase_fn) + if fns.matches_filters_fn or text ~= '' then + local search_tokens = text:split() + for idx = #vec-1,0,-1 do + local flag = flags_vec and flags_vec[idx] or nil + local search_key = fns.get_search_key_fn and fns.get_search_key_fn(vec[idx], flag) or nil + if (search_key and not utils.search_text(search_key, search_tokens)) or + (fns.matches_filters_fn and not fns.matches_filters_fn(vec[idx], flag)) + then + erase_fn(idx) + end + end + end +end + +function single_vector_search(fns, vec, data, text, incremental) + vec = utils.getval(vec) + if not data.saved_original then + data.saved_original = copy_to_lua_table(vec) + data.saved_original_size = #vec + elseif not incremental then + vec:assign(data.saved_original) + vec:resize(data.saved_original_size) + end + filter_vec(fns, nil, vec, text, function(idx) vec:erase(idx) end) + data.saved_visible = copy_to_lua_table(vec) + data.saved_visible_size = #vec + if fns.get_sort_fn then + table.sort(data.saved_visible, fns.get_sort_fn()) + vec:assign(data.saved_visible) + vec:resize(data.saved_visible_size) + end +end + +-- doesn't support sorting since nothing that uses this needs it yet +function flags_vector_search(fns, flags_vec, vec, data, text, incremental) + local get_elem_id_fn = fns.get_elem_id_fn or function(elem) return elem end + flags_vec, vec = utils.getval(flags_vec), utils.getval(vec) + if not data.saved_original then + -- we save the sizes since trailing nils get lost in the lua -> vec assignment + data.saved_original = copy_to_lua_table(vec) + data.saved_original_size = #vec + data.saved_flags = copy_to_lua_table(flags_vec) + data.saved_flags_size = #flags_vec + data.saved_idx_map = {} + for idx,elem in ipairs(data.saved_original) do + data.saved_idx_map[get_elem_id_fn(elem)] = idx -- 1-based idx + end + else -- sync flag changes to saved vector + for idx,elem in ipairs(vec) do -- 0-based idx + data.saved_flags[data.saved_idx_map[get_elem_id_fn(elem)]] = flags_vec[idx] + end + end + + if not incremental then + vec:assign(data.saved_original) + vec:resize(data.saved_original_size) + flags_vec:assign(data.saved_flags) + flags_vec:resize(data.saved_flags_size) + end + + filter_vec(fns, flags_vec, vec, text, function(idx) + vec:erase(idx) + flags_vec:erase(idx) + end) +end + +return _ENV diff --git a/plugins/lua/sort/unitselector.lua b/plugins/lua/sort/unitselector.lua new file mode 100644 index 000000000..377ef0551 --- /dev/null +++ b/plugins/lua/sort/unitselector.lua @@ -0,0 +1,280 @@ +local _ENV = mkmodule('plugins.sort.unitselector') + +local info = require('plugins.sort.info') +local gui = require('gui') +local sortoverlay = require('plugins.sort.sortoverlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +local unit_selector = df.global.game.main_interface.unit_selector + +-- ---------------------- +-- UnitSelectorOverlay +-- + +local WIDGET_WIDTH = 31 + +UnitSelectorOverlay = defclass(UnitSelectorOverlay, sortoverlay.SortOverlay) +UnitSelectorOverlay.ATTRS{ + default_pos={x=62, y=6}, + viewscreens='dwarfmode/UnitSelector', + frame={w=31, h=1}, + -- pen, pit, chain, and cage assignment are handled by dedicated screens + -- squad fill position screen has a specialized overlay + -- we *could* add search functionality to vanilla screens for pit and cage, + -- but then we'd have to handle the itemid vector + handled_screens={ + ZONE_BEDROOM_ASSIGNMENT='already', + ZONE_OFFICE_ASSIGNMENT='already', + ZONE_DINING_HALL_ASSIGNMENT='already', + ZONE_TOMB_ASSIGNMENT='already', + OCCUPATION_ASSIGNMENT='selected', + SQUAD_KILL_ORDER='selected', + }, +} + +local function get_unit_id_search_key(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + return sortoverlay.get_unit_search_key(unit) +end + +function UnitSelectorOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, w=WIDGET_WIDTH, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handlers() +end + +function UnitSelectorOverlay:register_handlers() + for name,flags_vec in pairs(self.handled_screens) do + self:register_handler(name, unit_selector.unid, + curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_unit_id_search_key}, + unit_selector[flags_vec])) + end +end + +function UnitSelectorOverlay:get_key() + local key = df.unit_selector_context_type[unit_selector.context] + if self.handled_screens[key] then + return key + end +end + +function UnitSelectorOverlay:onRenderBody(dc) + UnitSelectorOverlay.super.onRenderBody(self, dc) + if self.refresh_search then + self.refresh_search = nil + self:do_search(self.subviews.search.text) + end +end + +function UnitSelectorOverlay:onInput(keys) + if UnitSelectorOverlay.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L then + self.refresh_search = true + end +end + +-- ----------------------- +-- WorkerAssignmentOverlay +-- + +WorkerAssignmentOverlay = defclass(WorkerAssignmentOverlay, UnitSelectorOverlay) +WorkerAssignmentOverlay.ATTRS{ + default_pos={x=6, y=6}, + viewscreens='dwarfmode/UnitSelector', + frame={w=31, h=1}, + handled_screens={WORKER_ASSIGNMENT='selected'}, +} + +-- ----------------------- +-- BurrowAssignmentOverlay +-- + +local DEFAULT_OVERLAY_WIDTH = 58 + +BurrowAssignmentOverlay = defclass(BurrowAssignmentOverlay, UnitSelectorOverlay) +BurrowAssignmentOverlay.ATTRS{ + viewscreens='dwarfmode/UnitSelector', + frame={w=DEFAULT_OVERLAY_WIDTH, h=5}, + handled_screens={BURROW_ASSIGNMENT='selected'}, +} + +local function get_screen_width() + local sw = dfhack.screen.getWindowSize() + return sw +end + +local function toggle_all() + if #unit_selector.unid == 0 then return end + local burrow = df.burrow.find(unit_selector.burrow_id) + if not burrow then return end + local target_state = unit_selector.selected[0] == 0 + local target_val = target_state and 1 or 0 + for i,unit_id in ipairs(unit_selector.unid) do + local unit = df.unit.find(unit_id) + if unit then + dfhack.burrows.setAssignedUnit(burrow, unit, target_state) + unit_selector.selected[i] = target_val + end + end +end + +function BurrowAssignmentOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='top_mask', + frame={l=WIDGET_WIDTH, r=0, t=0, h=1}, + frame_background=gui.CLEAR_PEN, + visible=function() return get_screen_width() >= 144 end, + }, + widgets.Panel{ + view_id='wide_mask', + frame={r=0, t=1, h=2, w=DEFAULT_OVERLAY_WIDTH}, + frame_background=gui.CLEAR_PEN, + visible=function() return get_screen_width() >= 144 end, + }, + widgets.Panel{ + view_id='narrow_mask', + frame={l=0, t=1, h=2, w=24}, + frame_background=gui.CLEAR_PEN, + visible=function() return get_screen_width() < 144 end, + }, + widgets.BannerPanel{ + view_id='subset_panel', + frame={l=0, t=1, w=WIDGET_WIDTH, h=1}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='subset', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_F', + label='Show:', + options={ + {label='All', value='all', pen=COLOR_GREEN}, + {label='Military', value='military', pen=COLOR_YELLOW}, + {label='Civilians', value='civilian', pen=COLOR_CYAN}, + {label='Burrowed', value='burrow', pen=COLOR_MAGENTA}, + }, + on_change=function(value) + local squad = self.subviews.squad + local burrow = self.subviews.burrow + squad.visible = false + burrow.visible = false + if value == 'military' then + squad.options = info.get_squad_options() + squad:setOption('all') + squad.visible = true + elseif value == 'burrow' then + burrow.options = info.get_burrow_options() + burrow:setOption('all') + burrow.visible = true + end + self:do_search(self.subviews.search.text, true) + end, + }, + }, + }, + widgets.BannerPanel{ + view_id='subfilter_panel', + frame={l=0, t=2, w=WIDGET_WIDTH, h=1}, + visible=function() + local subset = self.subviews.subset:getOptionValue() + return subset == 'military' or subset == 'burrow' + end, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='squad', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_S', + label='Squad:', + options={ + {label='Any', value='all', pen=COLOR_GREEN}, + }, + visible=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + widgets.CycleHotkeyLabel{ + view_id='burrow', + frame={l=1, t=0, r=1}, + key='CUSTOM_SHIFT_B', + label='Burrow:', + options={ + {label='Any', value='all', pen=COLOR_GREEN}, + }, + visible=false, + on_change=function() self:do_search(self.subviews.search.text, true) end, + }, + }, + }, + widgets.BannerPanel{ + frame={r=0, t=4, w=25, h=1}, + subviews={ + widgets.HotkeyLabel{ + frame={l=1, t=0, r=1}, + label='Select all/none', + key='CUSTOM_CTRL_A', + on_activate=toggle_all, + }, + }, + }, + } +end + +function BurrowAssignmentOverlay:register_handlers() + for name,flags_vec in pairs(self.handled_screens) do + self:register_handler(name, unit_selector.unid, + curry(sortoverlay.flags_vector_search, { + get_search_key_fn=get_unit_id_search_key, + matches_filters_fn=self:callback('matches_filters'), + }, + unit_selector[flags_vec])) + end +end + +function BurrowAssignmentOverlay:matches_filters(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return false end + return info.matches_squad_burrow_filters(unit, self.subviews.subset:getOptionValue(), + self.subviews.squad:getOptionValue(), self.subviews.burrow:getOptionValue()) +end + +local function clicked_on_mask(self, keys) + if not keys._MOUSE_L then return false end + for _,mask in ipairs{'top_mask', 'wide_mask', 'narrow_mask'} do + if utils.getval(self.subviews[mask].visible) then + if self.subviews[mask]:getMousePos() then + return true + end + end + end + return false +end + +function BurrowAssignmentOverlay:onInput(keys) + return BurrowAssignmentOverlay.super.onInput(self, keys) or + clicked_on_mask(self, keys) +end + +function BurrowAssignmentOverlay:onRenderFrame(dc, rect) + local sw = get_screen_width() + self.frame.w = math.min(DEFAULT_OVERLAY_WIDTH, sw - 94) + BurrowAssignmentOverlay.super.onRenderFrame(self, dc, rect) +end + +return _ENV diff --git a/plugins/lua/sort/world.lua b/plugins/lua/sort/world.lua new file mode 100644 index 000000000..a4cf6c0e5 --- /dev/null +++ b/plugins/lua/sort/world.lua @@ -0,0 +1,88 @@ +local _ENV = mkmodule('plugins.sort.world') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +-- ---------------------- +-- WorldOverlay +-- + +WorldOverlay = defclass(WorldOverlay, sortoverlay.SortOverlay) +WorldOverlay.ATTRS{ + default_pos={x=-18, y=2}, + viewscreens='world/ARTIFACTS', + frame={w=40, h=1}, +} + +local function get_world_artifact_search_key(artifact, rumor) + local search_key = ('%s %s'):format(dfhack.TranslateName(artifact.name, true), + dfhack.items.getDescription(artifact.item, 0)) + if rumor then + local hf = df.historical_figure.find(rumor.hfid) + if hf then + search_key = ('%s %s %s'):format(search_key, + dfhack.TranslateName(hf.name), + dfhack.TranslateName(hf.name, true)) + end + local ws = df.world_site.find(rumor.stid) + if ws then + search_key = ('%s %s'):format(search_key, + dfhack.TranslateName(ws.name, true)) + end + else + local hf = df.historical_figure.find(artifact.holder_hf) + if hf then + local unit = df.unit.find(hf.unit_id) + if unit then + search_key = ('%s %s'):format(search_key, + dfhack.units.getReadableName(unit)) + end + end + end + return search_key +end + +local function cleanup_artifact_vectors(data) + local vs_world = dfhack.gui.getDFViewscreen(true) + vs_world.artifact:assign(data.saved_original) + vs_world.artifact:resize(data.saved_original_size) + vs_world.artifact_arl:assign(data.saved_flags) + vs_world.artifact_arl:resize(data.saved_flags_size) +end + +function WorldOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('ARTIFACTS', + function() return dfhack.gui.getDFViewscreen(true).artifact end, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=get_world_artifact_search_key, + get_elem_id_fn=function(artifact_record) return artifact_record.id end, + }, + function() return dfhack.gui.getDFViewscreen(true).artifact_arl end), + cleanup_artifact_vectors) +end + +function WorldOverlay:get_key() + local scr = dfhack.gui.getDFViewscreen(true) + if scr.view_mode == df.world_view_mode_type.ARTIFACTS then + return 'ARTIFACTS' + end +end + +return _ENV diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index 4707c97ad..1173fa2d9 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -262,6 +262,45 @@ local function do_export() export_view = export_view and export_view:raise() or StockpilesExportScreen{}:show() end +-------------------- +-- ConfigModal +-------------------- + +ConfigModal = defclass(ConfigModal, gui.ZScreenModal) +ConfigModal.ATTRS{ + focus_path='stockpiles_config', + on_close=DEFAULT_NIL, +} + +function ConfigModal:init() + local sp = dfhack.gui.getSelectedStockpile(true) + local cur_setting = false + if sp then + local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1] + cur_setting = config.melt_masterworks == 1 + end + + self:addviews{ + widgets.Window{ + frame={w=35, h=10}, + frame_title='Advanced logistics settings', + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='melt_masterworks', + frame={l=0, t=0}, + key='CUSTOM_M', + label='Melt masterworks', + initial_option=cur_setting, + }, + }, + }, + } +end + +function ConfigModal:onDismiss() + self.on_close{melt_masterworks=self.subviews.melt_masterworks:getOptionValue()} +end + -------------------- -- MinimizeButton -------------------- @@ -364,13 +403,15 @@ StockpilesOverlay.ATTRS{ function StockpilesOverlay:init() self.minimized = false + local function is_expanded() + return not self.minimized + end + local main_panel = widgets.Panel{ view_id='main', frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, - visible=function() - return not self.minimized - end, + visible=is_expanded, subviews={ -- widgets.HotkeyLabel{ -- frame={t=0, l=0}, @@ -440,13 +481,22 @@ function StockpilesOverlay:init() } self:addviews{ - main_panel, MinimizeButton{ + main_panel, + MinimizeButton{ frame={t=0, r=9}, - get_minimized_fn=function() - return self.minimized - end, + get_minimized_fn=function() return self.minimized end, on_click=self:callback('toggleMinimized'), }, + widgets.ConfigureButton{ + frame={t=0, r=5}, + on_click=function() ConfigModal{on_close=self:callback('on_custom_config')}:show() end, + visible=is_expanded, + }, + widgets.HelpButton{ + frame={t=0, r=1}, + command='stockpiles', + visible=is_expanded, + }, } end @@ -475,7 +525,16 @@ function StockpilesOverlay:toggleLogisticsFeature(feature) -- logical xor logistics.logistics_setStockpileConfig(config.stockpile_number, (feature == 'melt') ~= (config.melt == 1), (feature == 'trade') ~= (config.trade == 1), - (feature == 'dump') ~= (config.dump == 1), (feature == 'train') ~= (config.train == 1)) + (feature == 'dump') ~= (config.dump == 1), (feature == 'train') ~= (config.train == 1), + config.melt_masterworks == 1) +end + +function StockpilesOverlay:on_custom_config(custom) + local sp = dfhack.gui.getSelectedStockpile(true) + if not sp then return end + local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1] + logistics.logistics_setStockpileConfig(config.stockpile_number, + config.melt == 1, config.trade == 1, config.dump == 1, config.train == 1, custom.melt_masterworks) end function StockpilesOverlay:toggleMinimized() diff --git a/plugins/lua/stocks.lua b/plugins/lua/stocks.lua new file mode 100644 index 000000000..b87052530 --- /dev/null +++ b/plugins/lua/stocks.lua @@ -0,0 +1,55 @@ +local _ENV = mkmodule('plugins.stocks') + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local stocks = df.global.game.main_interface.stocks + +local function collapse_all() + local num_sections = #stocks.current_type_a_expanded + for idx=0,num_sections-1 do + stocks.current_type_a_expanded[idx] = false + end + stocks.i_height = num_sections * 3 +end + +-- ------------------- +-- StocksOverlay +-- + +StocksOverlay = defclass(StocksOverlay, overlay.OverlayWidget) +StocksOverlay.ATTRS{ + default_pos={x=-3,y=-20}, + default_enabled=true, + viewscreens='dwarfmode/Stocks', + frame={w=27, h=5}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function StocksOverlay:init() + self:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='collapse all', + key='CUSTOM_CTRL_X', + on_activate=collapse_all, + }, + widgets.Label{ + frame={t=2, l=0}, + text = 'Shift+Scroll', + text_pen=COLOR_LIGHTGREEN, + }, + widgets.Label{ + frame={t=2, l=12}, + text = ': fast scroll', + }, + } +end + +OVERLAY_WIDGETS = { + overlay=StocksOverlay, +} + +return _ENV diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua new file mode 100644 index 000000000..eb984093b --- /dev/null +++ b/plugins/lua/zone.lua @@ -0,0 +1,1092 @@ +local _ENV = mkmodule('plugins.zone') + +local gui = require('gui') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') + +local CH_UP = string.char(30) +local CH_DN = string.char(31) +local CH_MALE = string.char(11) +local CH_FEMALE = string.char(12) +local CH_NEUTER = '?' + +local DISPOSITION = { + NONE={label='Unknown', value=0}, + PET={label='Pet', value=1}, + TAME={label='Domesticated', value=2}, + TRAINED={label='Partially trained', value=3}, + WILD_TRAINABLE={label='Wild (trainable)', value=4}, + WILD_UNTRAINABLE={label='Wild (untrainable)', value=5}, + HOSTILE={label='Hostile', value=6}, +} +local DISPOSITION_REVMAP = {} +for k, v in pairs(DISPOSITION) do + DISPOSITION_REVMAP[v.value] = k +end + +-- ------------------- +-- AssignAnimal +-- + +local STATUS_COL_WIDTH = 18 +local DISPOSITION_COL_WIDTH = 18 +local GENDER_COL_WIDTH = 6 +local SLIDER_LABEL_WIDTH = math.max(STATUS_COL_WIDTH, DISPOSITION_COL_WIDTH) + 4 +local SLIDER_WIDTH = 48 + +AssignAnimal = defclass(AssignAnimal, widgets.Window) +AssignAnimal.ATTRS { + frame_title='Animal and prisoner assignment', + frame={w=6+SLIDER_WIDTH*2, h=47}, + resizable=true, + resize_min={h=27}, + target_name=DEFAULT_NIL, + status=DEFAULT_NIL, + status_revmap=DEFAULT_NIL, + get_status=DEFAULT_NIL, + get_allow_vermin=DEFAULT_NIL, + get_multi_select=DEFAULT_NIL, + attach=DEFAULT_NIL, + initial_min_disposition=DISPOSITION.PET.value, +} + +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') +end + +local function sort_by_race_desc(a, b) + if a.data.race == b.data.race then + return a.data.gender < b.data.gender + end + return a.data.race < b.data.race +end + +local function sort_by_race_asc(a, b) + if a.data.race == b.data.race then + return a.data.gender < b.data.gender + end + return a.data.race > b.data.race +end + +local function sort_by_name_desc(a, b) + if a.search_key == b.search_key then + return sort_by_race_desc(a, b) + end + return a.search_key < b.search_key +end + +local function sort_by_name_asc(a, b) + if a.search_key == b.search_key then + return sort_by_race_desc(a, b) + end + return a.search_key > b.search_key +end + +local function sort_by_gender_desc(a, b) + if a.data.gender == b.data.gender then + return sort_by_race_desc(a, b) + end + return a.data.gender < b.data.gender +end + +local function sort_by_gender_asc(a, b) + if a.data.gender == b.data.gender then + return sort_by_race_desc(a, b) + end + return a.data.gender > b.data.gender +end + +local function sort_by_disposition_desc(a, b) + if a.data.disposition == b.data.disposition then + return sort_by_race_desc(a, b) + end + return a.data.disposition < b.data.disposition +end + +local function sort_by_disposition_asc(a, b) + if a.data.disposition == b.data.disposition then + return sort_by_race_desc(a, b) + end + return a.data.disposition > b.data.disposition +end + +local function sort_by_status_desc(a, b) + if a.data.status == b.data.status then + return sort_by_race_desc(a, b) + end + return a.data.status < b.data.status +end + +local function sort_by_status_asc(a, b) + if a.data.status == b.data.status then + return sort_by_race_desc(a, b) + end + return a.data.status > b.data.status +end + +function AssignAnimal:init() + local status_options = {} + for k, v in ipairs(self.status_revmap) do + status_options[k] = self.status[v] + end + + local disposition_options = {} + for k, v in ipairs(DISPOSITION_REVMAP) do + disposition_options[k] = DISPOSITION[v] + end + + local can_assign_pets = self.initial_min_disposition == DISPOSITION.PET.value + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=0, t=0, w=26}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='status'..CH_DN, value=sort_by_status_desc}, + {label='status'..CH_UP, value=sort_by_status_asc}, + {label='disposition'..CH_DN, value=sort_by_disposition_desc}, + {label='disposition'..CH_UP, value=sort_by_disposition_asc}, + {label='gender'..CH_DN, value=sort_by_gender_desc}, + {label='gender'..CH_UP, value=sort_by_gender_asc}, + {label='race'..CH_DN, value=sort_by_race_desc}, + {label='race'..CH_UP, value=sort_by_race_asc}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_status_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={l=35, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + }, + widgets.Panel{ + frame={t=2, l=0, w=SLIDER_WIDTH, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_status', + frame={l=0, t=0, w=SLIDER_LABEL_WIDTH}, + label='Min status:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options=status_options, + initial_option=1, + on_change=function(val) + if self.subviews.max_status:getOptionValue() < val then + self.subviews.max_status:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_status', + frame={r=1, t=0, w=SLIDER_LABEL_WIDTH}, + label='Max status:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options=status_options, + initial_option=#self.status_revmap, + on_change=function(val) + if self.subviews.min_status:getOptionValue() > val then + self.subviews.min_status:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=#self.status_revmap, + get_left_idx_fn=function() + return self.subviews.min_status:getOptionValue() + end, + get_right_idx_fn=function() + return self.subviews.max_status:getOptionValue() + end, + on_left_change=function(idx) self.subviews.min_status:setOption(idx, true) end, + on_right_change=function(idx) self.subviews.max_status:setOption(idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=7, l=0, w=SLIDER_WIDTH, h=4}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='min_disposition', + frame={l=0, t=0, w=SLIDER_LABEL_WIDTH}, + label='Min disposition:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=disposition_options, + initial_option=self.initial_min_disposition, + on_change=function(val) + if self.subviews.max_disposition:getOptionValue() < val then + self.subviews.max_disposition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_disposition', + frame={r=1, t=0, w=SLIDER_LABEL_WIDTH}, + label='Max disposition:', + label_below=true, + key_back='CUSTOM_SHIFT_E', + key='CUSTOM_SHIFT_R', + options=disposition_options, + initial_option=DISPOSITION.HOSTILE.value, + on_change=function(val) + if self.subviews.min_disposition:getOptionValue() > val then + self.subviews.min_disposition:setOption(val) + end + self:refresh_list() + end, + }, + widgets.RangeSlider{ + frame={l=0, t=3}, + num_stops=6, + get_left_idx_fn=function() + return self.subviews.min_disposition:getOptionValue() + end, + get_right_idx_fn=function() + return self.subviews.max_disposition:getOptionValue() + end, + on_left_change=function(idx) self.subviews.min_disposition:setOption(idx, true) end, + on_right_change=function(idx) self.subviews.max_disposition:setOption(idx, true) end, + }, + }, + }, + widgets.Panel{ + frame={t=3, l=SLIDER_WIDTH+2, r=0, h=3}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='egg', + frame={l=0, t=0, w=23}, + key_back='CUSTOM_SHIFT_B', + key='CUSTOM_SHIFT_N', + label='Egg layers:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='graze', + frame={l=0, t=2, w=20}, + key_back='CUSTOM_SHIFT_T', + key='CUSTOM_SHIFT_Y', + label='Grazers:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + }, + }, + widgets.Panel{ + view_id='list_panel', + frame={t=12, l=0, r=0, b=4+(can_assign_pets and 0 or 1)}, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_status', + frame={t=0, l=0, w=7}, + options={ + {label='status', value=sort_noop}, + {label='status'..CH_DN, value=sort_by_status_desc}, + {label='status'..CH_UP, value=sort_by_status_asc}, + }, + initial_option=sort_by_status_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_status'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_disposition', + frame={t=0, l=STATUS_COL_WIDTH+2, w=12}, + options={ + {label='disposition', value=sort_noop}, + {label='disposition'..CH_DN, value=sort_by_disposition_desc}, + {label='disposition'..CH_UP, value=sort_by_disposition_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_disposition'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_gender', + frame={t=0, l=STATUS_COL_WIDTH+2+DISPOSITION_COL_WIDTH+2, w=7}, + options={ + {label='gender', value=sort_noop}, + {label='gender'..CH_DN, value=sort_by_gender_desc}, + {label='gender'..CH_UP, value=sort_by_gender_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_gender'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_race', + frame={t=0, l=STATUS_COL_WIDTH+2+DISPOSITION_COL_WIDTH+2+GENDER_COL_WIDTH+2, w=5}, + options={ + {label='race', value=sort_noop}, + {label='race'..CH_DN, value=sort_by_race_desc}, + {label='race'..CH_UP, value=sort_by_race_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_race'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={t=0, l=STATUS_COL_WIDTH+2+DISPOSITION_COL_WIDTH+2+GENDER_COL_WIDTH+2+7, w=5}, + options={ + {label='name', value=sort_noop}, + {label='name'..CH_DN, value=sort_by_name_desc}, + {label='name'..CH_UP, value=sort_by_name_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_name'), + }, + widgets.FilteredList{ + view_id='list', + frame={l=0, t=2, r=0, b=0}, + on_submit=self:callback('toggle_item'), + on_submit2=self:callback('toggle_range'), + on_select=self:callback('select_item'), + }, + } + }, + widgets.HotkeyLabel{ + frame={l=0, b=2+(can_assign_pets and 0 or 1)}, + label='Assign all/none', + key='CUSTOM_CTRL_A', + on_activate=self:callback('toggle_visible'), + visible=self.get_multi_select, + auto_width=true, + }, + widgets.WrappedLabel{ + frame={b=0, l=0, r=0}, + text_to_wrap=function() + return 'Click to assign/unassign to ' .. self.target_name .. '.' .. + (self.get_multi_select() and ' Shift click to assign/unassign a range.' or '') .. + (not can_assign_pets and '\nNote that pets cannot be assigned to cages or restraints.' or '') + end, + }, + } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self.subviews.list:setChoices(self:get_choices()) +end + +function AssignAnimal:refresh_list(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_status', 'sort_disposition', 'sort_gender', 'sort_race', 'sort_name'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.list + local saved_filter = list:getFilter() + list:setFilter('') + list:setChoices(self:get_choices(), list:getSelected()) + list:setFilter(saved_filter) +end + +function add_words(words, str) + for word in dfhack.toSearchNormalized(str):gmatch("[%w-]+") do + table.insert(words, word:lower()) + end +end + +function make_search_key(desc, race_raw) + local words = {} + add_words(words, desc) + if race_raw then + add_words(words, race_raw.name[0]) + end + return table.concat(words, ' ') +end + +function AssignAnimal:make_choice_text(data) + local gender_ch = CH_NEUTER + if data.gender == df.pronoun_type.she then + gender_ch = CH_FEMALE + elseif data.gender == df.pronoun_type.he then + gender_ch = CH_MALE + end + return { + {width=STATUS_COL_WIDTH, text=function() return self.status[self.status_revmap[data.status]].label end}, + {gap=2, width=DISPOSITION_COL_WIDTH, text=function() return DISPOSITION[DISPOSITION_REVMAP[data.disposition]].label end}, + {gap=2, width=GENDER_COL_WIDTH, text=gender_ch}, + {gap=2, text=data.desc}, + } +end + +local function get_general_ref(unit_or_vermin, ref_type) + local is_unit = df.unit:is_instance(unit_or_vermin) + return dfhack[is_unit and 'units' or 'items'].getGeneralRef(unit_or_vermin, ref_type) +end + +local function get_cage_ref(unit_or_vermin) + return get_general_ref(unit_or_vermin, df.general_ref_type.CONTAINED_IN_ITEM) +end + +local function get_built_cage(item_cage) + if not item_cage then return end + local built_cage_ref = dfhack.items.getGeneralRef(item_cage, df.general_ref_type.BUILDING_HOLDER) + if not built_cage_ref then return end + local built_cage = df.building.find(built_cage_ref.building_id) + if not built_cage then return end + if built_cage:getType() == df.building_type.Cage then + return built_cage + end +end + +local function get_bld_assignments() + local assignments = {} + for _,cage in ipairs(df.global.world.buildings.other.CAGE) do + for _,unit_id in ipairs(cage.assigned_units) do + assignments[unit_id] = cage + end + end + for _,chain in ipairs(df.global.world.buildings.other.CHAIN) do + if chain.assigned then + assignments[chain.assigned.id] = chain + end + end + return assignments +end + +local function get_unit_disposition(unit) + local disposition = DISPOSITION.NONE + if dfhack.units.isInvader(unit) or dfhack.units.isOpposedToLife(unit) then + disposition = DISPOSITION.HOSTILE + elseif dfhack.units.isPet(unit) then + disposition = DISPOSITION.PET + elseif dfhack.units.isDomesticated(unit) then + disposition = DISPOSITION.TAME + elseif dfhack.units.isTame(unit) then + disposition = DISPOSITION.TRAINED + elseif dfhack.units.isTamable(unit) then + disposition = DISPOSITION.WILD_TRAINABLE + else + disposition = DISPOSITION.WILD_UNTRAINABLE + end + return disposition.value +end + +local function get_item_disposition(item) + if dfhack.units.casteFlagSet(item.race, item.caste, df.caste_raw_flags.OPPOSED_TO_LIFE) then + return DISPOSITION.HOSTILE.value + end + + if df.item_petst:is_instance(item) then + if item.owner_id > -1 then + return DISPOSITION.PET.value + end + return DISPOSITION.TAME.value + end + + if dfhack.units.casteFlagSet(item.race, item.caste, df.caste_raw_flags.PET) or + dfhack.units.casteFlagSet(item.race, item.caste, df.caste_raw_flags.PET_EXOTIC) + then + return DISPOSITION.WILD_TRAINABLE.value + end + return DISPOSITION.WILD_UNTRAINABLE.value +end + +local function is_assignable_unit(unit) + return dfhack.units.isActive(unit) and + ((dfhack.units.isAnimal(unit) and dfhack.units.isOwnCiv(unit)) or get_cage_ref(unit)) and + not dfhack.units.isDead(unit) and + not dfhack.units.isMerchant(unit) and + not dfhack.units.isForest(unit) +end + +local function is_assignable_item(item) + -- all vermin/small pets are assignable + return true +end + +local function get_vermin_desc(vermin, raw) + if not raw then return 'Unknown vermin' end + if vermin.stack_size > 1 then + return ('%s [%d]'):format(raw.name[1], vermin.stack_size) + end + return ('%s'):format(raw.name[0]) +end + +local function get_small_pet_desc(raw) + if not raw then return 'Unknown small pet' end + return ('tame %s'):format(raw.name[0]) +end + +function AssignAnimal:cache_choices() + if self.choices then return self.choices end + + local bld_assignments = get_bld_assignments() + local choices = {} + for _, unit in ipairs(df.global.world.units.active) do + if not is_assignable_unit(unit) then goto continue end + local raw = df.creature_raw.find(unit.race) + local data = { + unit=unit, + desc=dfhack.units.getReadableName(unit), + gender=unit.sex, + race=raw and raw.creature_id or '', + status=self.get_status(unit, bld_assignments), + disposition=get_unit_disposition(unit), + egg=dfhack.units.isEggLayerRace(unit), + graze=dfhack.units.isGrazer(unit), + } + local choice = { + search_key=make_search_key(data.desc, raw), + data=data, + text=self:make_choice_text(data), + } + table.insert(choices, choice) + ::continue:: + end + for _, vermin in ipairs(df.global.world.items.other.VERMIN) do + if not is_assignable_item(vermin) then goto continue end + local raw = df.creature_raw.find(vermin.race) + local data = { + vermin=vermin, + desc=get_vermin_desc(vermin, raw), + gender=df.pronoun_type.it, + race=raw and raw.creature_id or '', + status=self.get_status(vermin, bld_assignments), + disposition=get_item_disposition(vermin), + } + local choice = { + search_key=make_search_key(data.desc, raw), + data=data, + text=self:make_choice_text(data), + } + table.insert(choices, choice) + ::continue:: + end + for _, small_pet in ipairs(df.global.world.items.other.PET) do + if not is_assignable_item(small_pet) then goto continue end + local raw = df.creature_raw.find(small_pet.race) + local data = { + vermin=small_pet, + desc=get_small_pet_desc(raw), + gender=df.pronoun_type.it, + race=raw and raw.creature_id or '', + status=self.get_status(small_pet, bld_assignments), + disposition=get_item_disposition(small_pet), + } + local choice = { + search_key=make_search_key(data.desc, raw), + data=data, + text=self:make_choice_text(data), + } + table.insert(choices, choice) + ::continue:: + end + + self.choices = choices + return choices +end + +function AssignAnimal:get_choices() + local raw_choices = self:cache_choices() + local show_vermin = self.get_allow_vermin() + local min_status = self.subviews.min_status:getOptionValue() + local max_status = self.subviews.max_status:getOptionValue() + local min_disposition = self.subviews.min_disposition:getOptionValue() + local max_disposition = self.subviews.max_disposition:getOptionValue() + local egg = self.subviews.egg:getOptionValue() + local graze = self.subviews.graze:getOptionValue() + local choices = {} + for _,choice in ipairs(raw_choices) do + local data = choice.data + if not show_vermin and data.vermin then goto continue end + if min_status > data.status then goto continue end + if max_status < data.status then goto continue end + if min_disposition > data.disposition then goto continue end + if max_disposition < data.disposition then goto continue end + if egg == 'only' and not data.egg then goto continue end + if egg == 'exclude' and data.egg then goto continue end + if graze == 'only' and not data.graze then goto continue end + if graze == 'exclude' and data.graze then goto continue end + table.insert(choices, choice) + ::continue:: + end + table.sort(choices, self.subviews.sort:getOptionValue()) + return choices +end + +local function get_bld_assigned_vec(bld, unit_or_vermin) + if not bld then return end + return df.unit:is_instance(unit_or_vermin) and bld.assigned_units or bld.assigned_items +end + +local function get_assigned_unit_or_vermin_idx(bld, unit_or_vermin, vec) + vec = vec or get_bld_assigned_vec(bld, unit_or_vermin) + if not vec then return end + for assigned_idx, assigned_id in ipairs(vec) do + if assigned_id == unit_or_vermin.id then + return assigned_idx + end + end +end + +local function unassign_unit_or_vermin(bld, unit_or_vermin) + if not bld then return end + if df.building_chainst:is_instance(bld) then + if bld.assigned == unit_or_vermin then + bld.assigned = nil + end + return + end + local vec = get_bld_assigned_vec(bld, unit_or_vermin) + local idx = get_assigned_unit_or_vermin_idx(bld, unit_or_vermin, vec) + if vec and idx then + vec:erase(idx) + end +end + +local function detach_unit_or_vermin(unit_or_vermin, bld_assignments) + for idx = #unit_or_vermin.general_refs-1, 0, -1 do + local ref = unit_or_vermin.general_refs[idx] + if df.general_ref_building_civzone_assignedst:is_instance(ref) then + unassign_unit_or_vermin(df.building.find(ref.building_id), unit_or_vermin) + unit_or_vermin.general_refs:erase(idx) + ref:delete() + elseif df.general_ref_contained_in_itemst:is_instance(ref) then + local built_cage = get_built_cage(df.item.find(ref.item_id)) + if built_cage and built_cage:getType() == df.building_type.Cage then + unassign_unit_or_vermin(built_cage, unit_or_vermin) + -- unit's general ref will be removed when the unit is released from the cage + end + end + end + bld_assignments = bld_assignments or get_bld_assignments() + if bld_assignments[unit_or_vermin.id] then + unassign_unit_or_vermin(bld_assignments[unit_or_vermin.id], unit_or_vermin) + end +end + +function AssignAnimal:toggle_item_base(choice, target_value, bld_assignments) + local true_value = self.status[self.status_revmap[1]].value + + if target_value == nil then + target_value = choice.data.status ~= true_value + end + + if target_value and choice.data.status == true_value then + return target_value + end + if not target_value and choice.data.status ~= true_value then + return target_value + end + + if self.initial_min_disposition ~= DISPOSITION.PET.value and choice.data.disposition == DISPOSITION.PET.value then + return target_value + end + + local unit_or_vermin = choice.data.unit or choice.data.vermin + detach_unit_or_vermin(unit_or_vermin, bld_assignments) + + if target_value then + local displaced_unit = self.attach(unit_or_vermin) + if displaced_unit then + -- assigning a unit to a restraint can unassign a different unit + for _, c in ipairs(self.subviews.list:getChoices()) do + if c.data.unit == displaced_unit then + c.data.status = self.get_status(displaced_unit) + end + end + end + end + + -- don't pass bld_assignments since it may no longer be valid + choice.data.status = self.get_status(unit_or_vermin) + + return target_value +end + +function AssignAnimal:select_item(idx, choice) + if not dfhack.internal.getModifiers().shift then + self.prev_list_idx = self.subviews.list.list:getSelected() + end +end + +function AssignAnimal:toggle_item(idx, choice) + self:toggle_item_base(choice) +end + +function AssignAnimal:toggle_range(idx, choice) + if not self.get_multi_select() then + self:toggle_item(idx, choice) + return + end + if not self.prev_list_idx then + self:toggle_item(idx, choice) + return + end + local choices = self.subviews.list:getVisibleChoices() + local list_idx = self.subviews.list.list:getSelected() + local bld_assignments = get_bld_assignments() + local target_value + for i = list_idx, self.prev_list_idx, list_idx < self.prev_list_idx and 1 or -1 do + target_value = self:toggle_item_base(choices[i], target_value, bld_assignments) + end + self.prev_list_idx = list_idx +end + +function AssignAnimal:toggle_visible() + local bld_assignments = get_bld_assignments() + local target_value + for _, choice in ipairs(self.subviews.list:getVisibleChoices()) do + target_value = self:toggle_item_base(choice, target_value, bld_assignments) + end +end + +-- ------------------- +-- AssignAnimalScreen +-- + +view = view or nil + +AssignAnimalScreen = defclass(AssignAnimalScreen, gui.ZScreen) +AssignAnimalScreen.ATTRS { + focus_path='zone/assign', + is_valid_ui_state=DEFAULT_NIL, + status=DEFAULT_NIL, + status_revmap=DEFAULT_NIL, + get_status=DEFAULT_NIL, + get_allow_vermin=DEFAULT_NIL, + get_multi_select=DEFAULT_NIL, + attach=DEFAULT_NIL, + initial_min_disposition=DEFAULT_NIL, + target_name=DEFAULT_NIL, +} + +function AssignAnimalScreen:init() + self:addviews{ + AssignAnimal{ + status=self.status, + status_revmap=self.status_revmap, + get_status=self.get_status, + get_allow_vermin=self.get_allow_vermin, + get_multi_select=self.get_multi_select, + attach=self.attach, + initial_min_disposition=self.initial_min_disposition, + target_name=self.target_name, + } + } +end + +function AssignAnimalScreen:onInput(keys) + local handled = AssignAnimalScreen.super.onInput(self, keys) + if not self.is_valid_ui_state() then + if view then + view:dismiss() + end + return + end + if keys._MOUSE_L then + -- if any click is made outside of our window, we need to recheck unit properties + local window = self.subviews[1] + if not window:getMouseFramePos() then + for _, choice in ipairs(self.subviews.list:getChoices()) do + choice.data.status = self.get_status(choice.data.unit or choice.data.vermin) + end + window:refresh_list() + end + end + return handled +end + +function AssignAnimalScreen:onRenderFrame() + if view and not self.is_valid_ui_state() then + view:dismiss() + end +end + +function AssignAnimalScreen:onDismiss() + view = nil +end + +-- ------------------- +-- PasturePondOverlay +-- + +PasturePondOverlay = defclass(PasturePondOverlay, overlay.OverlayWidget) +PasturePondOverlay.ATTRS{ + default_pos={x=7,y=13}, + default_enabled=true, + viewscreens={'dwarfmode/Zone/Some/Pen', 'dwarfmode/Zone/Some/Pond'}, + frame={w=31, h=4}, +} + +local function is_valid_zone() + return df.global.game.main_interface.bottom_mode_selected == df.main_bottom_mode_type.ZONE and + df.global.game.main_interface.civzone.cur_bld and + (df.global.game.main_interface.civzone.cur_bld.type == df.civzone_type.Pen or + df.global.game.main_interface.civzone.cur_bld.type == df.civzone_type.Pond) +end + +local function is_pit_selected() + return df.global.game.main_interface.bottom_mode_selected == df.main_bottom_mode_type.ZONE and + df.global.game.main_interface.civzone.cur_bld and + df.global.game.main_interface.civzone.cur_bld.type == df.civzone_type.Pond +end + +local function attach_to_zone(unit_or_vermin) + local zone = df.global.game.main_interface.civzone.cur_bld + local ref = df.new(df.general_ref_building_civzone_assignedst) + ref.building_id = zone.id; + unit_or_vermin.general_refs:insert('#', ref) + local is_unit = df.unit:is_instance(unit_or_vermin) + local vec = is_unit and zone.assigned_units or zone.assigned_items + utils.insert_sorted(vec, unit_or_vermin.id) +end + +local PASTURE_STATUS = { + NONE={label='Unknown', value=0}, + ASSIGNED_HERE={label='Assigned here', value=1}, + PASTURED={label='In other pasture', value=2}, + PITTED={label='In other pit/pond', value=3}, + RESTRAINED={label='On restraint', value=4}, + BUILT_CAGE={label='In built cage', value=5}, + ITEM_CAGE={label='In stockpiled cage', value=6}, + ROAMING={label='Roaming', value=7}, +} +local PASTURE_STATUS_REVMAP = {} +for k, v in pairs(PASTURE_STATUS) do + PASTURE_STATUS_REVMAP[v.value] = k +end + +local function get_zone_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 + if df.global.game.main_interface.civzone.cur_bld.id == assigned_zone_ref.building_id then + 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 + return PASTURE_STATUS.PASTURED.value + elseif civzone.type == df.civzone_type.Pond then + return PASTURE_STATUS.PITTED.value + else + return PASTURE_STATUS.NONE.value + end + end + end + if get_general_ref(unit_or_vermin, df.general_ref_type.BUILDING_CHAIN) then + return PASTURE_STATUS.RESTRAINED.value + end + local cage_ref = get_cage_ref(unit_or_vermin) + if cage_ref then + if get_built_cage(df.item.find(cage_ref.item_id)) then + return PASTURE_STATUS.BUILT_CAGE.value + else + return PASTURE_STATUS.ITEM_CAGE.value + end + end + bld_assignments = bld_assignments or get_bld_assignments() + if bld_assignments and bld_assignments[unit_or_vermin.id] then + if df.building_chainst:is_instance(bld_assignments[unit_or_vermin.id]) then + return PASTURE_STATUS.RESTRAINED.value + end + return PASTURE_STATUS.BUILT_CAGE.value + end + return PASTURE_STATUS.ROAMING.value +end + +local function show_pasture_pond_screen() + return AssignAnimalScreen{ + is_valid_ui_state=is_valid_zone, + status=PASTURE_STATUS, + status_revmap=PASTURE_STATUS_REVMAP, + get_status=get_zone_status, + get_allow_vermin=is_pit_selected, + get_multi_select=function() return true end, + attach=attach_to_zone, + target_name='pasture/pond/pit', + }:show() +end + +function PasturePondOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0, w=23, h=1}, + label='DFHack assign', + key='CUSTOM_CTRL_T', + on_activate=function() view = view and view:raise() or show_pasture_pond_screen() end, + }, + widgets.TextButton{ + frame={b=0, l=0, w=28, h=1}, + label='DFHack autobutcher', + key='CUSTOM_CTRL_B', + on_activate=function() dfhack.run_script('gui/autobutcher') end, + }, + } +end + +-- ------------------- +-- CageChainOverlay +-- + +CageChainOverlay = defclass(CageChainOverlay, overlay.OverlayWidget) +CageChainOverlay.ATTRS{ + default_pos={x=-40,y=34}, + default_enabled=true, + viewscreens={'dwarfmode/ViewSheets/BUILDING/Cage', 'dwarfmode/ViewSheets/BUILDING/Chain'}, + frame={w=23, h=1}, + frame_background=gui.CLEAR_PEN, +} + +local function is_valid_building() + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld or bld:getBuildStage() ~= bld:getMaxBuildStage() then return false end + local bt = bld:getType() + if bt ~= df.building_type.Cage and bt ~= df.building_type.Chain then return false end + for _,zone in ipairs(bld.relations) do + if zone.type == df.civzone_type.Dungeon then return false end + end + return true +end + +local function is_cage_selected() + local bld = dfhack.gui.getSelectedBuilding(true) + return bld and bld:getType() == df.building_type.Cage +end + +local function attach_to_building(unit_or_vermin) + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld then return end + local is_unit = df.unit:is_instance(unit_or_vermin) + if is_unit and unit_or_vermin.relationship_ids[df.unit_relationship_type.Pet] ~= -1 then + -- pet owners would just release them + return + end + if bld:getType() == df.building_type.Cage then + local vec = is_unit and bld.assigned_units or bld.assigned_items + vec:insert('#', unit_or_vermin.id) + elseif is_unit and bld:getType() == df.building_type.Chain then + local prev_unit = bld.assigned + bld.assigned = unit_or_vermin + return prev_unit + end +end + +local function get_built_chain(unit_or_vermin) + local built_chain_ref = get_general_ref(unit_or_vermin, df.general_ref_type.BUILDING_CHAIN) + if not built_chain_ref then return end + return df.building.find(built_chain_ref.building_id) +end + +local CAGE_STATUS = { + NONE={label='Unknown', value=0}, + ASSIGNED_HERE={label='Assigned here', value=1}, + PASTURED={label='In pasture', value=2}, + PITTED={label='In pit/pond', value=3}, + RESTRAINED={label='On other chain', value=4}, + BUILT_CAGE={label='In built cage', value=5}, + ITEM_CAGE={label='In stockpiled cage', value=6}, + ROAMING={label='Roaming', value=7}, +} +local CAGE_STATUS_REVMAP = {} +for k, v in pairs(CAGE_STATUS) do + CAGE_STATUS_REVMAP[v.value] = k +end + +local function get_cage_status(unit_or_vermin, bld_assignments) + local bld = dfhack.gui.getSelectedBuilding(true) + local is_chain = bld and df.building_chainst:is_instance(bld) + + if is_chain and bld.assigned == unit_or_vermin then + return CAGE_STATUS.ASSIGNED_HERE.value + elseif not is_chain and get_assigned_unit_or_vermin_idx(bld, unit_or_vermin) then + return CAGE_STATUS.ASSIGNED_HERE.value + end + local cage_ref = get_cage_ref(unit_or_vermin) + if cage_ref then + if get_built_cage(df.item.find(cage_ref.item_id)) then + return CAGE_STATUS.BUILT_CAGE.value + end + return CAGE_STATUS.ITEM_CAGE.value + end + local built_chain = get_built_chain(unit_or_vermin) + if built_chain then + if bld and bld == built_chain then + return CAGE_STATUS.ASSIGNED_HERE.value + end + return CAGE_STATUS.RESTRAINED.value + end + 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 + return CAGE_STATUS.PASTURED.value + elseif civzone.type == df.civzone_type.Pond then + return CAGE_STATUS.PITTED.value + else + return CAGE_STATUS.NONE.value + end + end + bld_assignments = bld_assignments or get_bld_assignments() + if bld_assignments and bld_assignments[unit_or_vermin.id] then + if df.building_chainst:is_instance(bld_assignments[unit_or_vermin.id]) then + return CAGE_STATUS.RESTRAINED.value + end + return CAGE_STATUS.BUILT_CAGE.value + end + return CAGE_STATUS.ROAMING.value +end + +local function show_cage_chain_screen() + return AssignAnimalScreen{ + is_valid_ui_state=is_valid_building, + status=CAGE_STATUS, + status_revmap=CAGE_STATUS_REVMAP, + get_status=get_cage_status, + get_allow_vermin=is_cage_selected, + get_multi_select=is_cage_selected, + attach=attach_to_building, + initial_min_disposition=DISPOSITION.TAME.value, + target_name='cage/restraint', + }:show() +end + +function CageChainOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0, r=0, h=1}, + label='DFHack assign', + key='CUSTOM_CTRL_T', + visible=is_valid_building, + on_activate=function() view = view and view:raise() or show_cage_chain_screen() end, + }, + } +end + +OVERLAY_WIDGETS = { + pasturepond=PasturePondOverlay, + cagechain=CageChainOverlay, +} + +return _ENV diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp index b8f6ce706..0a8e18a27 100644 --- a/plugins/manipulator.cpp +++ b/plugins/manipulator.cpp @@ -1875,14 +1875,14 @@ void viewscreen_unitlaborsst::feed(set *events) if (events->count(interface_key::CUSTOM_B)) { - Screen::show(dts::make_unique(units, true, &do_refresh_names), plugin_self); + Screen::show(std::make_unique(units, true, &do_refresh_names), plugin_self); } if (events->count(interface_key::CUSTOM_E)) { vector tmp; tmp.push_back(cur); - Screen::show(dts::make_unique(tmp, false, &do_refresh_names), plugin_self); + Screen::show(std::make_unique(tmp, false, &do_refresh_names), plugin_self); } if (events->count(interface_key::CUSTOM_P)) @@ -1893,11 +1893,11 @@ void viewscreen_unitlaborsst::feed(set *events) has_selected = true; if (has_selected) { - Screen::show(dts::make_unique(units, true), plugin_self); + Screen::show(std::make_unique(units, true), plugin_self); } else { vector tmp; tmp.push_back(cur); - Screen::show(dts::make_unique(tmp, false), plugin_self); + Screen::show(std::make_unique(tmp, false), plugin_self); } } @@ -2286,7 +2286,7 @@ struct unitlist_hook : df::viewscreen_unitlistst { if (units[page].size()) { - Screen::show(dts::make_unique(units[page], cursor_pos[page]), plugin_self); + Screen::show(std::make_unique(units[page], cursor_pos[page]), plugin_self); return; } } diff --git a/plugins/misery.cpp b/plugins/misery.cpp index f241394ae..5e3bf6e1f 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -72,7 +72,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & parameters) { @@ -111,6 +116,19 @@ static command_result orders_command(color_ostream & out, std::vectormanager_orders) { + if (it->item_conditions.size() && it->status.bits.active) { + ++count; + it->status.bits.active = false; + it->status.bits.validated = false; + } + } + out << "Re-checking conditions for " << count << " manager orders." << std::endl; + return CR_OK; +} + +static command_result orders_recheck_current_command(color_ostream & out) +{ + if (game->main_interface.info.work_orders.conditions.open) + { + game->main_interface.info.work_orders.conditions.wq->status.bits.active = false; + } + else + { + out << COLOR_LIGHTRED << "Order conditions is not open" << std::endl; + return CR_FAILURE; + } + return CR_OK; +} diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index 784e17129..c94397956 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -1,3 +1,4 @@ +#include "df/enabler.h" #include "df/viewscreen_adopt_regionst.h" #include "df/viewscreen_choose_game_typest.h" #include "df/viewscreen_choose_start_sitest.h" @@ -7,6 +8,7 @@ #include "df/viewscreen_initial_prepst.h" #include "df/viewscreen_legendsst.h" #include "df/viewscreen_loadgamest.h" +#include "df/viewscreen_new_arenast.h" #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_savegamest.h" #include "df/viewscreen_setupdwarfgamest.h" @@ -29,6 +31,7 @@ DFHACK_PLUGIN("overlay"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(enabler); namespace DFHack { DBG_DECLARE(overlay, control, DebugCategory::LINFO); @@ -76,12 +79,14 @@ struct viewscreen_overlay : T { [&](lua_State *L) { Lua::Push(L, T::_identity.getName()); Lua::Push(L, this); - Lua::PushInterfaceKeys(L, *input); + Lua::PushInterfaceKeys(L, Screen::normalize_text_keys(*input)); }, [&](lua_State *L) { input_is_handled = lua_toboolean(L, -1); }); if (!input_is_handled) INTERPOSE_NEXT(feed)(input); + else + dfhack_lua_viewscreen::markInputAsHandled(); } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); @@ -108,6 +113,7 @@ IMPLEMENT_HOOKS(game_cleaner) IMPLEMENT_HOOKS(initial_prep) IMPLEMENT_HOOKS(legends) IMPLEMENT_HOOKS(loadgame) +IMPLEMENT_HOOKS(new_arena) IMPLEMENT_HOOKS(new_region) IMPLEMENT_HOOKS(savegame) IMPLEMENT_HOOKS(setupdwarfgame) @@ -142,6 +148,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { INTERPOSE_HOOKS_FAILED(initial_prep) || INTERPOSE_HOOKS_FAILED(legends) || INTERPOSE_HOOKS_FAILED(loadgame) || + INTERPOSE_HOOKS_FAILED(new_arena) || INTERPOSE_HOOKS_FAILED(new_region) || INTERPOSE_HOOKS_FAILED(savegame) || INTERPOSE_HOOKS_FAILED(setupdwarfgame) || diff --git a/plugins/pathable.cpp b/plugins/pathable.cpp index 06394f838..be6537d32 100644 --- a/plugins/pathable.cpp +++ b/plugins/pathable.cpp @@ -1,19 +1,29 @@ +#include "Debug.h" +#include "MemAccess.h" +#include "PluginManager.h" +#include "TileTypes.h" + +#include "modules/EventManager.h" #include "modules/Gui.h" +#include "modules/Job.h" #include "modules/Maps.h" #include "modules/Screen.h" #include "modules/Textures.h" -#include "Debug.h" -#include "LuaTools.h" -#include "PluginManager.h" - +#include "df/block_square_event_designation_priorityst.h" #include "df/init.h" +#include "df/job_list_link.h" +#include "df/map_block.h" +#include "df/tile_designation.h" +#include "df/world.h" + +#include using namespace DFHack; DFHACK_PLUGIN("pathable"); -REQUIRE_GLOBAL(gps); +REQUIRE_GLOBAL(init); REQUIRE_GLOBAL(window_x); REQUIRE_GLOBAL(window_y); REQUIRE_GLOBAL(window_z); @@ -23,7 +33,10 @@ namespace DFHack { DBG_DECLARE(pathable, log, DebugCategory::LINFO); } +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); return CR_OK; } @@ -31,7 +44,7 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } -static void paintScreen(df::coord target, bool skip_unrevealed = false) { +static void paintScreenPathable(df::coord target, bool show_hidden = false) { DEBUG(log).print("entering paintScreen\n"); bool use_graphics = Screen::inGraphicsMode(); @@ -39,12 +52,12 @@ static void paintScreen(df::coord target, bool skip_unrevealed = false) { int selected_tile_texpos = 0; Screen::findGraphicsTile("CURSORS", 4, 3, &selected_tile_texpos); - long pathable_tile_texpos = df::global::init->load_bar_texpos[1]; - long unpathable_tile_texpos = df::global::init->load_bar_texpos[4]; - long on_off_texpos = Textures::getOnOffTexposStart(); + long pathable_tile_texpos = init->load_bar_texpos[1]; + long unpathable_tile_texpos = init->load_bar_texpos[4]; + long on_off_texpos = Textures::getTexposByHandle(textures[0]); if (on_off_texpos > 0) { - pathable_tile_texpos = on_off_texpos + 0; - unpathable_tile_texpos = on_off_texpos + 1; + pathable_tile_texpos = on_off_texpos; + unpathable_tile_texpos = Textures::getTexposByHandle(textures[1]); } auto dims = Gui::getDwarfmodeViewDims().map(); @@ -61,7 +74,7 @@ static void paintScreen(df::coord target, bool skip_unrevealed = false) { continue; } - if (skip_unrevealed && !Maps::isTileVisible(map_pos)) { + if (!show_hidden && !Maps::isTileVisible(map_pos)) { TRACE(log).print("skipping hidden tile\n"); continue; } @@ -110,7 +123,314 @@ static void paintScreen(df::coord target, bool skip_unrevealed = false) { } } +static bool is_warm(const df::coord &pos) { + auto block = Maps::getTileBlock(pos); + if (!block) + return false; + return block->temperature_1[pos.x&15][pos.y&15] >= 10075; +} + +static bool is_rough_wall(int16_t x, int16_t y, int16_t z) { + df::tiletype *tt = Maps::getTileType(x, y, z); + if (!tt) + return false; + + return tileShape(*tt) == df::tiletype_shape::WALL && + tileSpecial(*tt) != df::tiletype_special::SMOOTH; +} + +static bool will_leak(int16_t x, int16_t y, int16_t z) { + auto des = Maps::getTileDesignation(x, y, z); + if (!des) + return false; + if (des->bits.liquid_type == df::tile_liquid::Water && des->bits.flow_size >= 1) + return true; + if (des->bits.water_table && is_rough_wall(x, y, z)) + return true; + return false; +} + +static bool is_damp(const df::coord &pos) { + return will_leak(pos.x-1, pos.y-1, pos.z) || + will_leak(pos.x, pos.y-1, pos.z) || + will_leak(pos.x+1, pos.y-1, pos.z) || + will_leak(pos.x-1, pos.y, pos.z) || + will_leak(pos.x+1, pos.y, pos.z) || + will_leak(pos.x-1, pos.y+1, pos.z) || + will_leak(pos.x, pos.y+1, pos.z) || + will_leak(pos.x+1, pos.y+1, pos.z); + will_leak(pos.x, pos.y+1, pos.z+1); +} + +static void paintScreenWarmDamp(bool show_hidden = false) { + DEBUG(log).print("entering paintScreenDampWarm\n"); + + if (Screen::inGraphicsMode()) + return; + + auto dims = Gui::getDwarfmodeViewDims().map(); + for (int y = dims.first.y; y <= dims.second.y; ++y) { + for (int x = dims.first.x; x <= dims.second.x; ++x) { + df::coord map_pos(*window_x + x, *window_y + y, *window_z); + + if (!Maps::isValidTilePos(map_pos)) + continue; + + if (!show_hidden && !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", + map_pos.x, map_pos.y, map_pos.z, x, y); + + 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); + continue; + } + + int color = is_warm(map_pos) ? COLOR_RED : is_damp(map_pos) ? COLOR_BLUE : COLOR_BLACK; + if (color == COLOR_BLACK) { + TRACE(log).print("skipping non-warm, non-damp tile\n"); + continue; + } + + // this will also change the color of the cursor or any selection box that overlaps + // the tile. this is intentional since it makes the UI more readable + if (cur_tile.fg && cur_tile.ch != ' ') { + cur_tile.fg = color; + cur_tile.bg = 0; + } else { + cur_tile.fg = 0; + cur_tile.bg = color; + } + + cur_tile.bold = false; + + if (cur_tile.tile) + cur_tile.tile_mode = Screen::Pen::CharColor; + + Screen::paintTile(cur_tile, x, y, true); + } + } +} + +struct designation{ + df::coord pos; + df::tile_designation td; + df::tile_occupancy to; + designation() = default; + designation(const df::coord &c, const df::tile_designation &td, const df::tile_occupancy &to) : pos(c), td(td), to(to) {} + + bool operator==(const designation &rhs) const { + return pos == rhs.pos; + } + + bool operator!=(const designation &rhs) const { + return !(rhs == *this); + } +}; + +namespace std { + template<> + struct hash { + std::size_t operator()(const designation &c) const { + std::hash hash_coord; + return hash_coord(c.pos); + } + }; +} + +class Designations { +private: + std::unordered_map designations; +public: + Designations() { + df::job_list_link *link = world->jobs.list.next; + for (; link; link = link->next) { + df::job *job = link->item; + + if(!job || !Maps::isValidTilePos(job->pos)) + continue; + + df::tile_designation td; + df::tile_occupancy to; + bool keep_if_taken = false; + + switch (job->job_type) { + case df::job_type::SmoothWall: + case df::job_type::SmoothFloor: + keep_if_taken = true; + // fallthrough + case df::job_type::CarveFortification: + td.bits.smooth = 1; + break; + case df::job_type::DetailWall: + case df::job_type::DetailFloor: + td.bits.smooth = 2; + break; + case job_type::CarveTrack: + to.bits.carve_track_north = (job->item_category.whole >> 18) & 1; + to.bits.carve_track_south = (job->item_category.whole >> 19) & 1; + to.bits.carve_track_west = (job->item_category.whole >> 20) & 1; + to.bits.carve_track_east = (job->item_category.whole >> 21) & 1; + break; + default: + continue; + } + if (keep_if_taken || !Job::getWorker(job)) + designations.emplace(job->pos, designation(job->pos, td, to)); + } + } + + // get from job; if no job, then fall back to querying map + designation get(const df::coord &pos) const { + if (designations.count(pos)) { + return designations.at(pos); + } + auto pdes = Maps::getTileDesignation(pos); + auto pocc = Maps::getTileOccupancy(pos); + if (!pdes || !pocc) + return {}; + return designation(pos, *pdes, *pocc); + } +}; + +static bool is_designated_for_smoothing(const designation &designation) { + return designation.td.bits.smooth == 1; +} + +static bool is_designated_for_engraving(const designation &designation) { + return designation.td.bits.smooth == 2; +} + +static bool is_designated_for_track_carving(const designation &designation) { + const df::tile_occupancy &occ = designation.to; + return occ.bits.carve_track_east || occ.bits.carve_track_north || occ.bits.carve_track_south || occ.bits.carve_track_west; +} + +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) + return (char)0xCE; // NSEW + if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south) + return (char)0xCC; // NSE + if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_west) + return (char)0xCA; // NEW + if (occ.bits.carve_track_east && occ.bits.carve_track_south && occ.bits.carve_track_west) + return (char)0xCB; // SEW + if (occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) + return (char)0xB9; // NSW + if (occ.bits.carve_track_north && occ.bits.carve_track_south) + return (char)0xBA; // NS + if (occ.bits.carve_track_east && occ.bits.carve_track_west) + return (char)0xCD; // EW + if (occ.bits.carve_track_east && occ.bits.carve_track_north) + return (char)0xC8; // NE + if (occ.bits.carve_track_north && occ.bits.carve_track_west) + return (char)0xBC; // NW + if (occ.bits.carve_track_east && occ.bits.carve_track_south) + return (char)0xC9; // SE + if (occ.bits.carve_track_south && occ.bits.carve_track_west) + return (char)0xBB; // SW + if (occ.bits.carve_track_north) + return (char)0xD0; // N + if (occ.bits.carve_track_south) + return (char)0xD2; // S + if (occ.bits.carve_track_east) + return (char)0xC6; // E + if (occ.bits.carve_track_west) + return (char)0xB5; // W + return (char)0xC5; // single line cross; should never happen +} + +static bool is_smooth_wall(const df::coord &pos) { + df::tiletype *tt = Maps::getTileType(pos); + return tt && tileSpecial(*tt) == df::tiletype_special::SMOOTH + && tileShape(*tt) == df::tiletype_shape::WALL; +} + +static bool blink(int delay) { + return (Core::getInstance().p->getTickCount()/delay) % 2 == 0; +} + +static char get_tile_char(const df::coord &pos, char desig_char, bool draw_priority) { + if (!draw_priority) + return desig_char; + + std::vector priorities; + Maps::SortBlockEvents(Maps::getTileBlock(pos), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &priorities); + if (priorities.empty()) + return desig_char; + switch (priorities[0]->priority[pos.x % 16][pos.y % 16] / 1000) { + case 1: return '1'; + case 2: return '2'; + case 3: return '3'; + case 4: return '4'; + case 5: return '5'; + case 6: return '6'; + case 7: return '7'; + default: + return '4'; + } +} + +static void paintScreenCarve() { + DEBUG(log).print("entering paintScreenCarve\n"); + + if (Screen::inGraphicsMode() || blink(500)) + return; + + Designations designations; + bool draw_priority = blink(1000); + + auto dims = Gui::getDwarfmodeViewDims().map(); + for (int y = dims.first.y; y <= dims.second.y; ++y) { + for (int x = dims.first.x; x <= dims.second.x; ++x) { + df::coord map_pos(*window_x + x, *window_y + y, *window_z); + + 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", + map_pos.x, map_pos.y, map_pos.z, x, y); + + Screen::Pen cur_tile; + cur_tile.fg = COLOR_DARKGREY; + + auto des = designations.get(map_pos); + + if (is_designated_for_smoothing(des)) { + if (is_smooth_wall(map_pos)) + cur_tile.ch = get_tile_char(map_pos, (char)206, draw_priority); // hash, indicating a fortification designation + else + cur_tile.ch = get_tile_char(map_pos, (char)219, draw_priority); // solid block, indicating a smoothing designation + } + else if (is_designated_for_engraving(des)) { + cur_tile.ch = get_tile_char(map_pos, (char)10, draw_priority); // solid block with a circle on it + } + 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 { + TRACE(log).print("skipping tile with no carving designation\n"); + continue; + } + + Screen::paintTile(cur_tile, x, y, true); + } + } +} + DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(paintScreen), + DFHACK_LUA_FUNCTION(paintScreenPathable), + DFHACK_LUA_FUNCTION(paintScreenWarmDamp), + DFHACK_LUA_FUNCTION(paintScreenCarve), DFHACK_LUA_END }; diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp new file mode 100644 index 000000000..be560e1ce --- /dev/null +++ b/plugins/preserve-tombs.cpp @@ -0,0 +1,287 @@ +#include "Debug.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include +#include +#include +#include +#include +#include + +#include "modules/Units.h" +#include "modules/Buildings.h" +#include "modules/Persistence.h" +#include "modules/EventManager.h" +#include "modules/World.h" +#include "modules/Translation.h" + +#include "df/world.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/building_civzonest.h" + +using namespace DFHack; +using namespace df::enums; + + +// +DFHACK_PLUGIN("preserve-tombs"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +static int32_t cycle_timestamp; +static constexpr int32_t cycle_freq = 100; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, +}; + +static std::unordered_map tomb_assignments; + +namespace DFHack { + DBG_DECLARE(preservetombs, config, DebugCategory::LINFO); + DBG_DECLARE(preservetombs, cycle, DebugCategory::LINFO); + DBG_DECLARE(preservetombs, event, DebugCategory::LINFO); +} + + +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} + +static bool assign_to_tomb(int32_t unit_id, int32_t building_id); +static void update_tomb_assignments(color_ostream& out); +void onUnitDeath(color_ostream& out, void* ptr); +static command_result do_command(color_ostream& out, std::vector& params); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand( + plugin_name, + "Preserve tomb assignments when assigned units die.", + do_command)); + return CR_OK; +} + +static command_result do_command(color_ostream& out, std::vector& params) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot use %s without a loaded world.\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"); + 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 ? Translation::TranslateName(&unit->name) : "UNKNOWN UNIT" ; + out.print("%s (id %d) -> building %d\n", name.c_str(), unit_id, building_id); + }); + } + return CR_OK; + } + if (params[0] == "now") { + if (!is_enabled) { + out.printerr("Cannot update %s when not enabled", plugin_name); + return CR_FAILURE; + } + CoreSuspender suspend; + update_tomb_assignments(out); + out.print("Updated tomb assignments\n"); + return CR_OK; + } + return CR_WRONG_USAGE; +} + +// event listener +EventManager::EventHandler assign_tomb_handler(onUnitDeath, 0); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + if (enable) { + EventManager::registerListener(EventManager::EventType::UNIT_DEATH, assign_tomb_handler, plugin_self); + update_tomb_assignments(out); + } + else { + tomb_assignments.clear(); + EventManager::unregisterAll(plugin_self); + } + } else { + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); + +// PluginManager handles unregistering our handler from EventManager, +// so we don't have to do that here + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + } + + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + tomb_assignments.clear(); + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } + EventManager::unregisterAll(plugin_self); + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= cycle_freq) + update_tomb_assignments(out); + return CR_OK; +} +// + + + + +// On unit death - check if we assigned them a tomb +// +// +void onUnitDeath(color_ostream& out, void* ptr) { + // input is void* that contains the unit id + int32_t unit_id = reinterpret_cast(ptr); + + // check if unit was assigned a tomb in life + auto it = tomb_assignments.find(unit_id); + if (it == tomb_assignments.end()) return; + + // assign that unit to their previously assigned tomb in life + int32_t building_id = it->second; + if (!assign_to_tomb(unit_id, building_id)) { + WARN(event, out).print("Unit %d died - but failed to assign them back to their tomb %d\n", unit_id, building_id); + return; + } + // success, print status update and remove assignment from our memo-list + INFO(event, out).print("Unit %d died - assigning them back to their tomb\n", unit_id); + tomb_assignments.erase(it); + +} + + +// Update tomb assignments +// +// +static void update_tomb_assignments(color_ostream &out) { + cycle_timestamp = world->frame_counter; + // check tomb civzones for assigned units + for (auto* bld : world->buildings.other.ZONE_TOMB) { + + auto* tomb = virtual_cast(bld); + 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 + + 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", + 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", + plugin_name, tomb->assigned_unit_id, it->second, tomb->id); + it->second = tomb->id; + } + + } + + // now check our civzones for unassignment / deleted zone + std::erase_if(tomb_assignments,[&](const auto& p){ + auto &[unit_id, building_id] = p; + + const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) { + DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); + return true; + } + const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || !tomb->flags.bits.exists) { + DEBUG(cycle, out).print("%s tomb missing: %d - 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); + return true; + } + + return false; + }); + +} + + +// ASSIGN UNIT TO TOMB +// +// +static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { + + df::unit* unit = df::unit::find(unit_id); + + if (!unit || !Units::isDead(unit)) return false; + + const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) return false; + + df::building_civzonest* tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || tomb->assigned_unit) return false; + + Buildings::setOwner(tomb, unit); + return true; +} diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 4f74661ba..3091f0703 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -567,26 +567,11 @@ static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen, const prospect_options &options) { - out.printerr("prospector at embark is not currently available.\n"); - return CR_FAILURE; - -/* - if (!world || !world->world_data) - { + if (!world->world_data) { out.printerr("World data is not available.\n"); return CR_FAILURE; } - df::world_data *data = world->world_data; - coord2d cur_region = screen->location.region_pos; - auto cur_details = get_details(data, cur_region); - - if (!cur_details) - { - out.printerr("Current region details are not available.\n"); - return CR_FAILURE; - } - // Compute material maps MatMap layerMats; MatMap veinMats; @@ -595,12 +580,18 @@ static command_result embark_prospector(color_ostream &out, // Compute biomes std::map biomes; - for (int x = screen->location.embark_pos_min.x; x <= 15 && x <= screen->location.embark_pos_max.x; x++) - { - for (int y = screen->location.embark_pos_min.y; y <= 15 && y <= screen->location.embark_pos_max.y; y++) - { + int32_t max_x = (world->worldgen.worldgen_parms.dim_x * 16) - 1; + int32_t max_y = (world->worldgen.worldgen_parms.dim_y * 16) - 1; + + for (int x = screen->location.embark_pos_min.x; x <= max_x && x <= screen->location.embark_pos_max.x; ++x) { + for (int y = screen->location.embark_pos_min.y; y <= max_y && y <= screen->location.embark_pos_max.y; ++y) { + auto cur_details = get_details(world->world_data, coord2d(x / 16, y / 16)); + + if (!cur_details) + continue; + EmbarkTileLayout tile; - if (!estimate_underground(out, tile, cur_details, x, y) || + if (!estimate_underground(out, tile, cur_details, x % 16, y % 16) || !estimate_materials(out, tile, layerMats, veinMats)) return CR_FAILURE; @@ -627,7 +618,6 @@ static command_result embark_prospector(color_ostream &out, out << std::endl << "Warning: the above data is only a very rough estimate." << std::endl; return CR_OK; -*/ } static command_result map_prospector(color_ostream &con, diff --git a/plugins/proto/isoworldremote.proto b/plugins/proto/isoworldremote.proto index f53aa6aea..ebe5ef545 100644 --- a/plugins/proto/isoworldremote.proto +++ b/plugins/proto/isoworldremote.proto @@ -5,7 +5,7 @@ package isoworldremote; option optimize_for = LITE_RUNTIME; -// Plugin: isoworldremote +// DISABLED Plugin: isoworldremote enum BasicMaterial { AIR = 0; @@ -54,7 +54,7 @@ message EmbarkTile { optional bool is_valid = 7; } -// RPC GetEmbarkTile : TileRequest -> EmbarkTile +// DISABLED RPC GetEmbarkTile : TileRequest -> EmbarkTile message TileRequest { optional int32 want_x = 1; optional int32 want_y = 2; @@ -64,7 +64,7 @@ message MapRequest { optional string save_folder = 1; } -// RPC GetEmbarkInfo : MapRequest -> MapReply +// DISABLED RPC GetEmbarkInfo : MapRequest -> MapReply message MapReply { required bool available = 1; optional int32 region_x = 2; @@ -75,7 +75,7 @@ message MapReply { optional int32 current_season = 7; } -// RPC GetRawNames : MapRequest -> RawNames +// DISABLED RPC GetRawNames : MapRequest -> RawNames message RawNames { required bool available = 1; repeated string inorganic = 2; diff --git a/plugins/proto/rename.proto b/plugins/proto/rename.proto index 4391ecc10..ef3f57423 100644 --- a/plugins/proto/rename.proto +++ b/plugins/proto/rename.proto @@ -4,9 +4,9 @@ package dfproto; option optimize_for = LITE_RUNTIME; -// Plugin: rename +// DISABLED Plugin: rename -// RPC RenameSquad : RenameSquadIn -> EmptyMessage +// DISABLED RPC RenameSquad : RenameSquadIn -> EmptyMessage message RenameSquadIn { required int32 squad_id = 1; @@ -14,7 +14,7 @@ message RenameSquadIn { optional string alias = 3; } -// RPC RenameUnit : RenameUnitIn -> EmptyMessage +// DISABLED RPC RenameUnit : RenameUnitIn -> EmptyMessage message RenameUnitIn { required int32 unit_id = 1; @@ -22,7 +22,7 @@ message RenameUnitIn { optional string profession = 3; } -// RPC RenameBuilding : RenameBuildingIn -> EmptyMessage +// DISABLED RPC RenameBuilding : RenameBuildingIn -> EmptyMessage message RenameBuildingIn { required int32 building_id = 1; diff --git a/plugins/remotefortressreader/CMakeLists.txt b/plugins/remotefortressreader/CMakeLists.txt index 262d163f1..37aa64b23 100644 --- a/plugins/remotefortressreader/CMakeLists.txt +++ b/plugins/remotefortressreader/CMakeLists.txt @@ -25,7 +25,7 @@ set(PROJECT_PROTO ) if(UNIX AND NOT APPLE) - set(PROJECT_LIBS ${PROJECT_LIBS} SDL) + set(PROJECT_LIBS ${PROJECT_LIBS}) endif() # this makes sure all the stuff is put in proper places and linked to dfhack diff --git a/plugins/remotefortressreader/proto/RemoteFortressReader.proto b/plugins/remotefortressreader/proto/RemoteFortressReader.proto index a64e64d0b..f341031dc 100644 --- a/plugins/remotefortressreader/proto/RemoteFortressReader.proto +++ b/plugins/remotefortressreader/proto/RemoteFortressReader.proto @@ -526,6 +526,7 @@ message BlockRequest optional int32 max_y = 5; optional int32 min_z = 6; optional int32 max_z = 7; + optional bool force_reload = 8; } message BlockList diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index 56d29371b..1570a0e60 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -14,8 +14,6 @@ #include "PluginManager.h" #include "RemoteFortressReader.pb.h" #include "RemoteServer.h" -#include "SDL_events.h" -#include "SDL_keyboard.h" #include "TileTypes.h" #include "VersionInfo.h" #if DF_VERSION_INT > 34011 @@ -126,10 +124,12 @@ #include "dwarf_control.h" #include "item_reader.h" +#include +#include + using namespace DFHack; using namespace df::enums; using namespace RemoteFortressReader; -using namespace std; DFHACK_PLUGIN("RemoteFortressReader"); @@ -192,7 +192,7 @@ 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, vector & parameters) +command_result loadArtImageChunk(color_ostream &out, std::vector & parameters) { if (parameters.size() != 1) return CR_WRONG_USAGE; @@ -213,7 +213,7 @@ command_result loadArtImageChunk(color_ostream &out, vector & parameter return CR_OK; } -command_result RemoteFortressReader_version(color_ostream &out, vector ¶meters) +command_result RemoteFortressReader_version(color_ostream &out, std::vector ¶meters) { out.print(RFR_VERSION); return CR_OK; @@ -644,7 +644,7 @@ void CopyMat(RemoteFortressReader::MatPair * mat, int type, int index) } -map hashes; +std::map hashes; bool IsTiletypeChanged(DFCoord pos) { @@ -662,7 +662,7 @@ bool IsTiletypeChanged(DFCoord pos) return false; } -map waterHashes; +std::map waterHashes; bool IsDesignationChanged(DFCoord pos) { @@ -680,7 +680,7 @@ bool IsDesignationChanged(DFCoord pos) return false; } -map buildingHashes; +std::map buildingHashes; bool IsBuildingChanged(DFCoord pos) { @@ -699,7 +699,7 @@ bool IsBuildingChanged(DFCoord pos) return changed; } -map spatterHashes; +std::map spatterHashes; bool IsspatterChanged(DFCoord pos) { @@ -736,7 +736,7 @@ bool IsspatterChanged(DFCoord pos) return false; } -map itemHashes; +std::map itemHashes; bool isItemChanged(int i) { @@ -754,7 +754,7 @@ bool isItemChanged(int i) return false; } -bool areItemsChanged(vector * items) +bool areItemsChanged(std::vector * items) { bool result = false; for (size_t i = 0; i < items->size(); i++) @@ -765,7 +765,7 @@ bool areItemsChanged(vector * items) return result; } -map engravingHashes; +std::map engravingHashes; bool isEngravingNew(int index) { @@ -1392,6 +1392,7 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in int max_y = in->max_y(); int min_z = in->min_z(); int max_z = in->max_z(); + bool forceReload = in->force_reload(); bool firstBlock = true; //Always send all the buildings needed on the first block, and none on the rest. //stream.print("Got request for blocks from (%d, %d, %d) to (%d, %d, %d).\n", in->min_x(), in->min_y(), in->min_z(), in->max_x(), in->max_y(), in->max_z()); for (int zz = max_z - 1; zz >= min_z; zz--) @@ -1439,19 +1440,19 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in bool itemsChanged = block->items.size() > 0; bool flows = block->flows.size() > 0; RemoteFortressReader::MapBlock *net_block = nullptr; - if (tileChanged || desChanged || spatterChanged || firstBlock || itemsChanged || flows) + if (tileChanged || desChanged || spatterChanged || firstBlock || itemsChanged || flows || forceReload) { net_block = out->add_map_blocks(); net_block->set_map_x(block->map_pos.x); net_block->set_map_y(block->map_pos.y); net_block->set_map_z(block->map_pos.z); } - if (tileChanged) + if (tileChanged || forceReload) { CopyBlock(block, net_block, &MC, pos); blocks_sent++; } - if (desChanged) + if (desChanged || forceReload) CopyDesignation(block, net_block, &MC, pos); if (firstBlock) { @@ -1459,7 +1460,7 @@ static command_result GetBlockList(color_ostream &stream, const BlockRequest *in CopyProjectiles(net_block); firstBlock = false; } - if (spatterChanged) + if (spatterChanged || forceReload) Copyspatters(block, net_block, &MC, pos); if (itemsChanged) CopyItems(block, net_block, &MC, pos); @@ -2894,13 +2895,12 @@ static command_result CopyScreen(color_ostream &stream, const EmptyMessage *in, static command_result PassKeyboardEvent(color_ostream &stream, const KeyboardEvent *in) { #if DF_VERSION_INT > 34011 - SDL::Event e; + SDL_Event e; e.key.type = in->type(); e.key.state = in->state(); - e.key.ksym.mod = (SDL::Mod)in->mod(); - e.key.ksym.scancode = in->scancode(); - e.key.ksym.sym = (SDL::Key)in->sym(); - e.key.ksym.unicode = in->unicode(); + e.key.keysym.mod = in->mod(); + e.key.keysym.scancode = (SDL_Scancode)in->scancode(); + e.key.keysym.sym = in->sym(); DFHack::DFSDL::DFSDL_PushEvent(&e); #endif return CR_OK; diff --git a/plugins/search.cpp b/plugins/search.cpp deleted file mode 100644 index b69480778..000000000 --- a/plugins/search.cpp +++ /dev/null @@ -1,2568 +0,0 @@ -#include "MiscUtils.h" -#include "VTableInterpose.h" -#include "uicommon.h" - -#include "modules/Buildings.h" -#include "modules/Gui.h" -#include "modules/Job.h" -#include "modules/Screen.h" -#include "modules/Translation.h" -#include "modules/Units.h" - -#include "df/creature_raw.h" -#include "df/global_objects.h" -#include "df/historical_figure.h" -#include "df/interface_key.h" -#include "df/interfacest.h" -#include "df/job.h" -#include "df/layer_object_listst.h" -#include "df/misc_trait_type.h" -#include "df/report.h" -#include "df/ui_look_list.h" -#include "df/unit.h" -#include "df/unit_misc_trait.h" -#include "df/viewscreen_announcelistst.h" -#include "df/viewscreen_buildinglistst.h" -#include "df/viewscreen_dwarfmodest.h" -#include "df/viewscreen_joblistst.h" -#include "df/viewscreen_justicest.h" -#include "df/viewscreen_kitchenprefst.h" -#include "df/viewscreen_layer_militaryst.h" -#include "df/viewscreen_layer_noblelistst.h" -#include "df/viewscreen_layer_stockpilest.h" -#include "df/viewscreen_layer_stone_restrictionst.h" -#include "df/viewscreen_locationsst.h" -#include "df/viewscreen_petst.h" -#include "df/viewscreen_storesst.h" -#include "df/viewscreen_topicmeeting_fill_land_holder_positionsst.h" -#include "df/viewscreen_tradegoodsst.h" -#include "df/viewscreen_unitlistst.h" -#include "df/viewscreen_workshop_profilest.h" - -using namespace std; -using std::set; -using std::vector; -using std::string; - -using namespace DFHack; -using namespace df::enums; - -DFHACK_PLUGIN("search"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(gps); -REQUIRE_GLOBAL(gview); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_building_assign_units); -REQUIRE_GLOBAL(ui_building_in_assign); -REQUIRE_GLOBAL(ui_building_item_cursor); -REQUIRE_GLOBAL(ui_look_cursor); -REQUIRE_GLOBAL(ui_look_list); -REQUIRE_GLOBAL(world); - -/* -Search Plugin - -A plugin that adds a "Search" hotkey to some screens (Units, Trade and Stocks) -that allows filtering of the list items by a typed query. - -Works by manipulating the vector(s) that the list based viewscreens use to store -their items. When a search is started the plugin saves the original vectors and -with each keystroke creates a new filtered vector off the saves for the screen -to use. -*/ - - -void make_text_dim(int x1, int x2, int y) -{ - for (int x = x1; x <= x2; x++) - { - Screen::Pen pen = Screen::readTile(x,y); - - if (pen.valid()) - { - if (pen.fg != 0) - { - if (pen.fg == 7) - pen.adjust(0,true); - else - pen.bold = 0; - } - - Screen::paintTile(pen,x,y); - } - } -} - -static bool is_live_screen(const df::viewscreen *screen) -{ - for (df::viewscreen *cur = &gview->view; cur; cur = cur->child) - if (cur == screen && cur->breakdown_level == interface_breakdown_types::NONE) - return true; - return false; -} - -static string get_unit_description(df::unit *unit) -{ - if (!unit) - return ""; - string desc; - auto name = Units::getVisibleName(unit); - if (name->has_name) - desc = Translation::TranslateName(name, false); - desc += ", " + Units::getProfessionName(unit); // Check animal type too - - return desc; -} - -static bool cursor_key_pressed (std::set *input, bool in_entry_mode) -{ - if (in_entry_mode) - { - // give text input (e.g. "2") priority over cursor keys - for (auto it = input->begin(); it != input->end(); ++it) - { - if (Screen::keyToChar(*it) != -1) - return false; - } - } - return - input->count(df::interface_key::CURSOR_UP) || - input->count(df::interface_key::CURSOR_DOWN) || - input->count(df::interface_key::CURSOR_LEFT) || - input->count(df::interface_key::CURSOR_RIGHT) || - input->count(df::interface_key::CURSOR_UPLEFT) || - input->count(df::interface_key::CURSOR_UPRIGHT) || - input->count(df::interface_key::CURSOR_DOWNLEFT) || - input->count(df::interface_key::CURSOR_DOWNRIGHT) || - input->count(df::interface_key::CURSOR_UP_FAST) || - input->count(df::interface_key::CURSOR_DOWN_FAST) || - input->count(df::interface_key::CURSOR_LEFT_FAST) || - input->count(df::interface_key::CURSOR_RIGHT_FAST) || - input->count(df::interface_key::CURSOR_UPLEFT_FAST) || - input->count(df::interface_key::CURSOR_UPRIGHT_FAST) || - input->count(df::interface_key::CURSOR_DOWNLEFT_FAST) || - input->count(df::interface_key::CURSOR_DOWNRIGHT_FAST) || - input->count(df::interface_key::CURSOR_UP_Z) || - input->count(df::interface_key::CURSOR_DOWN_Z) || - input->count(df::interface_key::CURSOR_UP_Z_AUX) || - input->count(df::interface_key::CURSOR_DOWN_Z_AUX); -} - -// -// START: Generic Search functionality -// - -template -class search_generic -{ -public: - bool init(S *screen) - { - if (screen != viewscreen && !reset_on_change()) - return false; - - if (!can_init(screen)) - { - if (is_valid()) - { - clear_search(); - reset_all(); - } - - return false; - } - - if (!is_valid()) - { - this->viewscreen = screen; - this->cursor_pos = get_viewscreen_cursor(); - this->primary_list = get_primary_list(); - this->select_key = get_search_select_key(); - select_token = Screen::charToKey(select_key); - shift_select_token = Screen::charToKey(select_key + 'A' - 'a'); - valid = true; - do_post_init(); - } - - return true; - } - - // Called each time you enter or leave a searchable screen. Resets everything. - virtual void reset_all() - { - reset_search(); - valid = false; - primary_list = NULL; - viewscreen = NULL; - select_key = 's'; - } - - bool reset_on_change() - { - if (valid && is_live_screen(viewscreen)) - return false; - - reset_all(); - return true; - } - - bool is_valid() - { - return valid; - } - - // A new keystroke is received in a searchable screen - virtual bool process_input(set *input) - { - // If the page has two search options (Trade screen), only allow one to operate - // at a time - if (lock != NULL && lock != this) - return false; - - // Allows custom preprocessing for each screen - if (!should_check_input(input)) - return false; - - bool key_processed = true; - - if (entry_mode) - { - // Query typing mode - - df::interface_key last_token = get_string_key(input); - int charcode = Screen::keyToChar(last_token); - if (charcode >= 32 && charcode <= 126) - { - // Standard character - search_string += char(charcode); - do_search(); - } - else if (last_token == interface_key::STRING_A000) - { - // Backspace - if (search_string.length() > 0) - { - search_string.erase(search_string.length()-1); - do_search(); - } - } - else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) - { - // ENTER or ESC: leave typing mode - end_entry_mode(); - } - else if (cursor_key_pressed(input, entry_mode)) - { - // Arrow key pressed. Leave entry mode and allow screen to process key - end_entry_mode(); - key_processed = false; - } - } - // Not in query typing mode - else if (input->count(select_token)) - { - // Hotkey pressed, enter typing mode - start_entry_mode(); - } - else if (input->count(shift_select_token)) - { - // Shift + Hotkey pressed, clear query - clear_search(); - } - else - { - // Not a key for us, pass it on to the screen - key_processed = false; - } - - return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode - } - - // Called after a keystroke has been processed - virtual void do_post_input_feed() - { - } - - static search_generic *lock; - - bool in_entry_mode() - { - return entry_mode; - } - -protected: - virtual string get_element_description(T element) const = 0; - virtual void render() const = 0; - virtual int32_t *get_viewscreen_cursor() = 0; - virtual vector *get_primary_list() = 0; - - search_generic() - { - reset_all(); - } - - virtual bool can_init(S *screen) - { - return true; - } - - virtual void do_post_init() - { - - } - - void start_entry_mode() - { - entry_mode = true; - lock = this; - } - - void end_entry_mode() - { - entry_mode = false; - lock = NULL; - } - - virtual char get_search_select_key() - { - return 's'; - } - - virtual void reset_search() - { - end_entry_mode(); - search_string = ""; - saved_list1.clear(); - } - - // Shortcut to clear the search immediately - virtual void clear_search() - { - if (saved_list1.size() > 0) - { - *primary_list = saved_list1; - saved_list1.clear(); - } - search_string = ""; - } - - virtual void save_original_values() - { - saved_list1 = *primary_list; - } - - virtual void do_pre_incremental_search() - { - - } - - virtual void clear_viewscreen_vectors() - { - primary_list->clear(); - } - - virtual void add_to_filtered_list(size_t i) - { - primary_list->push_back(saved_list1[i]); - } - - virtual void do_post_search() - { - - } - - virtual bool is_valid_for_search(size_t index) - { - return true; - } - - virtual bool force_in_search(size_t index) - { - return false; - } - - // The actual sort - virtual void do_search() - { - if (search_string.length() == 0) - { - clear_search(); - return; - } - - if (saved_list1.size() == 0) - // On first run, save the original list - save_original_values(); - else - do_pre_incremental_search(); - - clear_viewscreen_vectors(); - - string search_string_l = to_search_normalized(search_string); - for (size_t i = 0; i < saved_list1.size(); i++ ) - { - if (force_in_search(i)) - { - add_to_filtered_list(i); - continue; - } - - if (!is_valid_for_search(i)) - continue; - - T element = saved_list1[i]; - string desc = to_search_normalized(get_element_description(element)); - if (desc.find(search_string_l) != string::npos) - { - add_to_filtered_list(i); - } - } - - do_post_search(); - - if (cursor_pos) - *cursor_pos = 0; - } - - virtual bool should_check_input(set *input) - { - return true; - } - - // Display hotkey message - void print_search_option(int x, int y = -1) const - { - auto dim = Screen::getWindowSize(); - if (y == -1) - y = dim.y - 2; - - OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key)); - OutputString((entry_mode) ? 10 : 15, x, y, ": Search"); - if (search_string.length() > 0 || entry_mode) - OutputString(15, x, y, ": " + search_string); - if (entry_mode) - OutputString(10, x, y, "_"); - } - - S *viewscreen; - vector saved_list1, reference_list, *primary_list; - - //bool redo_search; - string search_string; - -protected: - int *cursor_pos; - char select_key; - bool valid; - bool entry_mode; - - df::interface_key select_token; - df::interface_key shift_select_token; -}; - -template search_generic *search_generic ::lock = NULL; - - -// Search class helper for layered screens -template -class layered_search : public search_generic -{ -protected: - virtual bool can_init(S *screen) - { - auto list = getLayerList(screen); - if (!is_list_valid(screen) || !list || !list->active) - return false; - - return true; - } - - virtual bool is_list_valid(S*) - { - return true; - } - - virtual void do_search() - { - search_generic::do_search(); - auto list = getLayerList(this->viewscreen); - list->num_entries = this->get_primary_list()->size(); - } - - int32_t *get_viewscreen_cursor() - { - auto list = getLayerList(this->viewscreen); - return &list->cursor; - } - - virtual void clear_search() - { - search_generic::clear_search(); - - if (is_list_valid(this->viewscreen)) - { - auto list = getLayerList(this->viewscreen); - list->num_entries = this->get_primary_list()->size(); - } - } - -private: - static df::layer_object_listst *getLayerList(const df::viewscreen_layer *layer) - { - return virtual_cast(vector_get(layer->layer_objects, LIST_ID)); - } -}; - - - -// Parent class for screens that have more than one primary list to synchronise -template < class S, class T, class PARENT = search_generic > -class search_multicolumn_modifiable_generic : public PARENT -{ -protected: - vector reference_list; - vector saved_indexes; - bool read_only; - - virtual void update_saved_secondary_list_item(size_t i, size_t j) = 0; - virtual void save_secondary_values() = 0; - virtual void clear_secondary_viewscreen_vectors() = 0; - virtual void add_to_filtered_secondary_lists(size_t i) = 0; - virtual void clear_secondary_saved_lists() = 0; - virtual void reset_secondary_viewscreen_vectors() = 0; - virtual void restore_secondary_values() = 0; - - virtual void do_post_init() - { - // If true, secondary list isn't modifiable so don't bother synchronising values - read_only = false; - } - - void reset_all() - { - PARENT::reset_all(); - reference_list.clear(); - saved_indexes.clear(); - reset_secondary_viewscreen_vectors(); - } - - void reset_search() - { - PARENT::reset_search(); - reference_list.clear(); - saved_indexes.clear(); - clear_secondary_saved_lists(); - } - - virtual void clear_search() - { - if (this->saved_list1.size() > 0) - { - do_pre_incremental_search(); - restore_secondary_values(); - } - clear_secondary_saved_lists(); - PARENT::clear_search(); - do_post_search(); - } - - virtual bool is_match(T &a, T &b) = 0; - - virtual bool is_match(vector &a, vector &b) = 0; - - void do_pre_incremental_search() - { - PARENT::do_pre_incremental_search(); - if (read_only) - return; - - bool list_has_been_sorted = (this->primary_list->size() == reference_list.size() - && !is_match(*this->primary_list, reference_list)); - - for (size_t i = 0; i < saved_indexes.size(); i++) - { - int adjusted_item_index = i; - if (list_has_been_sorted) - { - for (size_t j = 0; j < this->primary_list->size(); j++) - { - if (is_match((*this->primary_list)[j], reference_list[i])) - { - adjusted_item_index = j; - break; - } - } - } - - update_saved_secondary_list_item(saved_indexes[i], adjusted_item_index); - } - saved_indexes.clear(); - } - - void clear_viewscreen_vectors() - { - search_generic::clear_viewscreen_vectors(); - saved_indexes.clear(); - clear_secondary_viewscreen_vectors(); - } - - void add_to_filtered_list(size_t i) - { - search_generic::add_to_filtered_list(i); - add_to_filtered_secondary_lists(i); - if (!read_only) - saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed - } - - virtual void do_post_search() - { - if (!read_only) - reference_list = *this->primary_list; - } - - void save_original_values() - { - search_generic::save_original_values(); - save_secondary_values(); - } -}; - -// This basic match function is separated out from the generic multi column class, because the -// pets screen, which uses a union in its primary list, will cause a compile failure if this -// match function exists in the generic class -template < class S, class T, class PARENT = search_generic > -class search_multicolumn_modifiable : public search_multicolumn_modifiable_generic -{ - bool is_match(T &a, T &b) - { - return a == b; - } - - bool is_match(vector &a, vector &b) - { - return a == b; - } -}; - -// General class for screens that have only one secondary list to keep in sync -template < class S, class T, class V, class PARENT = search_generic > -class search_twocolumn_modifiable : public search_multicolumn_modifiable -{ -public: -protected: - virtual vector * get_secondary_list() = 0; - - virtual void do_post_init() - { - search_multicolumn_modifiable::do_post_init(); - secondary_list = get_secondary_list(); - } - - void save_secondary_values() - { - saved_secondary_list = *secondary_list; - } - - void reset_secondary_viewscreen_vectors() - { - secondary_list = NULL; - } - - virtual void update_saved_secondary_list_item(size_t i, size_t j) - { - saved_secondary_list[i] = (*secondary_list)[j]; - } - - void clear_secondary_viewscreen_vectors() - { - secondary_list->clear(); - } - - void add_to_filtered_secondary_lists(size_t i) - { - secondary_list->push_back(saved_secondary_list[i]); - } - - void clear_secondary_saved_lists() - { - saved_secondary_list.clear(); - } - - void restore_secondary_values() - { - *secondary_list = saved_secondary_list; - } - - vector *secondary_list, saved_secondary_list; -}; - - -// Parent struct for the hooks, use optional param D to generate multiple search classes in the same -// viewscreen but different static modules -template -struct generic_search_hook : T -{ - typedef T interpose_base; - - static V module; - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!module.init(this)) - { - INTERPOSE_NEXT(feed)(input); - return; - } - - if (!module.process_input(input)) - { - INTERPOSE_NEXT(feed)(input); - module.do_post_input_feed(); - } - - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - bool ok = module.init(this); - INTERPOSE_NEXT(render)(); - if (ok) - module.render(); - } - - DEFINE_VMETHOD_INTERPOSE(bool, key_conflict, (df::interface_key key)) - { - if (module.in_entry_mode() && (key == interface_key::MOVIES || key == interface_key::HELP)) - return true; - return INTERPOSE_NEXT(key_conflict)(key); - } -}; - -template V generic_search_hook ::module; - - -// Hook definition helpers -#define IMPLEMENT_HOOKS_WITH_ID(screen, module, id, prio) \ - typedef generic_search_hook module##_hook; \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, prio); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, prio); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, key_conflict, prio) - -#define IMPLEMENT_HOOKS(screen, module) \ - typedef generic_search_hook module##_hook; \ - template<> IMPLEMENT_VMETHOD_INTERPOSE(module##_hook, feed); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE(module##_hook, render); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE(module##_hook, key_conflict) - -#define IMPLEMENT_HOOKS_PRIO(screen, module, prio) \ - typedef generic_search_hook module##_hook; \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, prio); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, prio); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, key_conflict, prio); - -// -// END: Generic Search functionality -// - - -// -// START: Animal screen search -// - -typedef search_multicolumn_modifiable_generic pets_search_base; -class pets_search : public pets_search_base -{ - typedef df::viewscreen_petst::T_animal T_animal; - typedef df::viewscreen_petst::T_mode T_mode; -public: - void render() const - { - print_search_option(25, 4); - } - -private: - bool can_init(df::viewscreen_petst *screen) - { - return pets_search_base::can_init(screen) && screen->mode == T_mode::List; - } - - int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor; - } - - vector *get_primary_list() - { - return &viewscreen->animal; - } - - virtual void do_post_init() - { - is_vermin = &viewscreen->is_vermin; - is_tame = &viewscreen->is_tame; - is_adopting = &viewscreen->is_adopting; - } - - string get_element_description(df::viewscreen_petst::T_animal element) const - { - return get_unit_description(element.unit); - } - - bool should_check_input() - { - return viewscreen->mode == T_mode::List; - } - - bool is_valid_for_search(size_t i) - { - return is_vermin_s[i] == 0; - } - - void save_secondary_values() - { - is_vermin_s = *is_vermin; - is_tame_s = *is_tame; - is_adopting_s = *is_adopting; - } - - void reset_secondary_viewscreen_vectors() - { - is_vermin = NULL; - is_tame = NULL; - is_adopting = NULL; - } - - void update_saved_secondary_list_item(size_t i, size_t j) - { - is_vermin_s[i] = (*is_vermin)[j]; - is_tame_s[i] = (*is_tame)[j]; - is_adopting_s[i] = (*is_adopting)[j]; - } - - void clear_secondary_viewscreen_vectors() - { - is_vermin->clear(); - is_tame->clear(); - is_adopting->clear(); - } - - void add_to_filtered_secondary_lists(size_t i) - { - is_vermin->push_back(is_vermin_s[i]); - is_tame->push_back(is_tame_s[i]); - is_adopting->push_back(is_adopting_s[i]); - } - - void clear_secondary_saved_lists() - { - is_vermin_s.clear(); - is_tame_s.clear(); - is_adopting_s.clear(); - } - - void restore_secondary_values() - { - *is_vermin = is_vermin_s; - *is_tame = is_tame_s; - *is_adopting = is_adopting_s; - } - - bool is_match(T_animal &a, T_animal &b) - { - return a.unit == b.unit; - } - - bool is_match(vector &a, vector &b) - { - for (size_t i = 0; i < a.size(); i++) - { - if (!is_match(a[i], b[i])) - return false; - } - - return true; - } - - std::vector *is_vermin, is_vermin_s; - std::vector *is_tame, is_tame_s; - std::vector *is_adopting, is_adopting_s; -}; - -IMPLEMENT_HOOKS_WITH_ID(df::viewscreen_petst, pets_search, 1, 0); - -// -// END: Animal screen search -// - - -// -// START: Animal knowledge screen search -// - -typedef search_generic animal_knowledge_search_base; -class animal_knowledge_search : public animal_knowledge_search_base -{ - typedef df::viewscreen_petst::T_mode T_mode; - bool can_init(df::viewscreen_petst *screen) - { - return animal_knowledge_search_base::can_init(screen) && screen->mode == T_mode::TrainingKnowledge; - } - -public: - void render() const - { - print_search_option(2, 4); - } - -private: - int32_t *get_viewscreen_cursor() - { - return NULL; - } - - vector *get_primary_list() - { - return &viewscreen->known; - } - - string get_element_description(int32_t id) const - { - auto craw = df::creature_raw::find(id); - string out; - if (craw) - { - for (size_t i = 0; i < 3; ++i) - out += craw->name[i] + " "; - } - return out; - } -}; - -IMPLEMENT_HOOKS_WITH_ID(df::viewscreen_petst, animal_knowledge_search, 2, 0); - -// -// END: Animal knowledge screen search -// - - -// -// START: Animal trainer search -// - -typedef search_twocolumn_modifiable animal_trainer_search_base; -class animal_trainer_search : public animal_trainer_search_base -{ - typedef df::viewscreen_petst::T_mode T_mode; - typedef df::viewscreen_petst::T_trainer_mode T_trainer_mode; - - bool can_init(df::viewscreen_petst *screen) - { - return animal_trainer_search_base::can_init(screen) && screen->mode == T_mode::SelectTrainer; - } - -public: - void render() const - { - Screen::paintTile(Screen::Pen('\xBA', 8, 0), 14, 2); - Screen::paintTile(Screen::Pen('\xBA', 8, 0), gps->dimx - 14, 2); - Screen::paintTile(Screen::Pen('\xC9', 8, 0), 14, 1); - Screen::paintTile(Screen::Pen('\xBB', 8, 0), gps->dimx - 14, 1); - for (int x = 15; x <= gps->dimx - 15; ++x) - { - Screen::paintTile(Screen::Pen('\xCD', 8, 0), x, 1); - Screen::paintTile(Screen::Pen('\x00', 0, 0), x, 2); - } - print_search_option(16, 2); - } - -private: - int32_t *get_viewscreen_cursor() - { - return &viewscreen->trainer_cursor; - } - - vector *get_primary_list() - { - return &viewscreen->trainer_unit; - } - - string get_element_description(df::unit *u) const - { - return get_unit_description(u); - } - - std::vector *get_secondary_list() - { - return &viewscreen->trainer_mode; - } - -public: - bool process_input(set *input) - { - if (input->count(interface_key::SELECT) && viewscreen->trainer_unit.empty() && !in_entry_mode()) - return true; - return animal_trainer_search_base::process_input(input); - } - -}; - -IMPLEMENT_HOOKS_WITH_ID(df::viewscreen_petst, animal_trainer_search, 3, 0); - -// -// END: Animal trainer search -// - - -// -// START: Stocks screen search -// -typedef search_generic stocks_search_base; -class stocks_search : public stocks_search_base -{ -public: - - void render() const - { - if (!viewscreen->in_group_mode) - print_search_option(2); - else - { - auto dim = Screen::getWindowSize(); - int x = 2, y = dim.y - 2; - OutputString(15, x, y, "Tab to enable Search"); - } - } - - bool process_input(set *input) - { - if (viewscreen->in_group_mode) - return false; - - redo_search = false; - - if ((input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)) && !viewscreen->in_right_list) - { - // Redo search if category changes - saved_list1.clear(); - end_entry_mode(); - if (search_string.length() > 0) - redo_search = true; - - return false; - } - - return stocks_search_base::process_input(input); - } - - virtual void do_post_input_feed() - { - if (viewscreen->in_group_mode) - { - // Disable search if item lists are grouped - clear_search(); - reset_search(); - } - else if (redo_search) - { - do_search(); - redo_search = false; - } - } - -private: - int32_t *get_viewscreen_cursor() - { - return &viewscreen->item_cursor; - } - - virtual vector *get_primary_list() - { - return &viewscreen->items; - } - - -private: - string get_element_description(df::item *element) const - { - if (!element) - return ""; - return Items::getDescription(element, 0, true); - } - - bool redo_search; -}; - - -IMPLEMENT_HOOKS_PRIO(df::viewscreen_storesst, stocks_search, 100); - -// -// END: Stocks screen search -// - - -// -// START: Unit screen search -// -typedef search_twocolumn_modifiable unitlist_search_base; -class unitlist_search : public unitlist_search_base -{ -public: - void render() const - { - print_search_option(28); - } - -private: - void do_post_init() - { - unitlist_search_base::do_post_init(); - read_only = true; - } - - static string get_non_work_description(df::unit *unit) - { - if (!unit) - return ""; - for (auto p = unit->status.misc_traits.begin(); p < unit->status.misc_traits.end(); p++) - { - if ((*p)->id == misc_trait_type::Migrant) - { - return ".new arrival.migrant"; - } - } - - if (Units::isBaby(unit) || - Units::isChild(unit) || - unit->profession == profession::DRUNK) - { - return ""; - } - - if (ENUM_ATTR(profession, military, unit->profession)) - return ".military"; - - return ".idle.no job"; - } - - string get_element_description(df::unit *unit) const - { - if (!unit) - return "Inactive"; - string desc = get_unit_description(unit); - if (!unit->job.current_job) - { - desc += get_non_work_description(unit); - } - - return desc; - } - - bool should_check_input(set *input) - { - if (input->count(interface_key::STANDARDSCROLL_LEFT) || - input->count(interface_key::STANDARDSCROLL_RIGHT) || - (!in_entry_mode() && input->count(interface_key::UNITVIEW_PRF_PROF))) - { - if (!in_entry_mode()) - { - // Changing screens, reset search - int32_t *cursor_pos = get_viewscreen_cursor(); - if (cursor_pos && *cursor_pos < 0) - *cursor_pos = 0; - clear_search(); - reset_all(); - return false; - } - else - { - // Ignore cursor keys when typing - input->erase(interface_key::STANDARDSCROLL_LEFT); - input->erase(interface_key::STANDARDSCROLL_RIGHT); - } - } - - return true; - } - - char get_search_select_key() - { - return 'q'; - } - - vector *get_secondary_list() - { - return &viewscreen->jobs[viewscreen->page]; - } - - int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor_pos[viewscreen->page]; - } - - vector *get_primary_list() - { - return &viewscreen->units[viewscreen->page]; - } -}; - -typedef generic_search_hook unitlist_search_hook; -IMPLEMENT_HOOKS_PRIO(df::viewscreen_unitlistst, unitlist_search, 100); - -// -// END: Unit screen search -// - - -// -// START: Trade screen search -// -class trade_search_base : public search_multicolumn_modifiable -{ -protected: - virtual vector *get_selected_list() = 0; - virtual vector *get_count_list() = 0; - -private: - string get_element_description(df::item *element) const - { - if (!element) - return ""; - return Items::getDescription(element, 0, true); - } - - bool should_check_input(set *input) - { - if (in_entry_mode()) - return true; - - if (input->count(interface_key::TRADE_TRADE) || - input->count(interface_key::TRADE_OFFER) || - input->count(interface_key::TRADE_SEIZE)) - { - // Block the keys if were searching - if (!search_string.empty()) - { - input->clear(); - } - - return false; - } - else if (input->count(interface_key::CUSTOM_ALT_C)) - { - clear_search_for_trade(); - return true; - } - - return true; - } - - void clear_search_for_trade() - { - // Trying to trade, reset search - clear_search(); - reset_all(); - } - - void do_post_init() - { - search_multicolumn_modifiable::do_post_init(); - - selected = get_selected_list(); - count = get_count_list(); - } - - void save_secondary_values() - { - selected_s = *selected; - count_s = *count; - } - - void reset_secondary_viewscreen_vectors() - { - selected = NULL; - count = NULL; - } - - void update_saved_secondary_list_item(size_t i, size_t j) - { - selected_s[i] = (*selected)[j]; - count_s[i] = (*count)[j]; - } - - void clear_secondary_viewscreen_vectors() - { - selected->clear(); - count->clear(); - } - - void add_to_filtered_secondary_lists(size_t i) - { - selected->push_back(selected_s[i]); - count->push_back(count_s[i]); - } - - void clear_secondary_saved_lists() - { - selected_s.clear(); - count_s.clear(); - } - - void restore_secondary_values() - { - *selected = selected_s; - *count = count_s; - } - - std::vector *selected, selected_s; - std::vector *count, count_s; -}; - - -class trade_search_merc : public trade_search_base -{ -public: - virtual void render() const - { - if (viewscreen->counteroffer.size() > 0) - { - // The merchant is proposing a counteroffer. - // Not only is there nothing to search, - // but the native hotkeys are where we normally write. - return; - } - - print_search_option(2, -1); - - if (!search_string.empty()) - { - int32_t x = 2; - int32_t y = gps->dimy - 3; - make_text_dim(2, gps->dimx-2, y); - OutputString(COLOR_LIGHTRED, x, y, string(1, select_key + 'A' - 'a')); - OutputString(COLOR_WHITE, x, y, ": Clear search to trade "); - } - } - -private: - int32_t *get_viewscreen_cursor() - { - return &viewscreen->trader_cursor; - } - - vector *get_primary_list() - { - return &viewscreen->trader_items; - } - - vector *get_selected_list() - { - return &viewscreen->trader_selected; - } - - vector *get_count_list() - { - return &viewscreen->trader_count; - } - - char get_search_select_key() - { - return 'q'; - } -}; - -IMPLEMENT_HOOKS_WITH_ID(df::viewscreen_tradegoodsst, trade_search_merc, 1, 100); - - -class trade_search_fort : public trade_search_base -{ -public: - virtual void render() const - { - if (viewscreen->counteroffer.size() > 0) - { - // The merchant is proposing a counteroffer. - // Not only is there nothing to search, - // but the native hotkeys are where we normally write. - return; - } - - int32_t x = gps->dimx / 2 + 2; - print_search_option(x, -1); - - if (!search_string.empty()) - { - int32_t y = gps->dimy - 3; - make_text_dim(2, gps->dimx-2, y); - OutputString(COLOR_LIGHTRED, x, y, string(1, select_key + 'A' - 'a')); - OutputString(COLOR_WHITE, x, y, ": Clear search to trade "); - } - } - -private: - int32_t *get_viewscreen_cursor() - { - return &viewscreen->broker_cursor; - } - - vector *get_primary_list() - { - return &viewscreen->broker_items; - } - - vector *get_selected_list() - { - return &viewscreen->broker_selected; - } - - vector *get_count_list() - { - return &viewscreen->broker_count; - } - - char get_search_select_key() - { - return 'w'; - } -}; - -IMPLEMENT_HOOKS_WITH_ID(df::viewscreen_tradegoodsst, trade_search_fort, 2, 100); - -// -// END: Trade screen search -// - - -// -// START: Stockpile screen search -// -typedef layered_search stocks_layer; -class stockpile_search : public search_twocolumn_modifiable -{ -public: - void update_saved_secondary_list_item(size_t i, size_t j) - { - *saved_secondary_list[i] = *(*secondary_list)[j]; - } - - string get_element_description(string *element) const - { - return *element; - } - - void render() const - { - print_search_option(51, 23); - } - - vector *get_primary_list() - { - return &viewscreen->item_names; - } - - vector *get_secondary_list() - { - return &viewscreen->item_status; - } - - bool should_check_input(set *input) - { - if (input->count(interface_key::STOCKPILE_SETTINGS_DISABLE) && !in_entry_mode() && !search_string.empty()) - { - // Restore original list - clear_search(); - reset_all(); - } - - return true; - } - -}; - -IMPLEMENT_HOOKS(df::viewscreen_layer_stockpilest, stockpile_search); - -// -// END: Stockpile screen search -// - - -// -// START: Military screen search -// -typedef layered_search military_search_base; -class military_search : public military_search_base -{ -public: - - string get_element_description(df::unit *element) const - { - return get_unit_description(element); - } - - void render() const - { - print_search_option(52, 22); - } - - char get_search_select_key() - { - return 'q'; - } - - // When not on the positions page, this list is used for something - // else entirely, so screwing with it seriously breaks stuff. - bool is_list_valid(df::viewscreen_layer_militaryst *screen) - { - if (screen->page != df::viewscreen_layer_militaryst::Positions) - return false; - - return true; - } - - vector *get_primary_list() - { - return &viewscreen->positions.candidates; - } - - bool should_check_input(set *input) - { - if (input->count(interface_key::SELECT) && !in_entry_mode() && !search_string.empty()) - { - // About to make an assignment, so restore original list (it will be changed by the game) - int32_t *cursor = get_viewscreen_cursor(); - auto list = get_primary_list(); - if (size_t(*cursor) >= list->size()) - return false; - df::unit *selected_unit = list->at(*cursor); - clear_search(); - - for (*cursor = 0; size_t(*cursor) < list->size(); (*cursor)++) - { - if (list->at(*cursor) == selected_unit) - break; - } - - reset_all(); - } - - return true; - } -}; - -IMPLEMENT_HOOKS_PRIO(df::viewscreen_layer_militaryst, military_search, 100); - -// -// END: Military screen search -// - - -// -// START: Room list search -// -typedef search_twocolumn_modifiable roomlist_search_base; -class roomlist_search : public roomlist_search_base -{ -public: - void render() const - { - print_search_option(2, 23); - } - -private: - void do_post_init() - { - roomlist_search_base::do_post_init(); - read_only = true; - } - - string get_element_description(df::building *bld) const - { - if (!bld) - return ""; - - string desc; - desc.reserve(100); - if (bld->owner) - desc += get_unit_description(bld->owner); - - desc += "."; - - string room_desc = Buildings::getRoomDescription(bld, nullptr); - desc += room_desc; - if (room_desc.empty()) - { - if (!bld->owner) - desc += "no owner"; - - string name; - bld->getName(&name); - if (!name.empty()) - { - desc += name; - } - } - - return desc; - } - - vector *get_secondary_list() - { - return &viewscreen->room_value; - } - - int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor; - } - - vector *get_primary_list() - { - return &viewscreen->buildings; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_buildinglistst, roomlist_search); - -// -// END: Room list search -// - - - -// -// START: Announcement list search -// -class announcement_search : public search_generic -{ -public: - void render() const - { - print_search_option(2, gps->dimy - 3); - } - -private: - int32_t *get_viewscreen_cursor() - { - return &viewscreen->sel_idx; - } - - virtual vector *get_primary_list() - { - return &viewscreen->reports; - } - - -private: - string get_element_description(df::report *element) const - { - if (!element) - return ""; - return element->text; - } -}; - - -IMPLEMENT_HOOKS(df::viewscreen_announcelistst, announcement_search); - -// -// END: Announcement list search -// - - -// -// START: Nobles search list -// -typedef df::viewscreen_layer_noblelistst::T_candidates T_candidates; -typedef layered_search nobles_search_base; -class nobles_search : public nobles_search_base -{ -public: - - string get_element_description(T_candidates *element) const - { - if (!element->unit) - return ""; - - return get_unit_description(element->unit); - } - - void render() const - { - print_search_option(2, 23); - } - - bool force_in_search(size_t index) - { - return index == 0; // Leave Vacant - } - - bool can_init(df::viewscreen_layer_noblelistst *screen) - { - if (screen->mode != df::viewscreen_layer_noblelistst::Appoint) - return false; - - return nobles_search_base::can_init(screen); - } - - vector *get_primary_list() - { - return &viewscreen->candidates; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_layer_noblelistst, nobles_search); - -// -// END: Nobles search list -// - -// -// START: Workshop profiles search list -// -typedef search_generic profiles_search_base; -class profiles_search : public profiles_search_base -{ -public: - - bool can_init (df::viewscreen_workshop_profilest *screen) - { - return screen->tab == df::viewscreen_workshop_profilest::T_tab::Workers; - } - - string get_element_description(df::unit *element) const - { - return get_unit_description(element); - } - - void render() const - { - print_search_option(2, gps->dimy - 5); - } - - vector *get_primary_list() - { - return &viewscreen->workers; - } - - int32_t *get_viewscreen_cursor() - { - return &viewscreen->worker_idx; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_workshop_profilest, profiles_search); - -// -// END: Workshop profiles search list -// - - -// -// START: Job list search -// -void get_job_details(string &desc, df::job *job) -{ - string job_name = ENUM_KEY_STR(job_type,job->job_type); - for (size_t i = 0; i < job_name.length(); i++) - { - char c = job_name[i]; - if (c >= 'A' && c <= 'Z') - desc += " "; - desc += c; - } - desc += "."; - - df::item_type itype = ENUM_ATTR(job_type, item, job->job_type); - - MaterialInfo mat(job); - if (itype == item_type::FOOD) - mat.decode(-1); - - if (mat.isValid() || job->material_category.whole) - { - desc += mat.toString(); - desc += "."; - if (job->material_category.whole) - { - desc += bitfield_to_string(job->material_category); - desc += "."; - } - } - - if (!job->reaction_name.empty()) - { - for (size_t i = 0; i < job->reaction_name.length(); i++) - { - if (job->reaction_name[i] == '_') - desc += " "; - else - desc += job->reaction_name[i]; - } - - desc += "."; - } - - if (job->flags.bits.suspend) - desc += "suspended."; -} - -typedef search_twocolumn_modifiable joblist_search_base; -class joblist_search : public joblist_search_base -{ -public: - void render() const - { - print_search_option(2); - } - -private: - void do_post_init() - { - joblist_search_base::do_post_init(); - read_only = true; - } - - string get_element_description(df::job *element) const - { - if (!element) - return "no job.idle"; - - string desc; - desc.reserve(100); - get_job_details(desc, element); - df::unit *worker = DFHack::Job::getWorker(element); - if (worker) - desc += get_unit_description(worker); - else - desc += "Inactive"; - - return desc; - } - - char get_search_select_key() - { - return 'q'; - } - - vector *get_secondary_list() - { - return &viewscreen->units; - } - - int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor_pos; - } - - vector *get_primary_list() - { - return &viewscreen->jobs; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_joblistst, joblist_search); - -// -// END: Job list search -// - - -// -// START: Look menu search -// - -typedef search_generic look_menu_search_base; -class look_menu_search : public look_menu_search_base -{ - typedef df::ui_look_list::T_items::T_type elt_type; -public: - bool can_init(df::viewscreen_dwarfmodest *screen) - { - if (plotinfo->main.mode == df::ui_sidebar_mode::LookAround) - { - return look_menu_search_base::can_init(screen); - } - - return false; - } - - string get_element_description(df::ui_look_list::T_items *element) const - { - std::string desc = ""; - switch (element->type) - { - case elt_type::Item: - if (element->data.Item) - desc = Items::getDescription(element->data.Item, 0, true); - break; - case elt_type::Unit: - if (element->data.Unit) - desc = get_unit_description(element->data.Unit); - break; - case elt_type::Building: - if (element->data.Building) - element->data.Building->getName(&desc); - break; - default: - break; - } - return desc; - } - - bool force_in_search (size_t i) - { - df::ui_look_list::T_items *element = saved_list1[i]; - switch (element->type) - { - case elt_type::Item: - case elt_type::Unit: - case elt_type::Building: - return false; - break; - default: - return true; - break; - } - } - - void render() const - { - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 1; - - print_search_option(x, y); - } - - vector *get_primary_list() - { - return &ui_look_list->items; - } - - virtual int32_t * get_viewscreen_cursor() - { - return ui_look_cursor; - } - - - bool should_check_input(set *input) - { - if (input->count(interface_key::SECONDSCROLL_UP) - || input->count(interface_key::SECONDSCROLL_DOWN) - || input->count(interface_key::SECONDSCROLL_PAGEUP) - || input->count(interface_key::SECONDSCROLL_PAGEDOWN)) - { - end_entry_mode(); - return false; - } - bool hotkey_pressed = - input->lower_bound(interface_key::D_HOTKEY1) != input->upper_bound(interface_key::D_HOTKEY16); - if (cursor_key_pressed(input, in_entry_mode()) || hotkey_pressed) - { - end_entry_mode(); - clear_search(); - return false; - } - - return true; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_dwarfmodest, look_menu_search); - -// -// END: Look menu search -// - - -// -// START: Burrow assignment search -// - -typedef search_twocolumn_modifiable burrow_search_base; -class burrow_search : public burrow_search_base -{ -public: - bool can_init(df::viewscreen_dwarfmodest *screen) - { - if (plotinfo->main.mode == df::ui_sidebar_mode::Burrows && plotinfo->burrows.in_add_units_mode) - { - return burrow_search_base::can_init(screen); - } - - return false; - } - - string get_element_description(df::unit *element) const - { - return get_unit_description(element); - } - - void render() const - { - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 23; - - print_search_option(x, y); - } - - vector *get_primary_list() - { - return &plotinfo->burrows.list_units; - } - - vector *get_secondary_list() - { - return &plotinfo->burrows.sel_units; - } - - virtual int32_t * get_viewscreen_cursor() - { - return &plotinfo->burrows.unit_cursor_pos; - } - - - bool should_check_input(set *input) - { - if (input->count(interface_key::SECONDSCROLL_UP) || input->count(interface_key::SECONDSCROLL_DOWN) - || input->count(interface_key::SECONDSCROLL_PAGEUP) || input->count(interface_key::SECONDSCROLL_PAGEDOWN)) - { - end_entry_mode(); - return false; - } - - return true; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_dwarfmodest, burrow_search); - -// -// END: Burrow assignment search -// - - -// -// START: Room assignment search -// - -typedef search_generic room_assign_search_base; -class room_assign_search : public room_assign_search_base -{ -public: - bool can_init(df::viewscreen_dwarfmodest *screen) - { - if (plotinfo->main.mode == df::ui_sidebar_mode::QueryBuilding && *ui_building_in_assign) - { - return room_assign_search_base::can_init(screen); - } - - return false; - } - - string get_element_description(df::unit *element) const - { - return element ? get_unit_description(element) : "Nobody"; - } - - void render() const - { - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 19; - - print_search_option(x, y); - } - - vector *get_primary_list() - { - return ui_building_assign_units; - } - - virtual int32_t * get_viewscreen_cursor() - { - return ui_building_item_cursor; - } - - bool should_check_input(set *input) - { - if (input->count(interface_key::SECONDSCROLL_UP) || input->count(interface_key::SECONDSCROLL_DOWN) - || input->count(interface_key::SECONDSCROLL_PAGEUP) || input->count(interface_key::SECONDSCROLL_PAGEDOWN)) - { - end_entry_mode(); - return false; - } - - return true; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_dwarfmodest, room_assign_search); - -// -// END: Room assignment search -// - -// -// START: Noble suggestion search -// - -typedef search_generic noble_suggest_search_base; -class noble_suggest_search : public noble_suggest_search_base -{ -public: - string get_element_description (int32_t hf_id) const - { - df::historical_figure *histfig = df::historical_figure::find(hf_id); - if (!histfig) - return ""; - df::unit *unit = df::unit::find(histfig->unit_id); - if (!unit) - return ""; - return get_unit_description(unit); - } - - void render() const - { - print_search_option(2, gps->dimy - 4); - } - - vector *get_primary_list() - { - return &viewscreen->candidate_histfig_ids; - } - - virtual int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor; - } - -}; - -IMPLEMENT_HOOKS(df::viewscreen_topicmeeting_fill_land_holder_positionsst, noble_suggest_search); - -// -// END: Noble suggestion search -// - -// -// START: Location occupation assignment search -// - -typedef search_generic location_assign_occupation_search_base; -class location_assign_occupation_search : public location_assign_occupation_search_base -{ -public: - bool can_init (df::viewscreen_locationsst *screen) - { - return screen->menu == df::viewscreen_locationsst::AssignOccupation; - } - - string get_element_description (df::unit *unit) const - { - return unit ? get_unit_description(unit) : "Nobody"; - } - - void render() const - { - print_search_option(37, gps->dimy - 3); - } - - vector *get_primary_list() - { - return &viewscreen->units; - } - - virtual int32_t *get_viewscreen_cursor() - { - return &viewscreen->unit_idx; - } - -}; - -IMPLEMENT_HOOKS(df::viewscreen_locationsst, location_assign_occupation_search); - -// -// END: Location occupation assignment search -// - -// -// START: Kitchen preferences search -// - -typedef search_multicolumn_modifiable kitchen_pref_search_base; -class kitchen_pref_search : public kitchen_pref_search_base -{ -public: - - string get_element_description(string *s) const override - { - return s ? *s : ""; - } - - void render() const override - { - print_search_option(40, gps->dimy - 2); - } - - int32_t *get_viewscreen_cursor() override - { - return &viewscreen->cursor; - } - - vector *get_primary_list() override - { - return &viewscreen->item_str[viewscreen->page]; - } - - bool should_check_input(set *input) override - { - if (input->count(interface_key::CHANGETAB) || input->count(interface_key::SEC_CHANGETAB)) - { - // Restore original list - clear_search(); - reset_all(); - } - - return true; - } - - -#define KITCHEN_VECTORS \ - KVEC(df::item_type, item_type); \ - KVEC(int16_t, item_subtype); \ - KVEC(int16_t, mat_type); \ - KVEC(int32_t, mat_index); \ - KVEC(int32_t, count); \ - KVEC(df::kitchen_pref_flag, forbidden); \ - KVEC(df::kitchen_pref_flag, possible) - - - virtual void do_post_init() - { - kitchen_pref_search_base::do_post_init(); - #define KVEC(type, name) name = &viewscreen->name[viewscreen->page] - KITCHEN_VECTORS; - #undef KVEC - } - - void save_secondary_values() - { - #define KVEC(type, name) name##_s = *name - KITCHEN_VECTORS; - #undef KVEC - } - - void reset_secondary_viewscreen_vectors() - { - #define KVEC(type, name) name = nullptr - KITCHEN_VECTORS; - #undef KVEC - } - - virtual void update_saved_secondary_list_item(size_t i, size_t j) - { - #define KVEC(type, name) name##_s[i] = (*name)[j]; - KITCHEN_VECTORS; - #undef KVEC - } - - void clear_secondary_viewscreen_vectors() - { - #define KVEC(type, name) name->clear() - KITCHEN_VECTORS; - #undef KVEC - } - - void add_to_filtered_secondary_lists(size_t i) - { - #define KVEC(type, name) name->push_back(name##_s[i]) - KITCHEN_VECTORS; - #undef KVEC - } - - void clear_secondary_saved_lists() - { - #define KVEC(type, name) name##_s.clear() - KITCHEN_VECTORS; - #undef KVEC - } - - void restore_secondary_values() - { - #define KVEC(type, name) *name = name##_s - KITCHEN_VECTORS; - #undef KVEC - } - - #define KVEC(type, name) vector *name, name##_s - KITCHEN_VECTORS; - #undef KVEC -#undef KITCHEN_VECTORS -}; - -IMPLEMENT_HOOKS(df::viewscreen_kitchenprefst, kitchen_pref_search); - -// -// END: Kitchen preferences search -// - - -// -// START: Stone status screen search -// -typedef layered_search stone_search_layer; -class stone_search : public search_twocolumn_modifiable -{ - // bool in_update = false; -public: - void render() const override - { - print_search_option(21, 23); - } - - vector *get_primary_list() override - { - return &viewscreen->stone_type[viewscreen->type_tab]; - } - - vector *get_secondary_list() override - { - return &viewscreen->stone_economic[viewscreen->type_tab]; - } - - string get_element_description(int32_t stone_type) const override - { - auto iraw = vector_get(world->raws.inorganics, stone_type); - if (!iraw) - return ""; - return iraw->material.stone_name + " " + iraw->material.state_name[0]; - } - - bool should_check_input(set *input) override - { - // if (in_update) - // return false; - - if (input->count(interface_key::CHANGETAB)) - { - // Restore original list - clear_search(); - reset_all(); - } - - return true; - } - - // virtual void do_post_input_feed() override - // { - // auto *list1 = get_primary_list(); - // auto *list2 = get_secondary_list(); - // bool appended = false; - // if (list1->empty()) - // { - // // Clear uses - // auto *use_list = virtual_cast(viewscreen->layer_objects[4]); - // if (use_list) - // use_list->num_entries = 0; - // return; - // } - // else if (list1->size() == 1) - // { - // list1->push_back(list1->back()); - // list2->push_back(list2->back()); - // appended = true; - // } - - // in_update = true; - // Core::printerr("updating\n"); - // viewscreen->feed_key(interface_key::STANDARDSCROLL_DOWN); - // viewscreen->feed_key(interface_key::STANDARDSCROLL_UP); - // Core::printerr("updating done\n"); - // in_update = false; - - // if (appended) - // { - // list1->pop_back(); - // list2->pop_back(); - // } - // } -}; - -IMPLEMENT_HOOKS(df::viewscreen_layer_stone_restrictionst, stone_search); - -// -// END: Stone status screen search -// - -// -// START: Justice screen conviction search -// - -typedef search_generic justice_conviction_search_base; -class justice_conviction_search : public justice_conviction_search_base -{ -public: - bool can_init (df::viewscreen_justicest *screen) - { - return screen->cur_column == df::viewscreen_justicest::ConvictChoices; - } - - string get_element_description (df::unit *unit) const - { - return get_unit_description(unit); - } - - void render() const - { - print_search_option(37); - } - - vector *get_primary_list() - { - return &viewscreen->convict_choices; - } - - virtual int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor_right; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_justicest, justice_conviction_search); - -// -// END: Justice screen conviction search -// - -// -// START: Justice screen interrogation search -// - -typedef search_generic justice_interrogation_search_base; -class justice_interrogation_search : public justice_interrogation_search_base -{ -public: - bool can_init (df::viewscreen_justicest *screen) - { - return screen->cur_column == df::viewscreen_justicest::InterrogateChoices; - } - - string get_element_description (df::unit *unit) const - { - return get_unit_description(unit); - } - - void render() const - { - print_search_option(37); - } - - vector *get_primary_list() - { - return &viewscreen->interrogate_choices; - } - - virtual int32_t *get_viewscreen_cursor() - { - return &viewscreen->cursor_right; - } -}; - -IMPLEMENT_HOOKS(df::viewscreen_justicest, justice_interrogation_search); - -// -// END: Justice screen conviction search -// - -#define SEARCH_HOOKS \ - HOOK_ACTION(unitlist_search_hook) \ - HOOK_ACTION(roomlist_search_hook) \ - HOOK_ACTION(trade_search_merc_hook) \ - HOOK_ACTION(trade_search_fort_hook) \ - HOOK_ACTION(stocks_search_hook) \ - HOOK_ACTION(pets_search_hook) \ - HOOK_ACTION(animal_knowledge_search_hook) \ - HOOK_ACTION(animal_trainer_search_hook) \ - HOOK_ACTION(military_search_hook) \ - HOOK_ACTION(nobles_search_hook) \ - HOOK_ACTION(profiles_search_hook) \ - HOOK_ACTION(announcement_search_hook) \ - HOOK_ACTION(joblist_search_hook) \ - HOOK_ACTION(look_menu_search_hook) \ - HOOK_ACTION(burrow_search_hook) \ - HOOK_ACTION(stockpile_search_hook) \ - HOOK_ACTION(room_assign_search_hook) \ - HOOK_ACTION(noble_suggest_search_hook) \ - HOOK_ACTION(location_assign_occupation_search_hook) \ - HOOK_ACTION(kitchen_pref_search_hook) \ - HOOK_ACTION(stone_search_hook) \ - HOOK_ACTION(justice_conviction_search_hook) \ - HOOK_ACTION(justice_interrogation_search_hook) \ - - -DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) -{ - if (!gps || !gview) - return CR_FAILURE; - - if (is_enabled != enable) - { -#define HOOK_ACTION(hook) \ - !INTERPOSE_HOOK(hook, feed).apply(enable) || \ - !INTERPOSE_HOOK(hook, render).apply(enable) || \ - !INTERPOSE_HOOK(hook, key_conflict).apply(enable) || - - if (SEARCH_HOOKS 0) - return CR_FAILURE; - - is_enabled = enable; - } -#undef HOOK_ACTION - - return CR_OK; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) -{ - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ -#define HOOK_ACTION(hook) \ - INTERPOSE_HOOK(hook, feed).remove(); \ - INTERPOSE_HOOK(hook, render).remove(); \ - INTERPOSE_HOOK(hook, key_conflict).remove(); - - SEARCH_HOOKS - -#undef HOOK_ACTION - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event event ) -{ -#define HOOK_ACTION(hook) hook::module.reset_on_change(); - - switch (event) { - case SC_VIEWSCREEN_CHANGED: - SEARCH_HOOKS - break; - - default: - break; - } - - return CR_OK; - -#undef HOOK_ACTION -} - -#undef IMPLEMENT_HOOKS -#undef SEARCH_HOOKS diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 4a2a03f3a..f56470c66 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -96,6 +96,24 @@ static void remove_seed_config(color_ostream &out, int id) { watched_seeds.erase(id); } +// this validation removes configuration data from versions prior to 50.09-r3 +// it can be removed once saves from 50.09 are no longer loadable + +static bool validate_seed_config(color_ostream& out, PersistentDataItem c) +{ + int seed_id = get_config_val(c, SEED_CONFIG_ID); + auto plant = df::plant_raw::find(seed_id); + if (!plant) { + WARN(config, out).print("discarded invalid seed id: %d\n", seed_id); + return false; + } + bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); + if (!valid) { + DEBUG(config, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); + } + return valid; +} + static const int32_t CYCLE_TICKS = 1200; static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle @@ -171,7 +189,8 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { world_plant_ids.clear(); for (size_t i = 0; i < world->raws.plants.all.size(); ++i) { auto & plant = world->raws.plants.all[i]; - if (plant->material_defs.type[plant_material_def::seed] != -1) + if (plant->material_defs.type[plant_material_def::seed] != -1 && + !plant->flags.is_set(df::enums::plant_raw_flags::TREE)) world_plant_ids[plant->id] = i; } @@ -180,8 +199,9 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { World::GetPersistentData(&seed_configs, SEED_CONFIG_KEY_PREFIX, true); const size_t num_seed_configs = seed_configs.size(); for (size_t idx = 0; idx < num_seed_configs; ++idx) { - auto &c = seed_configs[idx]; - watched_seeds.emplace(get_config_val(c, SEED_CONFIG_ID), c); + auto& c = seed_configs[idx]; + if (validate_seed_config(out, c)) + watched_seeds.emplace(get_config_val(c, SEED_CONFIG_ID), c); } config = World::GetPersistentData(CONFIG_KEY); diff --git a/plugins/sort.cpp b/plugins/sort.cpp index 62af2c416..453914f5a 100644 --- a/plugins/sort.cpp +++ b/plugins/sort.cpp @@ -1,43 +1,13 @@ -#include "Core.h" -#include "Console.h" -#include "Export.h" #include "PluginManager.h" -#include "modules/Gui.h" -#include "modules/Translation.h" -#include "modules/Units.h" -#include "modules/Job.h" - -#include "LuaTools.h" - -#include "DataDefs.h" -#include "df/plotinfost.h" -#include "df/world.h" -#include "df/viewscreen_joblistst.h" -#include "df/viewscreen_unitlistst.h" -#include "df/viewscreen_layer_militaryst.h" -#include "df/viewscreen_layer_noblelistst.h" -#include "df/viewscreen_layer_overall_healthst.h" -#include "df/viewscreen_layer_assigntradest.h" -#include "df/viewscreen_tradegoodsst.h" -#include "df/viewscreen_dwarfmodest.h" -#include "df/viewscreen_petst.h" -#include "df/viewscreen_storesst.h" -#include "df/viewscreen_workshop_profilest.h" -#include "df/layer_object_listst.h" -#include "df/assign_trade_status.h" - -#include "MiscUtils.h" - -#include - using std::vector; using std::string; -using std::endl; + using namespace DFHack; -using namespace df::enums; DFHACK_PLUGIN("sort"); + +/* REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(ui_building_in_assign); @@ -52,16 +22,18 @@ static bool item_list_hotkey(df::viewscreen *top); static command_result sort_units(color_ostream &out, vector & parameters); static command_result sort_items(color_ostream &out, vector & parameters); +*/ -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init (color_ostream &out, vector &commands) { - commands.push_back(PluginCommand( - "sort-units", "Sort the visible unit list.", sort_units, unit_list_hotkey)); - commands.push_back(PluginCommand( - "sort-items", "Sort the visible item list.", sort_items, item_list_hotkey)); + // commands.push_back(PluginCommand( + // "sort-units", "Sort the visible unit list.", sort_units, unit_list_hotkey)); + // commands.push_back(PluginCommand( + // "sort-items", "Sort the visible item list.", sort_items, item_list_hotkey)); return CR_OK; } +/* DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; @@ -232,10 +204,7 @@ typedef void (*SortHandler)(color_ostream *pout, lua_State *L, int top, static std::map unit_sorters; -/* - * Sort units in the 'u'nit list screen. - */ - +// Sort units in the 'u'nit list screen. DEFINE_SORT_HANDLER(unit_sorters, unitlist, "", units) { PARSE_SPEC("units", parameters); @@ -250,10 +219,7 @@ DEFINE_SORT_HANDLER(unit_sorters, unitlist, "", units) } } -/* - * Sort units in the 'j'ob list screen. - */ - +//Sort units in the 'j'ob list screen. DEFINE_SORT_HANDLER(unit_sorters, joblist, "", jobs) { PARSE_SPEC("units", parameters); @@ -275,10 +241,7 @@ DEFINE_SORT_HANDLER(unit_sorters, joblist, "", jobs) } } -/* - * Sort candidate units in the 'p'osition page of the 'm'ilitary screen. - */ - +// Sort candidate units in the 'p'osition page of the 'm'ilitary screen. DEFINE_SORT_HANDLER(unit_sorters, layer_military, "/Positions/Candidates", military) { auto &candidates = military->positions.candidates; @@ -293,7 +256,6 @@ DEFINE_SORT_HANDLER(unit_sorters, layer_military, "/Positions/Candidates", milit } } - DEFINE_SORT_HANDLER(unit_sorters, layer_noblelist, "/Appoint", nobles) { auto list2 = getLayerList(nobles, 1); @@ -312,10 +274,7 @@ DEFINE_SORT_HANDLER(unit_sorters, layer_noblelist, "/Appoint", nobles) } } -/* - * Sort animal units in the Animal page of the 'z' status screen. - */ - +//Sort animal units in the Animal page of the 'z' status screen. DEFINE_SORT_HANDLER(unit_sorters, pet, "/List", animals) { PARSE_SPEC("units", parameters); @@ -334,10 +293,7 @@ DEFINE_SORT_HANDLER(unit_sorters, pet, "/List", animals) } } -/* - * Sort candidate trainers in the Animal page of the 'z' status screen. - */ - +// Sort candidate trainers in the Animal page of the 'z' status screen. DEFINE_SORT_HANDLER(unit_sorters, pet, "/SelectTrainer", animals) { sort_null_first(parameters); @@ -351,10 +307,7 @@ DEFINE_SORT_HANDLER(unit_sorters, pet, "/SelectTrainer", animals) } } -/* - * Sort units in the Health page of the 'z' status screen. - */ - +// Sort units in the Health page of the 'z' status screen. DEFINE_SORT_HANDLER(unit_sorters, layer_overall_health, "/Units", health) { auto list1 = getLayerList(health, 0); @@ -371,10 +324,7 @@ DEFINE_SORT_HANDLER(unit_sorters, layer_overall_health, "/Units", health) } } -/* - * Sort burrow member candidate units in the 'w' sidebar mode. - */ - +// Sort burrow member candidate units in the 'w' sidebar mode. DEFINE_SORT_HANDLER(unit_sorters, dwarfmode, "/Burrows/AddUnits", screen) { PARSE_SPEC("units", parameters); @@ -387,10 +337,7 @@ DEFINE_SORT_HANDLER(unit_sorters, dwarfmode, "/Burrows/AddUnits", screen) } } -/* - * Sort building owner candidate units in the 'q' sidebar mode, or cage assignment. - */ - +// Sort building owner candidate units in the 'q' sidebar mode, or cage assignment. DEFINE_SORT_HANDLER(unit_sorters, dwarfmode, "/QueryBuilding/Some/Assign", screen) { sort_null_first(parameters); @@ -410,10 +357,7 @@ DEFINE_SORT_HANDLER(unit_sorters, dwarfmode, "/QueryBuilding/Some/Assign", scree } } -/* - * Sort units in the workshop 'q'uery 'P'rofile modification screen. - */ - +// Sort units in the workshop 'q'uery 'P'rofile modification screen. DEFINE_SORT_HANDLER(unit_sorters, workshop_profile, "/Unit", profile) { PARSE_SPEC("units", parameters); @@ -425,10 +369,7 @@ DEFINE_SORT_HANDLER(unit_sorters, workshop_profile, "/Unit", profile) } } -/* - * Sort pen assignment candidate units in 'z'->'N'. - */ - +// Sort pen assignment candidate units in 'z'->'N'. DEFINE_SORT_HANDLER(unit_sorters, dwarfmode, "/ZonesPenInfo/Assign", screen) { PARSE_SPEC("units", parameters); @@ -562,3 +503,4 @@ static command_result sort_items(color_ostream &out, vector ¶meters return CR_OK; } +*/ diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index 4e2806e22..536ad3e4e 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -615,7 +615,9 @@ static bool serialize_list_creature(color_ostream& out, FuncWriteExport add_valu } static string get_filter_string(df::creature_raw *r) { - if (!r->caste.size() || !r->caste[0]->flags.is_set(df::enums::caste_raw_flags::PET)) + if (!r->caste.size() || + (!r->caste[0]->flags.is_set(df::enums::caste_raw_flags::PET) && + !r->caste[0]->flags.is_set(df::enums::caste_raw_flags::PET_EXOTIC))) return r->name[0]; return r->name[0] + "/tameable"; } diff --git a/plugins/stocks.cpp b/plugins/stocks.cpp index 8cc27067c..771160b4a 100644 --- a/plugins/stocks.cpp +++ b/plugins/stocks.cpp @@ -1,3 +1,13 @@ +#include "PluginManager.h" + +using std::vector; +using std::string; + +using namespace DFHack; + +DFHACK_PLUGIN("stocks"); + +/* #include "uicommon.h" #include "listcolumn.h" @@ -41,12 +51,12 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) #define MAX_NAME 30 #define SIDEBAR_WIDTH 30 - +*/ /* * Utility */ - +/* static string get_quality_name(const df::item_quality quality) { if (gps->dimx - SIDEBAR_WIDTH < 60) @@ -66,12 +76,12 @@ static df::item *get_container_of(df::unit *unit) auto ref = Units::getGeneralRef(unit, general_ref_type::CONTAINED_IN_ITEM); return (ref) ? ref->getItem() : nullptr; } - +*/ /* * Trade Info */ - +/* class TradeDepotInfo { public: @@ -168,12 +178,12 @@ private: }; static TradeDepotInfo depot_info; - +*/ /* * Item manipulation */ - +/* static map items_in_cages; static df::job *get_item_job(df::item *item) @@ -685,7 +695,7 @@ public: } else if (input->count(interface_key::HELP)) { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); } bool key_processed = false; @@ -950,7 +960,7 @@ public: OutputHotkeyString(x, y, "Min Qual: ", "-+"); OutputString(COLOR_BROWN, x, y, get_quality_name(min_quality), true, left_margin); - OutputHotkeyString(x, y, "Max Qual: ", "/*"); + OutputHotkeyString(x, y, "Max Qual: ", "/ *"); OutputString(COLOR_BROWN, x, y, get_quality_name(max_quality), true, left_margin); OutputHotkeyString(x, y, "Min Wear: ", "Shift-W"); OutputString(COLOR_BROWN, x, y, int_to_string(min_wear), true, left_margin); @@ -1350,7 +1360,7 @@ struct stocks_hook : public df::viewscreen_storesst if (input->count(interface_key::CUSTOM_E)) { Screen::dismiss(this); - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); return; } INTERPOSE_NEXT(feed)(input); @@ -1385,7 +1395,7 @@ struct stocks_stockpile_hook : public df::viewscreen_dwarfmodest if (input->count(interface_key::CUSTOM_I)) { - Screen::show(dts::make_unique(sp), plugin_self); + Screen::show(std::make_unique(sp), plugin_self); return true; } @@ -1459,26 +1469,29 @@ static command_result stocks_cmd(color_ostream &out, vector & parameter } else if (toLower(parameters[0])[0] == 's') { - Screen::show(dts::make_unique(), plugin_self); + Screen::show(std::make_unique(), plugin_self); return CR_OK; } } return CR_WRONG_USAGE; } +*/ -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init (color_ostream &out, vector &commands) { + /* commands.push_back(PluginCommand( "stocks", "An improved stocks management screen.", stocks_cmd)); ViewscreenStocks::reset(); - + */ return CR_OK; } +/* DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { @@ -1491,3 +1504,4 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan return CR_OK; } +*/ diff --git a/plugins/stonesense b/plugins/stonesense index d7fa20079..2f87534ce 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit d7fa20079e89cc6516a0f5406a5ad112436066bb +Subproject commit 2f87534cebdeee4ce7281333c8f71ae5b93790b9 diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index b300eb795..88d1df071 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -1217,7 +1217,6 @@ command_result df_strangemood (color_ostream &out, vector & parameters) ref->setID(unit->id); job->general_refs.push_back(ref); unit->job.current_job = job; - job->wait_timer = 0; // Generate the artifact's name if (type == mood_type::Fell || type == mood_type::Macabre) diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 2b44d11ae..ac71749ee 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -186,6 +186,9 @@ public: } if (i->getWear() >= 1) continue; + if (i->getMakerRace() < 0) // sometimes we get borked items with no valid maker race + continue; + df::item_type t = i->getType(); int size = world->raws.creatures.all[i->getMakerRace()]->adultsize; @@ -219,7 +222,7 @@ public: // only count dyed std::string d; i->getItemDescription(&d, 0); - TRACE(cycle).print("tailor: skipping undyed %s\n", d.c_str()); + TRACE(cycle).print("tailor: skipping undyed %s\n", DF2CONSOLE(d).c_str()); continue; } MaterialInfo mat(i); @@ -239,7 +242,7 @@ public: { std::string d; i->getItemDescription(&d, 0); - DEBUG(cycle).print("tailor: weird cloth item found: %s (%d)\n", d.c_str(), i->id); + DEBUG(cycle).print("tailor: weird cloth item found: %s (%d)\n", DF2CONSOLE(d).c_str(), i->id); } } } @@ -298,14 +301,14 @@ public: available[std::make_pair(ty, usize)] -= 1; DEBUG(cycle).print("tailor: allocating a %s (size %d) to %s\n", ENUM_KEY_STR(item_type, ty).c_str(), usize, - Translation::TranslateName(&u->name, false).c_str()); + DF2CONSOLE(Translation::TranslateName(&u->name, false)).c_str()); wearing.insert(ty); } else if (ordered.count(ty) == 0) { DEBUG(cycle).print ("tailor: %s (size %d) worn by %s (size %d) needs replacement, but none available\n", - description.c_str(), isize, - Translation::TranslateName(&u->name, false).c_str(), usize); + DF2CONSOLE(description).c_str(), isize, + DF2CONSOLE(Translation::TranslateName(&u->name, false)).c_str(), usize); needed[std::make_pair(ty, usize)] += 1; ordered.insert(ty); } @@ -320,8 +323,8 @@ public: INFO(cycle).print( "tailor: %s %s from %s.\n", (confiscated ? "confiscated" : "could not confiscate"), - description.c_str(), - Translation::TranslateName(&u->name, false).c_str() + DF2CONSOLE(description).c_str(), + DF2CONSOLE(Translation::TranslateName(&u->name, false)).c_str() ); } @@ -338,7 +341,7 @@ public: TRACE(cycle).print("tailor: one %s of size %d needed to cover %s\n", ENUM_KEY_STR(item_type, ty).c_str(), usize, - Translation::TranslateName(&u->name, false).c_str()); + DF2CONSOLE(Translation::TranslateName(&u->name, false)).c_str()); needed[std::make_pair(ty, usize)] += 1; } } @@ -413,7 +416,7 @@ public: { supply[m] -= o->amount_left; TRACE(cycle).print("tailor: supply of %s reduced by %d due to being required for an existing order\n", - m.name.c_str(), o->amount_left); + DF2CONSOLE(m.name).c_str(), o->amount_left); } } @@ -422,7 +425,6 @@ public: int size = world->raws.creatures.all[race]->adultsize; - auto tt = jobTypeMap.find(o->job_type); if (tt == jobTypeMap.end()) { @@ -438,6 +440,21 @@ public: } + 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) { + 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->hist_figure_id == hfid && + order->material_category.whole == mcat.whole && + order->frequency == df::manager_order::T_frequency::OneTime) + return order; + } + return NULL; + } + int place_orders() { int ordered = 0; @@ -505,11 +522,11 @@ public: if (!can_make) { - INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", name_p.c_str()); + INFO(cycle).print("tailor: civilization cannot make %s, skipped\n", DF2CONSOLE(name_p).c_str()); continue; } - DEBUG(cycle).print("tailor: ordering %d %s\n", count, name_p.c_str()); + DEBUG(cycle).print("tailor: ordering %d %s\n", count, DF2CONSOLE(name_p).c_str()); for (auto& m : material_order) { @@ -525,32 +542,40 @@ public: { c = supply[m] - res; TRACE(cycle).print("tailor: order reduced from %d to %d to protect reserves of %s\n", - count, c, m.name.c_str()); + count, c, DF2CONSOLE(m.name).c_str()); } supply[m] -= c; - auto 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_order_next_id++; - order->hist_figure_id = sizes[size]; - order->material_category = m.job_material; - - world->manager_orders.push_back(order); + 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_order_next_id++; + order->hist_figure_id = sizes[size]; + order->material_category = m.job_material; + + world->manager_orders.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(), - (c > 1) ? name_p.c_str() : name_s.c_str(), - world->raws.creatures.all[order->hist_figure_id]->name[1].c_str() + DF2CONSOLE((c > 1) ? name_p : name_s).c_str(), + DF2CONSOLE(world->raws.creatures.all[order->hist_figure_id]->name[1]).c_str() ); count -= c; @@ -558,7 +583,7 @@ public: } else { - TRACE(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", m.name.c_str(), supply[m]); + TRACE(cycle).print("tailor: material %s skipped due to lack of reserves, %d available\n", DF2CONSOLE(m.name).c_str(), supply[m]); } } @@ -588,7 +613,7 @@ static int do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(config,out).print("initializing %s\n", plugin_name); - tailor_instance = dts::make_unique(); + tailor_instance = std::make_unique(); // provide a configuration interface for the plugin commands.push_back(PluginCommand( diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 878014bb0..5d162302a 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -15,6 +15,13 @@ // - unassign single creature under cursor from current zone // - pitting own dwarves :) +#include "PluginManager.h" + +using namespace DFHack; + +DFHACK_PLUGIN("zone"); + +/* #include #include #include @@ -29,8 +36,6 @@ #include "df/unit_relationship_type.h" #include "df/viewscreen_dwarfmodest.h" #include "df/world.h" - -#include "PluginManager.h" #include "uicommon.h" #include "VTableInterpose.h" @@ -49,9 +54,6 @@ using std::unordered_map; using std::unordered_set; using std::vector; -using namespace DFHack; - -DFHACK_PLUGIN("zone"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(cursor); @@ -2177,11 +2179,12 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { return CR_OK; } +*/ DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand( - "zone", - "Manage activity zones.", - df_zone)); + // commands.push_back(PluginCommand( + // "zone", + // "Manage activity zones.", + // df_zone)); return CR_OK; } diff --git a/scripts b/scripts index 4c74bb905..0ab280651 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4c74bb9055c1c7a1bd49cbe535e2fc453cd79664 +Subproject commit 0ab280651ead18b8678ea081f4ab1b0edc044847 diff --git a/test/core.lua b/test/core.lua index ba104c90a..cc102984e 100644 --- a/test/core.lua +++ b/test/core.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local function clean_path(p) -- todo: replace with dfhack.filesystem call? return p:gsub('\\', '/'):gsub('//', '/'):gsub('/$', '') diff --git a/test/encoding.lua b/test/encoding.lua index cb0a72664..7a81f0ec4 100644 --- a/test/encoding.lua +++ b/test/encoding.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.toSearchNormalized() expect.eq(dfhack.toSearchNormalized(''), '') expect.eq(dfhack.toSearchNormalized('abcd'), 'abcd') diff --git a/test/gui.lua b/test/gui.lua index 1550fc1de..6bbb34117 100644 --- a/test/gui.lua +++ b/test/gui.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.getCurViewscreen() local scr = dfhack.gui.getCurViewscreen() local scr2 = df.global.gview.view @@ -18,7 +20,7 @@ function test.getViewscreenByType() local bad_type = df.viewscreen_titlest if scr._type == bad_type then - bad_type = df.viewscreen_optionst + bad_type = df.viewscreen_dwarfmodest end local scr_bad = dfhack.gui.getViewscreenByType(bad_type) expect.eq(scr_bad, nil) diff --git a/test/library/argparse.lua b/test/library/argparse.lua index 5e85e6465..437f1ea2b 100644 --- a/test/library/argparse.lua +++ b/test/library/argparse.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local argparse = require('argparse') local guidm = require('gui.dwarfmode') diff --git a/test/library/gui.lua b/test/library/gui.lua index fe04614d6..3fc8b889d 100644 --- a/test/library/gui.lua +++ b/test/library/gui.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local gui = require('gui') function test.getKeyDisplay() @@ -10,11 +12,13 @@ end function test.clear_pen() expect.table_eq(gui.CLEAR_PEN, { + tile = df.global.init.texpos_border_interior, ch = string.byte(' '), fg = COLOR_BLACK, bg = COLOR_BLACK, bold = false, tile_color = false, + write_to_lower = true, }) end diff --git a/test/library/gui/dialogs.lua b/test/library/gui/dialogs.lua index 0d3724c2e..524a8b144 100644 --- a/test/library/gui/dialogs.lua +++ b/test/library/gui/dialogs.lua @@ -1,3 +1,5 @@ +--config.target = 'core' + local gui = require('gui') local function send_keys(...) local keys = {...} diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index 23558987b..8418b67d4 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local widgets = require('gui.widgets') function test.editfield_cursor() @@ -20,18 +22,18 @@ function test.editfield_cursor() expect.eq('ones two threes', e.text) expect.eq(5, e.cursor) - e:onInput{CURSOR_LEFT=true} + e:onInput{KEYBOARD_CURSOR_LEFT=true} expect.eq(4, e.cursor) - e:onInput{CURSOR_RIGHT=true} + e:onInput{KEYBOARD_CURSOR_RIGHT=true} expect.eq(5, e.cursor) - e:onInput{A_CARE_MOVE_W=true} - expect.eq(1, e.cursor, 'interpret alt-left as home') - e:onInput{A_MOVE_E_DOWN=true} - expect.eq(6, e.cursor, 'interpret ctrl-right as goto beginning of next word') - e:onInput{A_CARE_MOVE_E=true} - expect.eq(16, e.cursor, 'interpret alt-right as end') - e:onInput{A_MOVE_W_DOWN=true} - expect.eq(9, e.cursor, 'interpret ctrl-left as goto end of previous word') + -- e:onInput{A_CARE_MOVE_W=true} + -- expect.eq(1, e.cursor, 'interpret alt-left as home') -- uncomment when we have a home key + e:onInput{CUSTOM_CTRL_F=true} + expect.eq(6, e.cursor, 'interpret Ctrl-f as goto beginning of next word') + e:onInput{CUSTOM_CTRL_E=true} + expect.eq(16, e.cursor, 'interpret Ctrl-e as end') + e:onInput{CUSTOM_CTRL_B=true} + expect.eq(9, e.cursor, 'interpret Ctrl-b as goto end of previous word') end function test.editfield_click() @@ -40,17 +42,17 @@ function test.editfield_click() expect.eq(5, e.cursor) mock.patch(e, 'getMousePos', mock.func(0), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(1, e.cursor) end) mock.patch(e, 'getMousePos', mock.func(20), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(5, e.cursor, 'should only seek to end of text') end) mock.patch(e, 'getMousePos', mock.func(2), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(3, e.cursor) end) end diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua index 4693d3d0d..09ead7ad5 100644 --- a/test/library/gui/widgets.Label.lua +++ b/test/library/gui/widgets.Label.lua @@ -1,7 +1,9 @@ +config.target = 'core' + local gui = require('gui') local widgets = require('gui.widgets') -local fs = defclass(fs, gui.FramedScreen) +local fs = defclass(nil, gui.FramedScreen) fs.ATTRS = { frame_style = gui.GREY_LINE_FRAME, frame_title = 'TestFramedScreen', diff --git a/test/library/gui/widgets.Scrollbar.lua b/test/library/gui/widgets.Scrollbar.lua index dbe033ba4..dd490256c 100644 --- a/test/library/gui/widgets.Scrollbar.lua +++ b/test/library/gui/widgets.Scrollbar.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local gui = require('gui') local widgets = require('gui.widgets') @@ -10,7 +12,7 @@ function test.update() expect.eq(1, s.elems_per_page) expect.eq(1, s.num_elems) expect.eq(0, s.bar_offset) - expect.eq(1, s.bar_height) + expect.eq(2, s.bar_height) -- top_elem, elems_per_page, num_elems s:update(1, 10, 0) @@ -18,7 +20,7 @@ function test.update() expect.eq(10, s.elems_per_page) expect.eq(0, s.num_elems) expect.eq(0, s.bar_offset) - expect.eq(1, s.bar_height) + expect.eq(2, s.bar_height) -- first 10 of 50 shown s:update(1, 10, 50) @@ -57,37 +59,37 @@ function test.onInput() s:update(23, 10, 50) expect.false_(s:onInput{}, 'no mouse down') - expect.false_(s:onInput{_MOUSE_L_DOWN=true}, 'no y coord') + expect.false_(s:onInput{_MOUSE_L=true}, 'no y coord') spec, y = nil, 0 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('up_small', spec, 'on up arrow') spec, y = nil, 1 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('up_large', spec, 'on body above bar') spec, y = nil, 44 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('up_large', spec, 'on body just above bar') spec, y = nil, 45 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.nil_(spec, 'on top of bar') spec, y = nil, 63 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.nil_(spec, 'on bottom of bar') spec, y = nil, 64 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('down_large', spec, 'on body just below bar') spec, y = nil, 98 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('down_large', spec, 'on body below bar') spec, y = nil, 99 - expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.true_(s:onInput{_MOUSE_L=true}) expect.eq('down_small', spec, 'on down arrow') end diff --git a/test/library/gui/widgets.lua b/test/library/gui/widgets.lua index 51622e691..b37fbe04d 100644 --- a/test/library/gui/widgets.lua +++ b/test/library/gui/widgets.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local widgets = require('gui.widgets') function test.hotkeylabel_click() @@ -5,7 +7,7 @@ function test.hotkeylabel_click() local l = widgets.HotkeyLabel{key='SELECT', on_activate=func} mock.patch(l, 'getMousePos', mock.func(0), function() - l:onInput{_MOUSE_L_DOWN=true} + l:onInput{_MOUSE_L=true} expect.eq(1, func.call_count) end) end @@ -31,7 +33,7 @@ function test.togglehotkeylabel_click() local l = widgets.ToggleHotkeyLabel{} expect.true_(l:getOptionValue()) mock.patch(l, 'getMousePos', mock.func(0), function() - l:onInput{_MOUSE_L_DOWN=true} + l:onInput{_MOUSE_L=true} expect.false_(l:getOptionValue()) end) end diff --git a/test/library/helpdb.lua b/test/library/helpdb.lua index 1f1e58ba9..7be2488df 100644 --- a/test/library/helpdb.lua +++ b/test/library/helpdb.lua @@ -1,3 +1,5 @@ +--config.target = 'core' + local h = require('helpdb') local mock_plugin_db = { diff --git a/test/library/misc.lua b/test/library/misc.lua index e746ec1a6..58afed967 100644 --- a/test/library/misc.lua +++ b/test/library/misc.lua @@ -1,5 +1,7 @@ -- tests misc functions added by dfhack.lua +config.target = 'core' + function test.safe_pairs() for k,v in safe_pairs(nil) do expect.fail('nil should not be iterable') diff --git a/test/library/print.lua b/test/library/print.lua index 3f9b5a78c..28a2e7037 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -1,5 +1,7 @@ -- tests print-related functions added by dfhack.lua +config.target = 'core' + local dfhack = dfhack local mock_print = mock.func() diff --git a/test/library/string.lua b/test/library/string.lua index d22f262bf..f6374f652 100644 --- a/test/library/string.lua +++ b/test/library/string.lua @@ -1,5 +1,7 @@ -- tests string functions added by dfhack.lua +config.target = 'core' + function test.startswith() expect.true_(('abcd'):startswith('')) expect.true_(('abcd'):startswith('abc')) diff --git a/test/library/test_util/expect_unit.lua b/test/library/test_util/expect_unit.lua index 1c3bd51e8..320e49707 100644 --- a/test/library/test_util/expect_unit.lua +++ b/test/library/test_util/expect_unit.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local expect_raw = require('test_util.expect') function test.str_find() diff --git a/test/library/test_util/mock.lua b/test/library/test_util/mock.lua index 1031a496a..ce25e6e69 100644 --- a/test/library/test_util/mock.lua +++ b/test/library/test_util/mock.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local mock = require('test_util.mock') local test_table = { diff --git a/test/library/utils.lua b/test/library/utils.lua index cf2024618..ac40bea1a 100644 --- a/test/library/utils.lua +++ b/test/library/utils.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local utils = require 'utils' function test.OrderedTable() @@ -102,7 +104,7 @@ function test.df_expr_to_ref() expect.eq(df.reinterpret_cast(df.world, utils.df_expr_to_ref('unit[0]').value), df.global.world) expect.eq(utils.df_expr_to_ref('unit[1]'), utils.df_expr_to_ref('unit.1')) - expect.eq(df.reinterpret_cast(df.ui, utils.df_expr_to_ref('unit[1]').value), df.global.plotinfo) + expect.eq(df.reinterpret_cast(df.plotinfost, utils.df_expr_to_ref('unit[1]').value), df.global.plotinfo) expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit.2') end) expect.error_match('index out of bounds', function() utils.df_expr_to_ref('unit[2]') end) diff --git a/test/modules/job.lua b/test/modules/job.lua new file mode 100644 index 000000000..518cb26a3 --- /dev/null +++ b/test/modules/job.lua @@ -0,0 +1,19 @@ +config.target = 'core' +config.mode = 'title' -- alters world state, not safe when a world is loaded + +function test.removeJob() + -- removeJob() calls DF code, so ensure that that DF code is actually running + + -- for an explanation of why this is necessary to check, + -- see https://github.com/DFHack/dfhack/pull/3713 and Job.cpp:removeJob() + + expect.nil_(df.global.world.jobs.list.next, 'job list is not empty') + + local job = df.job:new() -- will be deleted by removeJob() if the test passes + dfhack.job.linkIntoWorld(job) + expect.true_(df.global.world.jobs.list.next, 'job list is empty') + expect.eq(df.global.world.jobs.list.next.item, job, 'expected job not found in list') + + expect.true_(dfhack.job.removeJob(job)) + expect.nil_(df.global.world.jobs.list.next, 'job list is not empty after removeJob()') +end diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua index 6146aea3f..35d32e1ff 100644 --- a/test/plugins/blueprint.lua +++ b/test/plugins/blueprint.lua @@ -1,3 +1,5 @@ +config.target = 'blueprint' + local b = require('plugins.blueprint') -- also covers code shared between parse_gui_commandline and parse_commandline @@ -117,9 +119,9 @@ function test.parse_gui_commandline() function() b.parse_gui_commandline({}, {''}) end) opts = {} - b.parse_gui_commandline(opts, {'imaname', 'dig', 'query'}) + b.parse_gui_commandline(opts, {'imaname', 'dig', 'place'}) expect.table_eq({auto_phase=false, format='minimal', split_strategy='none', - name='imaname', dig=true, query=true}, + name='imaname', dig=true, place=true}, opts) expect.error_match('unknown phase', @@ -203,9 +205,9 @@ function test.do_phase_positive_dims() function() local spos = {x=10, y=20, z=30} local epos = {x=11, y=21, z=31} - b.query(spos, epos, 'imaname') + b.place(spos, epos, 'imaname') expect.eq(1, mock_run.call_count) - expect.table_eq({'2', '2', '2', 'imaname', 'query', + expect.table_eq({'2', '2', '2', 'imaname', 'place', '--cursor=10,20,30'}, mock_run.call_args[1]) end) @@ -217,9 +219,9 @@ function test.do_phase_negative_dims() function() local spos = {x=11, y=21, z=31} local epos = {x=10, y=20, z=30} - b.query(spos, epos, 'imaname') + b.place(spos, epos, 'imaname') expect.eq(1, mock_run.call_count) - expect.table_eq({'2', '2', '-2', 'imaname', 'query', + expect.table_eq({'2', '2', '-2', 'imaname', 'place', '--cursor=10,20,31'}, mock_run.call_args[1]) end) @@ -231,9 +233,9 @@ function test.do_phase_ensure_cursor_is_at_upper_left() function() local spos = {x=11, y=20, z=30} local epos = {x=10, y=21, z=31} - b.query(spos, epos, 'imaname') + b.place(spos, epos, 'imaname') expect.eq(1, mock_run.call_count) - expect.table_eq({'2', '2', '2', 'imaname', 'query', + expect.table_eq({'2', '2', '2', 'imaname', 'place', '--cursor=10,20,30'}, mock_run.call_args[1]) end) @@ -241,16 +243,16 @@ end function test.get_filename() local opts = {name='a', split_strategy='none'} - expect.eq('blueprints/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq('dfhack-config/blueprints/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='none'} - expect.eq('blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) + expect.eq('dfhack-config/blueprints/a/a.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a', split_strategy='phase'} - expect.eq('blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) + expect.eq('dfhack-config/blueprints/a-1-dig.csv', b.get_filename(opts, 'dig', 1)) opts = {name='a/', split_strategy='phase'} - expect.eq('blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) + expect.eq('dfhack-config/blueprints/a/a-5-dig.csv', b.get_filename(opts, 'dig', 5)) expect.error_match('could not parse basename', function() b.get_filename({name='', split_strategy='none'}) diff --git a/test/plugins/cxxrandom.lua b/test/plugins/cxxrandom.lua index 6b11e1937..997cb2cb3 100644 --- a/test/plugins/cxxrandom.lua +++ b/test/plugins/cxxrandom.lua @@ -1,3 +1,5 @@ +config.target = 'cxxrandom' + local rng = require('plugins.cxxrandom') function test.cxxrandom_distributions() diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index a0959ae94..d851adb02 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -1,4 +1,5 @@ config.mode = 'fortress' +config.target = 'orders' local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json' diff --git a/test/plugins/workflow.lua b/test/plugins/workflow.lua index a03d1c2f6..8d0c69c68 100644 --- a/test/plugins/workflow.lua +++ b/test/plugins/workflow.lua @@ -1,3 +1,5 @@ +config.target = 'workflow' + local workflow = require('plugins.workflow') function test.job_outputs() diff --git a/test/quickfort/ecosystem.lua b/test/quickfort/ecosystem.lua index 3a4c014e4..bbe16319f 100644 --- a/test/quickfort/ecosystem.lua +++ b/test/quickfort/ecosystem.lua @@ -26,6 +26,7 @@ -- crashing the game. config.mode = 'fortress' +config.target = {'quickfort', 'blueprint', 'dig-now', 'tiletypes', 'gui/quantum'} local argparse = require('argparse') local gui = require('gui') diff --git a/test/structures/find.lua b/test/structures/find.lua index c01f071e1..590df8971 100644 --- a/test/structures/find.lua +++ b/test/structures/find.lua @@ -1,4 +1,5 @@ -config.mode = 'title' +config.mode = 'title' -- not safe to run when a world is loaded +config.target = 'core' local function clean_vec(vec) while #vec > 0 do diff --git a/test/structures/globals.lua b/test/structures/globals.lua new file mode 100644 index 000000000..81d77f8c7 --- /dev/null +++ b/test/structures/globals.lua @@ -0,0 +1,32 @@ +config.target = 'core' + +local function with_temp_global_address(name, addr, callback, ...) + dfhack.call_with_finalizer(2, true, + dfhack.internal.setAddress, name, dfhack.internal.getAddress(name), + function(...) + dfhack.internal.setAddress(name, addr) + callback(...) + end, ...) +end + +function test.unknown_global_address() + expect.ne(dfhack.internal.getAddress('army_next_id'), 0) + local old_id = df.global.army_next_id + + with_temp_global_address('army_next_id', 0, function() + expect.error_match('Cannot read field global.army_next_id: global address not known.', function() + local _ = df.global.army_next_id + end) + + expect.error_match('Cannot write field global.army_next_id: global address not known.', function() + df.global.army_next_id = old_id + end) + + expect.error_match('Cannot reference field global.army_next_id: global address not known.', function() + local _ = df.global:_field('army_next_id') + end) + end) + + expect.gt(dfhack.internal.getAddress('army_next_id'), 0) + expect.eq(df.global.army_next_id, old_id) +end diff --git a/test/structures/misc.lua b/test/structures/misc.lua index 5c9f41561..13febff56 100644 --- a/test/structures/misc.lua +++ b/test/structures/misc.lua @@ -1,6 +1,8 @@ +config.target = 'core' + function test.overlappingGlobals() local globals = {} - for name, _ in pairs(df.global) do + for name in pairs(df.global) do local gvar = df.global:_field(name) local size, addr = gvar:sizeof() table.insert(globals, { @@ -26,7 +28,7 @@ function test.viewscreenDtors() for name, type in pairs(df) do if name:startswith('viewscreen') then print('testing', name) - v = type:new() + local v = type:new() expect.true_(v:delete(), "destructor returned false: " .. name) end end diff --git a/test/structures/other_vectors.lua b/test/structures/other_vectors.lua index bfc6086a4..aea00c27c 100644 --- a/test/structures/other_vectors.lua +++ b/test/structures/other_vectors.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.index_name() for _, k in ipairs(df.units_other_id) do expect.eq(df.global.world.units.other[k]._kind, 'container') diff --git a/test/structures/primitive_refs.lua b/test/structures/primitive_refs.lua index 74abd02b9..7bd7daf3c 100644 --- a/test/structures/primitive_refs.lua +++ b/test/structures/primitive_refs.lua @@ -1,3 +1,5 @@ +config.target = 'core' + utils = require('utils') function with_temp_ref(...) diff --git a/test/structures/ref_target.lua b/test/structures/ref_target.lua index b9c568805..560a5e4f7 100644 --- a/test/structures/ref_target.lua +++ b/test/structures/ref_target.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.get() dfhack.with_temp_object(df.unit:new(), function(unit) expect.eq(unit:_field('hist_figure_id').ref_target, df.historical_figure) diff --git a/test/structures/struct_fields.lua b/test/structures/struct_fields.lua new file mode 100644 index 000000000..01e4175d4 --- /dev/null +++ b/test/structures/struct_fields.lua @@ -0,0 +1,112 @@ +config.target = 'core' + +local COORD_FIELD_NAMES = {'x', 'y', 'z', 'isValid', 'clear'} +local COORD_FIELD_EXPECTED_DATA = { + x = {type_name='int16_t'}, + y = {type_name='int16_t'}, + z = {type_name='int16_t'}, + isValid = {type_name='function'}, + clear = {type_name='function'}, +} + +local READONLY_MSG = 'Attempt to change a read%-only table.' + +local function listFieldNames(t) + local names = {} + for name in pairs(t._fields) do + table.insert(names, name) + end + return names +end + +function test.access() + local fields = df.coord._fields + expect.true_(fields) + expect.eq(fields, df.coord._fields) + + for name, expected in pairs(COORD_FIELD_EXPECTED_DATA) do + expect.true_(fields[name], name) + expect.eq(fields[name].name, name, name) + expect.eq(fields[name].type_name, expected.type_name, name) + expect.eq(type(fields[name].offset), 'number', name) + expect.eq(type(fields[name].mode), 'number', name) + expect.eq(type(fields[name].count), 'number', name) + end +end + +function test.globals_original_name() + for name, info in pairs(df.global._fields) do + expect.eq(type(info.original_name), 'string', name) + end +end + +function test.order() + expect.table_eq(listFieldNames(df.coord), COORD_FIELD_NAMES) +end + +function test.nonexistent() + expect.nil_(df.coord._fields.nonexistent) + + expect.error_match('string expected', function() + expect.nil_(df.coord._fields[2]) + end) + expect.error_match('string expected', function() + expect.nil_(df.coord._fields[nil]) + end) +end + +function test.count() + expect.eq(df.unit._fields.relationship_ids.count, 10) +end + +function test.index_enum() + expect.eq(df.unit._fields.relationship_ids.index_enum, df.unit_relationship_type) +end + +function test.ref_target() + expect.eq(df.unit._fields.hist_figure_id.ref_target, df.historical_figure) +end + +function test.readonly() + expect.error_match(READONLY_MSG, function() + df.coord._fields.x = 'foo' + end) + expect.error_match(READONLY_MSG, function() + df.coord._fields.nonexistent = 'foo' + end) + expect.nil_(df.coord._fields.nonexistent) + + -- should have no effect + df.coord._fields.x.name = 'foo' + expect.eq(df.coord._fields.x.name, 'x') +end + +function test.circular_refs_init() + expect.eq(df.job._fields.list_link.type, df.job_list_link) + expect.eq(df.job_list_link._fields.item.type, df.job) +end + +function test.subclass_match() + for f, parent in pairs(df.viewscreen._fields) do + local child = df.viewscreen_titlest._fields[f] + expect.table_eq(parent, child, f) + end +end + +function test.subclass_order() + -- ensure that parent class fields come before subclass fields + local hierarchy = {df.item, df.item_actual, df.item_crafted, df.item_constructed, df.item_bedst} + local field_names = {} + for _, t in pairs(hierarchy) do + field_names[t] = listFieldNames(t) + end + for ic = 1, #hierarchy do + for ip = 1, ic - 1 do + local parent_fields = listFieldNames(hierarchy[ip]) + local child_fields = listFieldNames(hierarchy[ic]) + child_fields = table.pack(table.unpack(child_fields, 1, #parent_fields)) + child_fields.n = nil + expect.table_eq(child_fields, parent_fields, ('compare %s to %s'):format(hierarchy[ip], hierarchy[ic])) + end + end +end diff --git a/test/structures/types_meta.lua b/test/structures/types_meta.lua index 212e9c7ed..79395575f 100644 --- a/test/structures/types_meta.lua +++ b/test/structures/types_meta.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.struct() expect.eq(df.coord._kind, 'struct-type') expect.eq(tostring(df.coord), '') diff --git a/test/structures/unions.lua b/test/structures/unions.lua index 714786a96..a664506df 100644 --- a/test/structures/unions.lua +++ b/test/structures/unions.lua @@ -1,3 +1,5 @@ +config.target = 'core' + local utils = require('utils') function test.unit_action_fields() diff --git a/test/test.lua b/test/test.lua index 11f038d66..fbad06003 100644 --- a/test/test.lua +++ b/test/test.lua @@ -1,3 +1,5 @@ +config.target = 'core' + function test.internal_in_test() expect.true_(dfhack.internal.IN_TEST) end